summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2021-11-05 15:09:39 +0100
committerMartin Polden <mpolden@mpolden.no>2021-11-08 10:43:48 +0100
commitd4e336a38d8a6222053dc68447b707b172d7ef79 (patch)
tree12b09d64238abe990f28bf8e7469719812a09ed0 /controller-server
parent7b5ea3f61fb4dd529986d59cbec6d519c298f3f3 (diff)
Maintain ALIAS records for application-level endpoints
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java27
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java38
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingId.java21
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java201
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java38
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java178
10 files changed, 426 insertions, 94 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
index 6be62367407..5d02302795d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
@@ -139,6 +139,29 @@ public class RoutingController {
return EndpointList.copyOf(endpoints);
}
+ /** Returns application-scoped endpoints for given application */
+ public EndpointList endpointsOf(TenantAndApplicationId applicationId) {
+ Application app = controller.applications().requireApplication(applicationId);
+ Set<Endpoint> endpoints = new LinkedHashSet<>();
+ for (var declaredEndpoint : app.deploymentSpec().endpoints()) {
+ Map<DeploymentId, Integer> deployments = declaredEndpoint.targets().stream()
+ .collect(Collectors.toMap(t -> new DeploymentId(applicationId.instance(t.instance()),
+ ZoneId.from(Environment.prod, t.region())),
+ t -> t.weight()));
+ List<RoutingMethod> availableRoutingMethods = routingMethodsOfAll(deployments.keySet(), app.deploymentSpec());
+ for (var routingMethod : availableRoutingMethods) {
+ endpoints.add(Endpoint.of(applicationId)
+ .targetApplication(EndpointId.of(declaredEndpoint.endpointId()),
+ ClusterSpec.Id.from(declaredEndpoint.containerId()),
+ deployments)
+ .routingMethod(routingMethod)
+ .on(Port.fromRoutingMethod(routingMethod))
+ .in(controller.system()));
+ }
+ }
+ return EndpointList.copyOf(endpoints);
+ }
+
/** Returns all zone-scoped endpoints and corresponding cluster IDs for given deployments, grouped by their zone */
public Map<ZoneId, List<Endpoint>> zoneEndpointsOf(Collection<DeploymentId> deployments) {
var endpoints = new TreeMap<ZoneId, List<Endpoint>>(Comparator.comparing(ZoneId::value));
@@ -287,7 +310,7 @@ public class RoutingController {
}
/** Returns the routing methods that are available across all given deployments */
- private List<RoutingMethod> routingMethodsOfAll(List<DeploymentId> deployments, DeploymentSpec deploymentSpec) {
+ private List<RoutingMethod> routingMethodsOfAll(Collection<DeploymentId> deployments, DeploymentSpec deploymentSpec) {
var deploymentsByMethod = new HashMap<RoutingMethod, Set<DeploymentId>>();
for (var deployment : deployments) {
for (var method : controller.zoneRegistry().routingMethods(deployment.zoneId())) {
@@ -306,7 +329,7 @@ public class RoutingController {
}
/** Returns whether traffic can be directly routed to all given deployments */
- private boolean canRouteDirectlyTo(List<DeploymentId> deployments, DeploymentSpec deploymentSpec) {
+ private boolean canRouteDirectlyTo(Collection<DeploymentId> deployments, DeploymentSpec deploymentSpec) {
return deployments.stream().allMatch(deployment -> canRouteDirectlyTo(deployment, deploymentSpec));
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java
index 2c59211e50c..aecb1e7a2c1 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java
@@ -73,6 +73,11 @@ public class NameServiceForwarder {
forward(new RemoveRecords(type, data), priority);
}
+ /** Remove all records of given type, name and data */
+ public void removeRecords(Record.Type type, RecordName name, RecordData data, NameServiceQueue.Priority priority) {
+ forward(new RemoveRecords(type, name, data), priority);
+ }
+
protected void forward(NameServiceRequest request, NameServiceQueue.Priority priority) {
try (Lock lock = db.lockNameServiceQueue()) {
NameServiceQueue queue = db.readNameServiceQueue();
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java
index 6bc67af5b98..f940d53fab3 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.dns;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
@@ -12,7 +13,11 @@ import java.util.Objects;
import java.util.Optional;
/**
- * Permanently removes all matching records by type and name or data.
+ * Permanently removes all matching records by type and matching either:
+ *
+ * - name and data
+ * - only name
+ * - only data
*
* @author mpolden
*/
@@ -30,13 +35,17 @@ public class RemoveRecords implements NameServiceRequest {
this(type, Optional.empty(), Optional.of(data));
}
+ public RemoveRecords(Record.Type type, RecordName name, RecordData data) {
+ this(type, Optional.of(name), Optional.of(data));
+ }
+
/** DO NOT USE. Public for serialization purposes */
public RemoveRecords(Record.Type type, Optional<RecordName> name, Optional<RecordData> data) {
this.type = Objects.requireNonNull(type, "type must be non-null");
this.name = Objects.requireNonNull(name, "name must be non-null");
this.data = Objects.requireNonNull(data, "data must be non-null");
- if (name.isPresent() == data.isPresent()) {
- throw new IllegalArgumentException("exactly one of name or data must be non-empty");
+ if (name.isEmpty() && data.isEmpty()) {
+ throw new IllegalArgumentException("at least one of name and data must be non-empty");
}
}
@@ -55,8 +64,23 @@ public class RemoveRecords implements NameServiceRequest {
@Override
public void dispatchTo(NameService nameService) {
List<Record> records = new ArrayList<>();
- name.ifPresent(n -> records.addAll(nameService.findRecords(type, n)));
- data.ifPresent(d -> records.addAll(nameService.findRecords(type, d)));
+ if (name.isPresent() && data.isPresent()) {
+ nameService.findRecords(type, name.get())
+ .stream()
+ .filter(record -> {
+ // Records to remove must match both name and data fields
+ String dataValue = record.data().asString();
+ // If we're comparing an ALIAS record we have to unpack it to access the target name
+ if (record.type() == Record.Type.ALIAS) {
+ dataValue = AliasTarget.unpack(record.data()).name().value();
+ }
+ return fqdn(dataValue).equals(fqdn(data.get().asString()));
+ })
+ .forEach(records::add);
+ } else {
+ name.ifPresent(n -> records.addAll(nameService.findRecords(type, n)));
+ data.ifPresent(d -> records.addAll(nameService.findRecords(type, d)));
+ }
nameService.removeRecords(records);
}
@@ -82,4 +106,8 @@ public class RemoveRecords implements NameServiceRequest {
return Objects.hash(type, name, data);
}
+ private static String fqdn(String name) {
+ return name.endsWith(".") ? name : name + ".";
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index 2845dd53b24..17b15230458 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -1563,7 +1563,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
private void setGlobalEndpointStatus(DeploymentId deployment, boolean inService, HttpRequest request) {
var agent = isOperator(request) ? GlobalRouting.Agent.operator : GlobalRouting.Agent.tenant;
var status = inService ? GlobalRouting.Status.in : GlobalRouting.Status.out;
- controller.routing().policies().setGlobalRoutingStatus(deployment, status, agent);
+ controller.routing().policies().setRoutingStatus(deployment, status, agent);
}
/** Set the global rotation status for given deployment. This only applies to global endpoints backed by a rotation */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
index 34fcda3bff8..334ab0b9f73 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
@@ -211,7 +211,7 @@ public class RoutingApiHandler extends AuditLoggingRequestHandler {
var zone = zoneFrom(path);
if (controller.zoneRegistry().zones().directlyRouted().ids().contains(zone)) {
var status = in ? GlobalRouting.Status.in : GlobalRouting.Status.out;
- controller.routing().policies().setGlobalRoutingStatus(zone, status);
+ controller.routing().policies().setRoutingStatus(zone, status);
} else {
controller.serviceRegistry().configServer().setGlobalRotationStatus(zone, in);
}
@@ -256,7 +256,7 @@ public class RoutingApiHandler extends AuditLoggingRequestHandler {
}
// Set policy status
- controller.routing().policies().setGlobalRoutingStatus(deployment, status, agent);
+ controller.routing().policies().setRoutingStatus(deployment, status, agent);
return new MessageResponse("Set global routing status for " + deployment + " to " + (in ? "IN" : "OUT"));
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingId.java
index 343fa5417ce..67eafe6235d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingId.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingId.java
@@ -3,22 +3,30 @@ package com.yahoo.vespa.hosted.controller.routing;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.vespa.hosted.controller.application.EndpointId;
+import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import java.util.Objects;
/**
- * Unique identifier for a global routing table entry (instance x endpoint ID).
+ * Unique identifier for a instance routing table entry (instance x endpoint ID).
*
* @author mpolden
*/
public class RoutingId {
+ private final TenantAndApplicationId application;
private final ApplicationId instance;
private final EndpointId endpointId;
- public RoutingId(ApplicationId instance, EndpointId endpointId) {
- this.instance = Objects.requireNonNull(instance, "instance must be non-null");
+ private RoutingId(ApplicationId instance, EndpointId endpointId) {
+ this.instance = Objects.requireNonNull(instance, "application must be non-null");
this.endpointId = Objects.requireNonNull(endpointId, "endpointId must be non-null");
+
+ application = TenantAndApplicationId.from(instance);
+ }
+
+ public TenantAndApplicationId application() {
+ return application;
}
public ApplicationId instance() {
@@ -33,14 +41,13 @@ public class RoutingId {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- RoutingId that = (RoutingId) o;
- return instance.equals(that.instance) &&
- endpointId.equals(that.endpointId);
+ RoutingId routingId = (RoutingId) o;
+ return application.equals(routingId.application) && instance.equals(routingId.instance) && endpointId.equals(routingId.endpointId);
}
@Override
public int hashCode() {
- return Objects.hash(instance, endpointId);
+ return Objects.hash(application, instance, endpointId);
}
@Override
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
index e0c0df5234e..fdfb224b8ca 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
@@ -18,6 +18,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedAliasTarget;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.EndpointId;
+import com.yahoo.vespa.hosted.controller.application.EndpointList;
+import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.dns.NameServiceForwarder;
import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue.Priority;
import com.yahoo.vespa.hosted.controller.dns.NameServiceRequest;
@@ -32,11 +34,12 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
- * Updates routing policies and their associated DNS records based on an deployment's load balancers.
+ * Updates routing policies and their associated DNS records based on a deployment's load balancers.
*
* @author mortent
* @author mpolden
@@ -75,24 +78,29 @@ public class RoutingPolicies {
}
/**
- * Refresh routing policies for application in given zone. This is idempotent and changes will only be performed if
- * load balancers for given application have changed.
+ * Refresh routing policies for instance in given zone. This is idempotent and changes will only be performed if
+ * load balancers for given instance have changed.
*/
- public void refresh(ApplicationId application, DeploymentSpec deploymentSpec, ZoneId zone) {
- var allocation = new LoadBalancerAllocation(application, zone, controller.serviceRegistry().configServer()
- .getLoadBalancers(application, zone),
- deploymentSpec);
- var inactiveZones = inactiveZones(application, deploymentSpec);
+ public void refresh(ApplicationId instance, DeploymentSpec deploymentSpec, ZoneId zone) {
+ LoadBalancerAllocation allocation = new LoadBalancerAllocation(instance, zone, controller.serviceRegistry().configServer()
+ .getLoadBalancers(instance, zone),
+ deploymentSpec);
+ Set<ZoneId> inactiveZones = inactiveZones(instance, deploymentSpec);
try (var lock = db.lockRoutingPolicies()) {
removeGlobalDnsUnreferencedBy(allocation, lock);
+ removeApplicationDnsUnreferencedBy(allocation, lock);
+
storePoliciesOf(allocation, lock);
removePoliciesUnreferencedBy(allocation, lock);
- updateGlobalDnsOf(get(allocation.deployment.applicationId()).values(), inactiveZones, lock);
+
+ Collection<RoutingPolicy> policies = get(allocation.deployment.applicationId()).values();
+ updateGlobalDnsOf(policies, inactiveZones, lock);
+ updateApplicationDnsOf(policies, inactiveZones, lock);
}
}
/** Set the status of all global endpoints in given zone */
- public void setGlobalRoutingStatus(ZoneId zone, GlobalRouting.Status status) {
+ public void setRoutingStatus(ZoneId zone, GlobalRouting.Status status) {
try (var lock = db.lockRoutingPolicies()) {
db.writeZoneRoutingPolicy(new ZoneRoutingPolicy(zone, GlobalRouting.status(status, GlobalRouting.Agent.operator,
controller.clock().instant())));
@@ -104,24 +112,25 @@ public class RoutingPolicies {
}
/** Set the status of all global endpoints for given deployment */
- public void setGlobalRoutingStatus(DeploymentId deployment, GlobalRouting.Status status, GlobalRouting.Agent agent) {
+ public void setRoutingStatus(DeploymentId deployment, GlobalRouting.Status status, GlobalRouting.Agent agent) {
try (var lock = db.lockRoutingPolicies()) {
var policies = get(deployment.applicationId());
var newPolicies = new LinkedHashMap<>(policies);
for (var policy : policies.values()) {
- if (!policy.id().zone().equals(deployment.zoneId())) continue; // Wrong zone
+ if (!policy.appliesTo(deployment)) continue;
var newPolicy = policy.with(policy.status().with(GlobalRouting.status(status, agent,
controller.clock().instant())));
newPolicies.put(policy.id(), newPolicy);
}
db.writeRoutingPolicies(deployment.applicationId(), newPolicies);
updateGlobalDnsOf(newPolicies.values(), Set.of(), lock);
+ updateApplicationDnsOf(newPolicies.values(), Set.of(), lock);
}
}
/** Update global DNS records for given policies */
private void updateGlobalDnsOf(Collection<RoutingPolicy> routingPolicies, Set<ZoneId> inactiveZones, @SuppressWarnings("unused") Lock lock) {
- Map<RoutingId, List<RoutingPolicy>> routingTable = routingTableFrom(routingPolicies);
+ Map<RoutingId, List<RoutingPolicy>> routingTable = instanceRoutingTable(routingPolicies);
for (Map.Entry<RoutingId, List<RoutingPolicy>> routeEntry : routingTable.entrySet()) {
RoutingId routingId = routeEntry.getKey();
controller.routing().endpointsOf(routingId.instance())
@@ -167,7 +176,6 @@ public class RoutingPolicies {
Priority.normal));
}
-
/** Compute region endpoints and their targets from given policies */
private Collection<RegionEndpoint> computeRegionEndpoints(List<RoutingPolicy> policies, Set<ZoneId> inactiveZones) {
Map<Endpoint, RegionEndpoint> endpoints = new LinkedHashMap<>();
@@ -178,8 +186,8 @@ public class RoutingPolicies {
Endpoint regionEndpoint = policy.regionEndpointIn(controller.system(), routingMethod);
var zonePolicy = db.readZoneRoutingPolicy(policy.id().zone());
long weight = 1;
- if (isConfiguredOut(policy, zonePolicy, inactiveZones)) {
- weight = 0; // A record with 0 weight will not received traffic. If all records within a group have 0
+ if (isConfiguredOut(zonePolicy, policy, inactiveZones)) {
+ weight = 0; // A record with 0 weight will not receive traffic. If all records within a group have 0
// weight, traffic is routed to all records with equal probability.
}
var weightedTarget = new WeightedAliasTarget(policy.canonicalName(), policy.dnsZone().get(),
@@ -193,6 +201,43 @@ public class RoutingPolicies {
return endpoints.values();
}
+
+ private void updateApplicationDnsOf(Collection<RoutingPolicy> routingPolicies, Set<ZoneId> inactiveZones, @SuppressWarnings("unused") Lock lock) {
+ // In the context of single deployment (which this is) there is only one routing policy per routing ID. I.e.
+ // there is no scenario where more than one deployment within an instance can be a member the same
+ // application-level endpoint. However, to allow this in the future the routing table remains
+ // Map<RoutingId, List<RoutingPolicy>> instead of Map<RoutingId, RoutingPolicy>.
+ Map<RoutingId, List<RoutingPolicy>> routingTable = applicationRoutingTable(routingPolicies);
+ Map<String, Set<AliasTarget>> targetsByEndpoint = new LinkedHashMap<>();
+ for (Map.Entry<RoutingId, List<RoutingPolicy>> routeEntry : routingTable.entrySet()) {
+ RoutingId routingId = routeEntry.getKey();
+ EndpointList endpoints = controller.routing().endpointsOf(routingId.application())
+ .named(routingId.endpointId());
+ if (endpoints.isEmpty()) continue;
+ if (endpoints.size() > 1) {
+ throw new IllegalArgumentException("Expected at most 1 endpoint with ID '" + routingId.endpointId() +
+ ", got " + endpoints.size());
+ }
+ Endpoint endpoint = endpoints.asList().get(0);
+ for (var policy : routeEntry.getValue()) {
+ for (var target : endpoint.targets()) {
+ if (!policy.appliesTo(target.deployment())) continue;
+ int weight = target.weight();
+ if (isConfiguredOut(policy, inactiveZones)) {
+ weight = 0;
+ }
+ WeightedAliasTarget weightedAliasTarget = new WeightedAliasTarget(policy.canonicalName(), policy.dnsZone().get(),
+ target.deployment().zoneId(), weight);
+ targetsByEndpoint.computeIfAbsent(endpoint.dnsName(), (k) -> new LinkedHashSet<>())
+ .add(weightedAliasTarget);
+ }
+ }
+ }
+ targetsByEndpoint.forEach((applicationEndpoint, targets) -> {
+ controller.nameServiceForwarder().createAlias(RecordName.from(applicationEndpoint), targets, Priority.normal);
+ });
+ }
+
/** Store routing policies for given load balancers */
private void storePoliciesOf(LoadBalancerAllocation allocation, @SuppressWarnings("unused") Lock lock) {
var policies = new LinkedHashMap<>(get(allocation.deployment.applicationId()));
@@ -201,8 +246,8 @@ public class RoutingPolicies {
var policyId = new RoutingPolicyId(loadBalancer.application(), loadBalancer.cluster(), allocation.deployment.zoneId());
var existingPolicy = policies.get(policyId);
var newPolicy = new RoutingPolicy(policyId, loadBalancer.hostname().get(), loadBalancer.dnsZone(),
- allocation.endpointIdsOf(loadBalancer),
- Set.of(),
+ allocation.instanceEndpointsOf(loadBalancer),
+ allocation.applicationEndpointsOf(loadBalancer),
new Status(isActive(loadBalancer), GlobalRouting.DEFAULT_STATUS));
// Preserve global routing status for existing policy
if (existingPolicy != null) {
@@ -230,8 +275,7 @@ public class RoutingPolicies {
var activeIds = allocation.asPolicyIds();
for (var policy : policies.values()) {
// Leave active load balancers and irrelevant zones alone
- if (activeIds.contains(policy.id()) ||
- !policy.id().zone().equals(allocation.deployment.zoneId())) continue;
+ if (activeIds.contains(policy.id()) || !policy.appliesTo(allocation.deployment)) continue;
for (var endpoint : policy.zoneEndpointsIn(controller.system(), RoutingMethod.exclusive, controller.zoneRegistry())) {
var dnsName = endpoint.dnsName();
nameServiceForwarderIn(allocation.deployment.zoneId()).removeRecords(Record.Type.CNAME,
@@ -243,39 +287,86 @@ public class RoutingPolicies {
db.writeRoutingPolicies(allocation.deployment.applicationId(), newPolicies);
}
- /** Remove unreferenced global endpoints from DNS */
+ /** Remove unreferenced instance endpoints from DNS */
private void removeGlobalDnsUnreferencedBy(LoadBalancerAllocation allocation, @SuppressWarnings("unused") Lock lock) {
- var zonePolicies = get(allocation.deployment).values();
- var removalCandidates = new HashSet<>(routingTableFrom(zonePolicies).keySet());
- var activeRoutingIds = routingIdsFrom(allocation);
+ Collection<RoutingPolicy> zonePolicies = get(allocation.deployment).values();
+ Set<RoutingId> removalCandidates = new HashSet<>(instanceRoutingTable(zonePolicies).keySet());
+ Set<RoutingId> activeRoutingIds = instanceRoutingIds(allocation);
removalCandidates.removeAll(activeRoutingIds);
for (var id : removalCandidates) {
- var endpoints = controller.routing().endpointsOf(id.instance())
- .not().requiresRotation()
- .named(id.endpointId());
- var forwarder = nameServiceForwarderIn(allocation.deployment.zoneId());
+ EndpointList endpoints = controller.routing().endpointsOf(id.instance())
+ .not().requiresRotation()
+ .named(id.endpointId());
+ NameServiceForwarder forwarder = nameServiceForwarderIn(allocation.deployment.zoneId());
+ // This removes all ALIAS records having this DNS name. There is no attempt to delete only the entry for the
+ // affected zone. Instead, the correct set of records is (re)created by updateGlobalDnsOf
endpoints.forEach(endpoint -> forwarder.removeRecords(Record.Type.ALIAS, RecordName.from(endpoint.dnsName()),
Priority.normal));
}
}
+ /** Remove unreferenced application endpoints in given allocation from DNS */
+ private void removeApplicationDnsUnreferencedBy(LoadBalancerAllocation allocation, @SuppressWarnings("unused") Lock lock) {
+ Collection<RoutingPolicy> zonePolicies = get(allocation.deployment).values();
+ Map<RoutingId, List<RoutingPolicy>> routingTable = applicationRoutingTable(zonePolicies);
+ Set<RoutingId> removalCandidates = new HashSet<>(routingTable.keySet());
+ Set<RoutingId> activeRoutingIds = applicationRoutingIds(allocation);
+ removalCandidates.removeAll(activeRoutingIds);
+ for (var id : removalCandidates) {
+ TenantAndApplicationId application = TenantAndApplicationId.from(id.instance());
+ EndpointList endpoints = controller.routing()
+ .endpointsOf(application)
+ .named(id.endpointId());
+ List<RoutingPolicy> policies = routingTable.get(id);
+ for (var policy : policies) {
+ if (!policy.appliesTo(allocation.deployment)) continue;
+ NameServiceForwarder forwarder = nameServiceForwarderIn(policy.id().zone());
+ endpoints.forEach(endpoint -> forwarder.removeRecords(Record.Type.ALIAS,
+ RecordName.from(endpoint.dnsName()),
+ RecordData.fqdn(policy.canonicalName().value()),
+ Priority.normal));
+ }
+ }
+ }
+
+ private Set<RoutingId> instanceRoutingIds(LoadBalancerAllocation allocation) {
+ return routingIdsFrom(allocation, false);
+ }
+
+ private Set<RoutingId> applicationRoutingIds(LoadBalancerAllocation allocation) {
+ return routingIdsFrom(allocation, true);
+ }
+
/** Compute routing IDs from given load balancers */
- private static Set<RoutingId> routingIdsFrom(LoadBalancerAllocation allocation) {
+ private static Set<RoutingId> routingIdsFrom(LoadBalancerAllocation allocation, boolean applicationLevel) {
Set<RoutingId> routingIds = new LinkedHashSet<>();
for (var loadBalancer : allocation.loadBalancers) {
- for (var endpointId : allocation.endpointIdsOf(loadBalancer)) {
- routingIds.add(new RoutingId(loadBalancer.application(), endpointId));
+ Set<EndpointId> endpoints = applicationLevel
+ ? allocation.applicationEndpointsOf(loadBalancer)
+ : allocation.instanceEndpointsOf(loadBalancer);
+ for (var endpointId : endpoints) {
+ routingIds.add(RoutingId.of(loadBalancer.application(), endpointId));
}
}
return Collections.unmodifiableSet(routingIds);
}
- /** Compute a routing table from given policies */
- private static Map<RoutingId, List<RoutingPolicy>> routingTableFrom(Collection<RoutingPolicy> routingPolicies) {
- var routingTable = new LinkedHashMap<RoutingId, List<RoutingPolicy>>();
+ /** Compute a routing table for instance-level endpoints from given policies */
+ private static Map<RoutingId, List<RoutingPolicy>> instanceRoutingTable(Collection<RoutingPolicy> routingPolicies) {
+ return routingTable(routingPolicies, false);
+ }
+
+ /** Compute a routing table for application-level endpoints from given policies */
+ private static Map<RoutingId, List<RoutingPolicy>> applicationRoutingTable(Collection<RoutingPolicy> routingPolicies) {
+ return routingTable(routingPolicies, true);
+ }
+
+ private static Map<RoutingId, List<RoutingPolicy>> routingTable(Collection<RoutingPolicy> routingPolicies, boolean applicationLevel) {
+ Map<RoutingId, List<RoutingPolicy>> routingTable = new LinkedHashMap<>();
for (var policy : routingPolicies) {
- for (var endpoint : policy.instanceEndpoints()) {
- var id = new RoutingId(policy.id().owner(), endpoint);
+ Set<EndpointId> endpoints = applicationLevel ? policy.applicationEndpoints() : policy.instanceEndpoints();
+ for (var endpoint : endpoints) {
+ RoutingId id = RoutingId.of(policy.id().owner(), endpoint);
routingTable.computeIfAbsent(id, k -> new ArrayList<>())
.add(policy);
}
@@ -283,13 +374,22 @@ public class RoutingPolicies {
return Collections.unmodifiableMap(routingTable);
}
- /** Returns whether the global routing status of given policy is configured to be {@link GlobalRouting.Status#out} */
- private static boolean isConfiguredOut(RoutingPolicy policy, ZoneRoutingPolicy zonePolicy, Set<ZoneId> inactiveZones) {
- // A deployment is can be configured out at any of the following levels:
- // - zone level (ZoneRoutingPolicy)
+ /** Returns whether the endpoints of given policy are globally configured {@link GlobalRouting.Status#out} */
+ private static boolean isConfiguredOut(ZoneRoutingPolicy zonePolicy, RoutingPolicy policy, Set<ZoneId> inactiveZones) {
+ return isConfiguredOut(policy, Optional.of(zonePolicy), inactiveZones);
+ }
+
+ /** Returns whether the endpoints of given policy are configured {@link GlobalRouting.Status#out} */
+ private static boolean isConfiguredOut(RoutingPolicy policy, Set<ZoneId> inactiveZones) {
+ return isConfiguredOut(policy, Optional.empty(), inactiveZones);
+ }
+
+ private static boolean isConfiguredOut(RoutingPolicy policy, Optional<ZoneRoutingPolicy> zonePolicy, Set<ZoneId> inactiveZones) {
+ // A deployment can be configured out from endpoints at any of the following levels:
+ // - zone level (ZoneRoutingPolicy, only applies to global endpoints)
// - deployment level (RoutingPolicy)
// - application package level (deployment.xml)
- return zonePolicy.globalRouting().status() == GlobalRouting.Status.out ||
+ return (zonePolicy.isPresent() && zonePolicy.get().globalRouting().status() == GlobalRouting.Status.out) ||
policy.status().globalRouting().status() == GlobalRouting.Status.out ||
inactiveZones.contains(policy.id().zone());
}
@@ -363,8 +463,8 @@ public class RoutingPolicies {
.collect(Collectors.toUnmodifiableSet());
}
- /** Compute all endpoint IDs for given load balancer */
- private Set<EndpointId> endpointIdsOf(LoadBalancer loadBalancer) {
+ /** Returns all instance endpoint IDs served by given load balancer */
+ private Set<EndpointId> instanceEndpointsOf(LoadBalancer loadBalancer) {
if (!deployment.zoneId().environment().isProduction()) { // Only production deployments have configurable endpoints
return Set.of();
}
@@ -384,6 +484,21 @@ public class RoutingPolicies {
.collect(Collectors.toUnmodifiableSet());
}
+ /** Returns all application endpoint IDs served by given load balancer */
+ private Set<EndpointId> applicationEndpointsOf(LoadBalancer loadBalancer) {
+ if (!deployment.zoneId().environment().isProduction()) { // Only production deployments have configurable endpoints
+ return Set.of();
+ }
+ return deploymentSpec.endpoints().stream()
+ .filter(endpoint -> endpoint.containerId().equals(loadBalancer.cluster().value()))
+ .filter(endpoint -> endpoint.targets().stream()
+ .anyMatch(target -> target.region().equals(deployment.zoneId().region()) &&
+ target.instance().equals(deployment.applicationId().instance())))
+ .map(com.yahoo.config.application.api.Endpoint::endpointId)
+ .map(EndpointId::of)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
}
/** Returns zones where global routing is declared inactive for instance through deploymentSpec */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java
index 5653b51f6c9..ba8bf9c88ba 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java
@@ -75,6 +75,12 @@ public class RoutingPolicy {
return status;
}
+ /** Returns whether this policy applies to given deployment */
+ public boolean appliesTo(DeploymentId deployment) {
+ return id.owner().equals(deployment.applicationId()) &&
+ id.zone().equals(deployment.zoneId());
+ }
+
/** Returns a copy of this with status set to given status */
public RoutingPolicy with(Status status) {
return new RoutingPolicy(id, canonicalName, dnsZone, instanceEndpoints, applicationEndpoints, status);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java
index 64821756105..91a12d3b465 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java
@@ -5,6 +5,7 @@ import com.yahoo.component.Version;
import com.yahoo.config.application.api.ValidationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.AthenzService;
+import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
import com.yahoo.security.SignatureAlgorithm;
import com.yahoo.security.X509CertificateBuilder;
@@ -26,8 +27,10 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.OptionalInt;
import java.util.StringJoiner;
+import java.util.TreeMap;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -48,6 +51,7 @@ public class ApplicationPackageBuilder {
"<notifications>\n <email ",
"/>\n</notifications>\n").setEmptyValue("");
private final StringBuilder endpointsBody = new StringBuilder();
+ private final StringBuilder applicationEndpointsBody = new StringBuilder();
private final List<X509Certificate> trustedCertificates = new ArrayList<>();
private OptionalInt majorVersion = OptionalInt.empty();
@@ -86,9 +90,9 @@ public class ApplicationPackageBuilder {
return this;
}
- public ApplicationPackageBuilder endpoint(String endpointId, String containerId, String... regions) {
+ public ApplicationPackageBuilder endpoint(String id, String containerId, String... regions) {
endpointsBody.append(" <endpoint");
- endpointsBody.append(" id='").append(endpointId).append("'");
+ endpointsBody.append(" id='").append(id).append("'");
endpointsBody.append(" container-id='").append(containerId).append("'");
endpointsBody.append(">\n");
for (var region : regions) {
@@ -98,6 +102,23 @@ public class ApplicationPackageBuilder {
return this;
}
+ public ApplicationPackageBuilder applicationEndpoint(String id, String containerId, String region,
+ Map<InstanceName, Integer> instanceWeights) {
+ if (instanceWeights.isEmpty()) throw new IllegalArgumentException("At least one instance must be given");
+ applicationEndpointsBody.append(" <endpoint");
+ applicationEndpointsBody.append(" id='").append(id).append("'");
+ applicationEndpointsBody.append(" container-id='").append(containerId).append("'");
+ applicationEndpointsBody.append(" region='").append(region).append("'");
+ applicationEndpointsBody.append(">\n");
+ for (var kv : new TreeMap<>(instanceWeights).entrySet()) {
+ applicationEndpointsBody.append(" <instance weight='").append(kv.getValue().toString()).append("'>")
+ .append(kv.getKey().value())
+ .append("</instance>\n");
+ }
+ applicationEndpointsBody.append(" </endpoint>\n");
+ return this;
+ }
+
public ApplicationPackageBuilder systemTest() {
explicitSystemTest = true;
return this;
@@ -248,10 +269,17 @@ public class ApplicationPackageBuilder {
xml.append(">\n");
xml.append(prodBody);
xml.append(" </prod>\n");
- xml.append(" <endpoints>\n");
- xml.append(endpointsBody);
- xml.append(" </endpoints>\n");
+ if (endpointsBody.length() > 0 ) {
+ xml.append(" <endpoints>\n");
+ xml.append(endpointsBody);
+ xml.append(" </endpoints>\n");
+ }
xml.append(" </instance>\n");
+ if (applicationEndpointsBody.length() > 0) {
+ xml.append(" <endpoints>\n");
+ xml.append(applicationEndpointsBody);
+ xml.append(" </endpoints>\n");
+ }
xml.append("</deployment>\n");
return xml.toString().getBytes(UTF_8);
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java
index e2ef27492af..5fffa640811 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java
@@ -27,6 +27,7 @@ import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.EndpointId;
import com.yahoo.vespa.hosted.controller.application.EndpointList;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
+import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
@@ -165,8 +166,8 @@ public class RoutingPoliciesTest {
tester.policiesOf(context.instance().id()).size());
// A zone in shared region is set out
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone4), GlobalRouting.Status.out,
- GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone4), GlobalRouting.Status.out,
+ GlobalRouting.Agent.tenant);
context.flushDnsUpdates();
// Weight of inactive zone is set to zero
@@ -176,16 +177,16 @@ public class RoutingPoliciesTest {
// Other zone in shared region is set out. Entire record group for the region is removed as all zones in the
// region are out (weight sum = 0)
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone3), GlobalRouting.Status.out,
- GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone3), GlobalRouting.Status.out,
+ GlobalRouting.Agent.tenant);
context.flushDnsUpdates();
tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, ImmutableMap.of(zone1, 1L));
// Everything is set back in
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone3), GlobalRouting.Status.in,
- GlobalRouting.Agent.tenant);
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone4), GlobalRouting.Status.in,
- GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone3), GlobalRouting.Status.in,
+ GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone4), GlobalRouting.Status.in,
+ GlobalRouting.Agent.tenant);
context.flushDnsUpdates();
tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, ImmutableMap.of(zone1, 1L,
zone3, 1L,
@@ -480,8 +481,8 @@ public class RoutingPoliciesTest {
// Global routing status is overridden in one zone
var changedAt = tester.controllerTester().clock().instant();
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone1), GlobalRouting.Status.out,
- GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone1), GlobalRouting.Status.out,
+ GlobalRouting.Agent.tenant);
context.flushDnsUpdates();
// Inactive zone is removed from global DNS record
@@ -509,7 +510,7 @@ public class RoutingPoliciesTest {
// Deployment is set back in
tester.controllerTester().clock().advance(Duration.ofHours(1));
changedAt = tester.controllerTester().clock().instant();
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone1), GlobalRouting.Status.in, GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone1), GlobalRouting.Status.in, GlobalRouting.Agent.tenant);
context.flushDnsUpdates();
tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1, zone2);
tester.assertTargets(context.instanceId(), EndpointId.of("r1"), 0, zone1, zone2);
@@ -562,7 +563,7 @@ public class RoutingPoliciesTest {
}
// Set zone out
- tester.routingPolicies().setGlobalRoutingStatus(zone2, GlobalRouting.Status.out);
+ tester.routingPolicies().setRoutingStatus(zone2, GlobalRouting.Status.out);
context1.flushDnsUpdates();
tester.assertTargets(context1.instanceId(), EndpointId.defaultId(), 0, zone1);
tester.assertTargets(context2.instanceId(), EndpointId.defaultId(), 0, zone1);
@@ -582,17 +583,17 @@ public class RoutingPoliciesTest {
assertEquals(changedAt.truncatedTo(ChronoUnit.MILLIS), zonePolicy.globalRouting().changedAt());
// Setting status per deployment does not affect status as entire zone is out
- tester.routingPolicies().setGlobalRoutingStatus(context1.deploymentIdIn(zone2), GlobalRouting.Status.in, GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context1.deploymentIdIn(zone2), GlobalRouting.Status.in, GlobalRouting.Agent.tenant);
context1.flushDnsUpdates();
tester.assertTargets(context1.instanceId(), EndpointId.defaultId(), 0, zone1);
tester.assertTargets(context2.instanceId(), EndpointId.defaultId(), 0, zone1);
// Set single deployment out
- tester.routingPolicies().setGlobalRoutingStatus(context1.deploymentIdIn(zone2), GlobalRouting.Status.out, GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context1.deploymentIdIn(zone2), GlobalRouting.Status.out, GlobalRouting.Agent.tenant);
context1.flushDnsUpdates();
// Set zone back in. Deployment set explicitly out, remains out, the rest are in
- tester.routingPolicies().setGlobalRoutingStatus(zone2, GlobalRouting.Status.in);
+ tester.routingPolicies().setRoutingStatus(zone2, GlobalRouting.Status.in);
context1.flushDnsUpdates();
tester.assertTargets(context1.instanceId(), EndpointId.defaultId(), 0, zone1);
tester.assertTargets(context2.instanceId(), EndpointId.defaultId(), 0, zone1, zone2);
@@ -646,38 +647,38 @@ public class RoutingPoliciesTest {
tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1, zone2);
// Global routing status is overridden for one deployment
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone1), GlobalRouting.Status.out,
- GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone1), GlobalRouting.Status.out,
+ GlobalRouting.Agent.tenant);
context.flushDnsUpdates();
tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone2);
// Setting other deployment out implicitly sets all deployments in. Weight is set to zero, but that has no
// impact on routing decisions when the weight sum is zero
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone2), GlobalRouting.Status.out,
- GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone2), GlobalRouting.Status.out,
+ GlobalRouting.Agent.tenant);
context.flushDnsUpdates();
tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, ImmutableMap.of(zone1, 0L, zone2, 0L));
// One inactive deployment is put back in. Global DNS record now points to the only active deployment
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone1), GlobalRouting.Status.in,
- GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone1), GlobalRouting.Status.in,
+ GlobalRouting.Agent.tenant);
context.flushDnsUpdates();
tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1);
// Setting zone (containing active deployment) out puts all deployments in
- tester.routingPolicies().setGlobalRoutingStatus(zone1, GlobalRouting.Status.out);
+ tester.routingPolicies().setRoutingStatus(zone1, GlobalRouting.Status.out);
context.flushDnsUpdates();
assertEquals(GlobalRouting.Status.out, tester.routingPolicies().get(zone1).globalRouting().status());
tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, ImmutableMap.of(zone1, 0L, zone2, 0L));
// Setting zone back in removes the currently inactive deployment
- tester.routingPolicies().setGlobalRoutingStatus(zone1, GlobalRouting.Status.in);
+ tester.routingPolicies().setRoutingStatus(zone1, GlobalRouting.Status.in);
context.flushDnsUpdates();
tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1);
// Inactive deployment is set in
- tester.routingPolicies().setGlobalRoutingStatus(context.deploymentIdIn(zone2), GlobalRouting.Status.in,
- GlobalRouting.Agent.tenant);
+ tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone2), GlobalRouting.Status.in,
+ GlobalRouting.Agent.tenant);
context.flushDnsUpdates();
for (var policy : tester.routingPolicies().get(context.instanceId()).values()) {
assertSame(GlobalRouting.Status.in, policy.status().globalRouting().status());
@@ -701,12 +702,103 @@ public class RoutingPoliciesTest {
records.get(0).data());
}
+ @Test
+ public void application_endpoint_routing_policy() {
+ RoutingPoliciesTester tester = new RoutingPoliciesTester();
+ TenantAndApplicationId application = TenantAndApplicationId.from("tenant1", "app1");
+ ApplicationId betaInstance = application.instance("beta");
+ ApplicationId mainInstance = application.instance("main");
+
+ DeploymentContext betaContext = tester.newDeploymentContext(betaInstance);
+ DeploymentContext mainContext = tester.newDeploymentContext(mainInstance);
+ var applicationPackage = applicationPackageBuilder()
+ .instances("beta,main")
+ .region(zone1.region())
+ .region(zone2.region())
+ .applicationEndpoint("a0", "c0", "us-west-1",
+ Map.of(betaInstance.instance(), 2,
+ mainInstance.instance(), 8))
+ .applicationEndpoint("a1", "c1", "us-central-1",
+ Map.of(betaInstance.instance(), 4,
+ mainInstance.instance(), 6))
+ .build();
+ for (var zone : List.of(zone1, zone2)) {
+ tester.provisionLoadBalancers(2, betaInstance, zone);
+ tester.provisionLoadBalancers(2, mainInstance, zone);
+ }
+
+ // Deploy both instances
+ betaContext.submit(applicationPackage).deploy();
+
+ // Application endpoint points to both instances with correct weights
+ DeploymentId betaZone1 = betaContext.deploymentIdIn(zone1);
+ DeploymentId mainZone1 = mainContext.deploymentIdIn(zone1);
+ DeploymentId betaZone2 = betaContext.deploymentIdIn(zone2);
+ DeploymentId mainZone2 = mainContext.deploymentIdIn(zone2);
+ tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
+ Map.of(betaZone1, 2,
+ mainZone1, 8));
+ tester.assertTargets(application, EndpointId.of("a1"), ClusterSpec.Id.from("c1"), 1,
+ Map.of(betaZone2, 4,
+ mainZone2, 6));
+
+ // Weights are updated
+ applicationPackage = applicationPackageBuilder()
+ .instances("beta,main")
+ .region(zone1.region())
+ .region(zone2.region())
+ .applicationEndpoint("a0", "c0", "us-west-1",
+ Map.of(betaInstance.instance(), 3,
+ mainInstance.instance(), 7))
+ .applicationEndpoint("a1", "c1", "us-central-1",
+ Map.of(betaInstance.instance(), 1,
+ mainInstance.instance(), 9))
+ .build();
+ betaContext.submit(applicationPackage).deploy();
+ tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
+ Map.of(betaZone1, 3,
+ mainZone1, 7));
+ tester.assertTargets(application, EndpointId.of("a1"), ClusterSpec.Id.from("c1"), 1,
+ Map.of(betaZone2, 1,
+ mainZone2, 9));
+
+ // Changing routing status updates weight
+ tester.routingPolicies().setRoutingStatus(mainZone2, GlobalRouting.Status.out, RoutingStatus.Agent.tenant);
+ betaContext.flushDnsUpdates();
+ tester.assertTargets(application, EndpointId.of("a1"), ClusterSpec.Id.from("c1"), 1,
+ Map.of(betaZone2, 1,
+ mainZone2, 0));
+ tester.routingPolicies().setRoutingStatus(mainZone2, GlobalRouting.Status.in, GlobalRouting.Agent.tenant);
+ betaContext.flushDnsUpdates();
+ tester.assertTargets(application, EndpointId.of("a1"), ClusterSpec.Id.from("c1"), 1,
+ Map.of(betaZone2, 1,
+ mainZone2, 9));
+
+ // An endpoint is removed
+ applicationPackage = applicationPackageBuilder()
+ .instances("beta,main")
+ .region(zone1.region())
+ .region(zone2.region())
+ .applicationEndpoint("a0", "c0", "us-west-1",
+ Map.of(betaInstance.instance(), 1))
+ .build();
+ betaContext.submit(applicationPackage).deploy();
+
+ // Application endpoints now point to a single instance
+ tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
+ Map.of(betaZone1, 1));
+ assertTrue("Endpoint removed",
+ tester.controllerTester().controller().routing()
+ .endpointsOf(application)
+ .named(EndpointId.of("a1")).isEmpty());
+ }
+
/** Returns an application package builder that satisfies requirements for a directly routed endpoint */
private static ApplicationPackageBuilder applicationPackageBuilder() {
- return new ApplicationPackageBuilder()
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"));
+ return new ApplicationPackageBuilder().athenzIdentity(AthenzDomain.from("domain"),
+ AthenzService.from("service"));
}
-
+
private static List<LoadBalancer> createLoadBalancers(ZoneId zone, ApplicationId application, boolean shared, int count) {
List<LoadBalancer> loadBalancers = new ArrayList<>();
for (int i = 0; i < count; i++) {
@@ -819,7 +911,35 @@ public class RoutingPoliciesTest {
.collect(Collectors.toList());
}
- private void assertTargets(ApplicationId instance, EndpointId endpointId, ClusterSpec.Id cluster, int loadBalancerId, Map<ZoneId, Long> zoneWeights) {
+ /** Assert that an application endpoint points to given targets and weights */
+ private void assertTargets(TenantAndApplicationId application, EndpointId endpointId, ClusterSpec.Id cluster,
+ int loadBalancerId, Map<DeploymentId, Integer> deploymentWeights) {
+ Map<String, List<DeploymentId>> deploymentsByDnsName = new HashMap<>();
+ for (var deployment : deploymentWeights.keySet()) {
+ EndpointList applicationEndpoints = tester.controller().routing().endpointsOf(application)
+ .named(endpointId)
+ .targets(deployment)
+ .cluster(cluster);
+ assertEquals("Expected a single endpoint with ID '" + endpointId + "'", 1,
+ applicationEndpoints.size());
+ String dnsName = applicationEndpoints.asList().get(0).dnsName();
+ deploymentsByDnsName.computeIfAbsent(dnsName, (k) -> new ArrayList<>())
+ .add(deployment);
+ }
+ assertEquals("Found " + endpointId + " for " + application, 1, deploymentsByDnsName.size());
+ deploymentsByDnsName.forEach((dnsName, deployments) -> {
+ Set<String> weightedTargets = deployments.stream()
+ .map(d -> "weighted/lb-" + loadBalancerId + "--" +
+ d.applicationId().serializedForm() + "--" + d.zoneId().value() +
+ "/dns-zone-1/" + d.zoneId().value() + "/" + deploymentWeights.get(d))
+ .collect(Collectors.toSet());
+ assertEquals(dnsName + " has expected targets", weightedTargets, aliasDataOf(dnsName));
+ });
+ }
+
+ /** Assert that an instance endpoint points to given targets and weights */
+ private void assertTargets(ApplicationId instance, EndpointId endpointId, ClusterSpec.Id cluster,
+ int loadBalancerId, Map<ZoneId, Long> zoneWeights) {
Set<String> latencyTargets = new HashSet<>();
Map<String, List<ZoneId>> zonesByRegionEndpoint = new HashMap<>();
for (var zone : zoneWeights.keySet()) {