summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorØyvind Grønnesby <oyving@verizonmedia.com>2019-07-03 09:29:53 +0200
committerGitHub <noreply@github.com>2019-07-03 09:29:53 +0200
commitf8f0ae1e90f40a4c2d3fca3ef0fa98fa0a4ee1b2 (patch)
tree95e1f986b049ae2bbde14bd2319565bcefe2538b
parent1c5ffa84dc4752e243399e77284fecc07dbd3b09 (diff)
parent3f176936be8b6a33da51238bc35c42cdae24881a (diff)
Merge pull request #9919 from vespa-engine/ogronnesby/assign-multiple-rotations
Assign multiple rotations per application
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/Endpoint.java2
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java11
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java13
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java122
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java27
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java18
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java127
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java211
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java44
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java22
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java22
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java6
16 files changed, 560 insertions, 108 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/Endpoint.java b/config-model-api/src/main/java/com/yahoo/config/application/api/Endpoint.java
index ca8eadd8d1f..158fbfb175f 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/Endpoint.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/Endpoint.java
@@ -13,7 +13,7 @@ import java.util.stream.Collectors;
* endpoint (endpointId) and the name of the container cluster that the endpoint
* should point to.
*
- * If the endpointId is not set, it will default to the same as the containerId.
+ * If the endpoint is not set it will default to the string "default".
*
* @author ogronnesby
*/
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
index 650f68591b6..72a806bb7be 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
@@ -27,6 +27,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -161,7 +162,7 @@ public class DeploymentSpecXmlReader {
final var endpointsElement = XML.getChild(root, endpointsTag);
if (endpointsElement == null) { return Collections.emptyList(); }
- final var endpoints = new ArrayList<Endpoint>();
+ final var endpoints = new LinkedHashMap<String, Endpoint>();
for (var endpointElement : XML.getChildren(endpointsElement, endpointTag)) {
final Optional<String> rotationId = stringAttribute("id", endpointElement);
@@ -184,13 +185,13 @@ public class DeploymentSpecXmlReader {
}
var endpoint = new Endpoint(rotationId, containerId.get(), regions);
- if (endpoints.contains(endpoint)) {
- throw new IllegalArgumentException("Duplicate 'endpoint' in 'endpoints' tag");
+ if (endpoints.containsKey(endpoint.endpointId())) {
+ throw new IllegalArgumentException("Duplicate attribute 'id' on 'endpoint': " + endpoint.endpointId());
}
- endpoints.add(endpoint);
+ endpoints.put(endpoint.endpointId(), endpoint);
}
- return endpoints;
+ return List.copyOf(endpoints.values());
}
/**
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
index ba00203ec34..9eae2965c45 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
@@ -30,7 +30,7 @@ public interface ConfigServer {
}
PreparedApplication deploy(DeploymentId deployment, DeployOptions deployOptions, Set<String> rotationNames,
- List<ContainerEndpoint> containerEndpoints, ApplicationCertificate applicationCertificate, byte[] content);
+ Set<ContainerEndpoint> containerEndpoints, ApplicationCertificate applicationCertificate, byte[] content);
void restart(DeploymentId deployment, Optional<Hostname> hostname);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
index 9ca73d27120..d8b56502fc3 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
@@ -20,6 +20,7 @@ import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
+import com.yahoo.vespa.hosted.controller.application.EndpointId;
import com.yahoo.vespa.hosted.controller.application.EndpointList;
import com.yahoo.vespa.hosted.controller.application.RotationStatus;
import com.yahoo.vespa.hosted.controller.rotation.RotationId;
@@ -218,12 +219,18 @@ public class Application {
return rotations;
}
+ /** Returns the default global endpoints for this in given system - for a given endpoint ID */
+ public EndpointList endpointsIn(SystemName system, EndpointId endpointId) {
+ if (rotations.isEmpty()) return EndpointList.EMPTY;
+ return EndpointList.create(id, endpointId, system);
+ }
+
/** Returns the default global endpoints for this in given system */
public EndpointList endpointsIn(SystemName system) {
- // TODO: Do we need to change something here? .defaultGlobalId seems like it is
- // TODO: making some assumptions on naming.
if (rotations.isEmpty()) return EndpointList.EMPTY;
- return EndpointList.defaultGlobal(id, system);
+ final var endpointStream = rotations.stream()
+ .flatMap(rotation -> EndpointList.create(id, rotation.endpointId(), system).asList().stream());
+ return EndpointList.of(endpointStream);
}
public Optional<String> pemDeployKey() { return pemDeployKey; }
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
index 197dda8c409..d015e65a5e1 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.controller;
import com.google.common.collect.ImmutableList;
+import com.yahoo.collections.ArraySet;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationId;
@@ -9,6 +10,7 @@ import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.athenz.api.AthenzDomain;
@@ -30,6 +32,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.certificates.Applicatio
import com.yahoo.vespa.hosted.controller.api.integration.certificates.ApplicationCertificateProvider;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NotFoundException;
@@ -87,6 +90,7 @@ import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -97,6 +101,7 @@ import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.active;
import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.reserved;
@@ -126,6 +131,7 @@ public class ApplicationController {
private final RoutingPolicies routingPolicies;
private final Clock clock;
private final BooleanFlag redirectLegacyDnsFlag;
+ private final BooleanFlag useMultipleEndpoints;
private final DeploymentTrigger deploymentTrigger;
private final BooleanFlag provisionApplicationCertificate;
private final ApplicationCertificateProvider applicationCertificateProvider;
@@ -143,6 +149,7 @@ public class ApplicationController {
this.routingPolicies = new RoutingPolicies(controller);
this.clock = clock;
this.redirectLegacyDnsFlag = Flags.REDIRECT_LEGACY_DNS_NAMES.bindTo(controller.flagSource());
+ this.useMultipleEndpoints = Flags.MULTIPLE_GLOBAL_ENDPOINTS.bindTo(controller.flagSource());
this.artifactRepository = artifactRepository;
this.applicationStore = applicationStore;
@@ -293,7 +300,8 @@ public class ApplicationController {
Version platformVersion;
ApplicationVersion applicationVersion;
ApplicationPackage applicationPackage;
- Set<String> rotationNames = new HashSet<>();
+ Set<String> legacyRotations = new LinkedHashSet<>();
+ Set<ContainerEndpoint> endpoints = new LinkedHashSet<>();
ApplicationCertificate applicationCertificate;
try (Lock lock = lock(applicationId)) {
@@ -332,13 +340,34 @@ public class ApplicationController {
// TODO: Remove this when all packages are validated upon submission, as in ApplicationApiHandler.submit(...).
verifyApplicationIdentityConfiguration(applicationId.tenant(), applicationPackage, deployingIdentity);
+
// Assign global rotation
- application = withRotation(application, zone);
- Application app = application.get();
- // Include global DNS names
- app.endpointsIn(controller.system()).asList().stream().map(Endpoint::dnsName).forEach(rotationNames::add);
- // Include rotation ID to ensure that deployment can respond to health checks with rotation ID as Host header
- app.rotations().stream().map(RotationId::asString).forEach(rotationNames::add);
+ if (useMultipleEndpoints.with(FetchVector.Dimension.APPLICATION_ID, application.get().id().serializedForm()).value()) {
+ application = withRotation(application, zone);
+
+ // Include global DNS names
+ Application app = application.get();
+ app.assignedRotations().stream()
+ .filter(assignedRotation -> assignedRotation.regions().contains(zone.region()))
+ .map(assignedRotation -> {
+ return new ContainerEndpoint(
+ assignedRotation.clusterId().value(),
+ Stream.concat(
+ app.endpointsIn(controller.system(), assignedRotation.endpointId()).legacy(false).asList().stream().map(Endpoint::dnsName),
+ app.rotations().stream().map(RotationId::asString)
+ ).collect(Collectors.toList())
+ );
+ })
+ .forEach(endpoints::add);
+ } else {
+ application = withRotationLegacy(application, zone);
+
+ // Add both the names we have in DNS for each endpoint as well as name of the rotation so healthchecks works
+ Application app = application.get();
+ app.endpointsIn(controller.system()).asList().stream().map(Endpoint::dnsName).forEach(legacyRotations::add);
+ app.rotations().stream().map(RotationId::asString).forEach(legacyRotations::add);
+ }
+
// Get application certificate (provisions a new certificate if missing)
application = withApplicationCertificate(application);
@@ -354,7 +383,7 @@ public class ApplicationController {
// Carry out deployment without holding the application lock.
options = withVersion(platformVersion, options);
- ActivateResult result = deploy(applicationId, applicationPackage, zone, options, rotationNames, applicationCertificate);
+ ActivateResult result = deploy(applicationId, applicationPackage, zone, options, legacyRotations, endpoints, applicationCertificate);
lockOrThrow(applicationId, application ->
store(application.withNewDeployment(zone, applicationVersion, platformVersion, clock.instant(),
@@ -421,7 +450,7 @@ public class ApplicationController {
artifactRepository.getSystemApplicationPackage(application.id(), zone, version)
);
DeployOptions options = withVersion(version, DeployOptions.none());
- return deploy(application.id(), applicationPackage, zone, options, Set.of(), /* No application cert */ null);
+ return deploy(application.id(), applicationPackage, zone, options, Set.of(), Set.of(), /* No application cert */ null);
} else {
throw new RuntimeException("This system application does not have an application package: " + application.id().toShortString());
}
@@ -429,16 +458,16 @@ public class ApplicationController {
/** Deploys the given tester application to the given zone. */
public ActivateResult deployTester(TesterId tester, ApplicationPackage applicationPackage, ZoneId zone, DeployOptions options) {
- return deploy(tester.id(), applicationPackage, zone, options, Set.of(), /* No application cert for tester*/ null);
+ return deploy(tester.id(), applicationPackage, zone, options, Set.of(), Set.of(), /* No application cert for tester*/ null);
}
private ActivateResult deploy(ApplicationId application, ApplicationPackage applicationPackage,
ZoneId zone, DeployOptions deployOptions,
- Set<String> rotationNames, ApplicationCertificate applicationCertificate) {
+ Set<String> legacyRotations, Set<ContainerEndpoint> endpoints, ApplicationCertificate applicationCertificate) {
DeploymentId deploymentId = new DeploymentId(application, zone);
try {
ConfigServer.PreparedApplication preparedApplication =
- configServer.deploy(deploymentId, deployOptions, rotationNames, List.of(), applicationCertificate, applicationPackage.zippedContent());
+ configServer.deploy(deploymentId, deployOptions, legacyRotations, endpoints, applicationCertificate, applicationPackage.zippedContent());
return new ActivateResult(new RevisionId(applicationPackage.hash()), preparedApplication.prepareResponse(),
applicationPackage.zippedContent().length);
} finally {
@@ -449,19 +478,20 @@ public class ApplicationController {
}
/** Makes sure the application has a global rotation, if eligible. */
- private LockedApplication withRotation(LockedApplication application, ZoneId zone) {
+ private LockedApplication withRotationLegacy(LockedApplication application, ZoneId zone) {
if (zone.environment() == Environment.prod && application.get().deploymentSpec().globalServiceId().isPresent()) {
try (RotationLock rotationLock = rotationRepository.lock()) {
Rotation rotation = rotationRepository.getOrAssignRotation(application.get(), rotationLock);
- application = application.with(List.of(new AssignedRotation(new ClusterSpec.Id(application.get().deploymentSpec().globalServiceId().get()), EndpointId.default_(), rotation.id())));
+ application = application.with(createDefaultGlobalIdRotation(application.get(), rotation));
store(application); // store assigned rotation even if deployment fails
boolean redirectLegacyDns = redirectLegacyDnsFlag.with(FetchVector.Dimension.APPLICATION_ID, application.get().id().serializedForm())
- .value();
+ .value();
EndpointList globalEndpoints = application.get()
- .endpointsIn(controller.system())
- .scope(Endpoint.Scope.global);
+ .endpointsIn(controller.system())
+ .scope(Endpoint.Scope.global);
+
globalEndpoints.main().ifPresent(mainEndpoint -> {
registerCname(mainEndpoint.dnsName(), rotation.name());
if (redirectLegacyDns) {
@@ -475,6 +505,54 @@ public class ApplicationController {
return application;
}
+ private List<AssignedRotation> createDefaultGlobalIdRotation(Application application, Rotation rotation) {
+ // This is guaranteed by .withRotationLegacy, but add this to make inspections accept the use of .get() below
+ assert application.deploymentSpec().globalServiceId().isPresent();
+
+ final Set<RegionName> regions = application.deploymentSpec().zones().stream()
+ .filter(zone -> zone.environment().isProduction())
+ .flatMap(zone -> zone.region().stream())
+ .collect(Collectors.toSet());
+
+ final var assignment = new AssignedRotation(
+ ClusterSpec.Id.from(application.deploymentSpec().globalServiceId().get()),
+ EndpointId.default_(),
+ rotation.id(),
+ regions
+ );
+
+ return List.of(assignment);
+ }
+
+ /** Makes sure the application has a global rotation, if eligible. */
+ private LockedApplication withRotation(LockedApplication application, ZoneId zone) {
+ if (zone.environment() == Environment.prod) {
+ try (RotationLock rotationLock = rotationRepository.lock()) {
+ final var rotations = rotationRepository.getOrAssignRotations(application.get(), rotationLock);
+ application = application.with(rotations);
+ store(application); // store assigned rotation even if deployment fails
+ registerAssignedRotationCnames(application.get());
+ }
+ }
+ return application;
+ }
+
+ private void registerAssignedRotationCnames(Application application) {
+ application.assignedRotations().forEach(assignedRotation -> {
+ final var endpoints = application
+ .endpointsIn(controller.system(), assignedRotation.endpointId())
+ .scope(Endpoint.Scope.global);
+
+ final var maybeRotation = rotationRepository.getRotation(assignedRotation.rotationId());
+
+ maybeRotation.ifPresent(rotation -> {
+ endpoints.main().ifPresent(mainEndpoint -> {
+ registerCname(mainEndpoint.dnsName(), rotation.name());
+ });
+ });
+ });
+ }
+
private LockedApplication withApplicationCertificate(LockedApplication application) {
ApplicationId applicationId = application.get().id();
@@ -631,8 +709,14 @@ public class ApplicationController {
applicationStore.removeAll(id);
applicationStore.removeAll(TesterId.of(id));
- EndpointList endpoints = application.get().endpointsIn(controller.system());
- endpoints.asList().stream().map(Endpoint::dnsName).forEach(name -> controller.nameServiceForwarder().removeRecords(Record.Type.CNAME, RecordName.from(name), Priority.normal));
+ application.get().assignedRotations().forEach(assignedRotation -> {
+ final var endpoints = application.get().endpointsIn(controller.system(), assignedRotation.endpointId());
+ endpoints.asList().stream()
+ .map(Endpoint::dnsName)
+ .forEach(name -> {
+ controller.nameServiceForwarder().removeRecords(Record.Type.CNAME, RecordName.from(name), Priority.normal);
+ });
+ });
log.info("Deleted " + application);
}));
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java
index e1ed278a79e..ec13066d069 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java
@@ -1,12 +1,16 @@
package com.yahoo.vespa.hosted.controller.application;
import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.RegionName;
import com.yahoo.vespa.hosted.controller.rotation.RotationId;
+import java.util.Collection;
import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
- * Contains the tuple of [clusterId, endpointId, rotationId], to keep track
+ * Contains the tuple of [clusterId, endpointId, rotationId, regions[]], to keep track
* of which services have assigned which rotations under which name.
*
* @author ogronnesby
@@ -15,16 +19,19 @@ public class AssignedRotation {
private final ClusterSpec.Id clusterId;
private final EndpointId endpointId;
private final RotationId rotationId;
+ private final Set<RegionName> regions;
- public AssignedRotation(ClusterSpec.Id clusterId, EndpointId endpointId, RotationId rotationId) {
+ public AssignedRotation(ClusterSpec.Id clusterId, EndpointId endpointId, RotationId rotationId, Set<RegionName> regions) {
this.clusterId = requireNonEmpty(clusterId, clusterId.value(), "clusterId");
this.endpointId = Objects.requireNonNull(endpointId);
this.rotationId = Objects.requireNonNull(rotationId);
+ this.regions = Set.copyOf(Objects.requireNonNull(regions));
}
public ClusterSpec.Id clusterId() { return clusterId; }
public EndpointId endpointId() { return endpointId; }
public RotationId rotationId() { return rotationId; }
+ public Set<RegionName> regions() { return regions; }
@Override
public String toString() {
@@ -32,6 +39,7 @@ public class AssignedRotation {
"clusterId=" + clusterId +
", endpointId='" + endpointId + '\'' +
", rotationId=" + rotationId +
+ ", regions=" + regions +
'}';
}
@@ -42,12 +50,13 @@ public class AssignedRotation {
AssignedRotation that = (AssignedRotation) o;
return clusterId.equals(that.clusterId) &&
endpointId.equals(that.endpointId) &&
- rotationId.equals(that.rotationId);
+ rotationId.equals(that.rotationId) &&
+ regions.equals(that.regions);
}
@Override
public int hashCode() {
- return Objects.hash(clusterId, endpointId, rotationId);
+ return Objects.hash(clusterId, endpointId, rotationId, regions);
}
private static <T> T requireNonEmpty(T object, String value, String field) {
@@ -58,4 +67,14 @@ public class AssignedRotation {
}
return object;
}
+
+ /** Convenience method intended for tests */
+ public static AssignedRotation fromStrings(String clusterId, String endpointId, String rotationId, Collection<String> regions) {
+ return new AssignedRotation(
+ new ClusterSpec.Id(clusterId),
+ new EndpointId(endpointId),
+ new RotationId(rotationId),
+ regions.stream().map(RegionName::from).collect(Collectors.toSet())
+ );
+ }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java
index 5026ca75a83..5dccd5c8120 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java
@@ -217,6 +217,7 @@ public class Endpoint {
private ZoneId zone;
private ClusterSpec.Id cluster;
private RotationName rotation;
+ private EndpointId endpointId;
private Port port;
private boolean legacy = false;
private boolean directRouting = false;
@@ -227,8 +228,8 @@ public class Endpoint {
/** Sets the cluster and zone target of this */
public EndpointBuilder target(ClusterSpec.Id cluster, ZoneId zone) {
- if (rotation != null) {
- throw new IllegalArgumentException("Cannot set both cluster and rotation target");
+ if (rotation != null || endpointId != null) {
+ throw new IllegalArgumentException("Cannot set multiple target types");
}
this.cluster = cluster;
this.zone = zone;
@@ -237,13 +238,22 @@ public class Endpoint {
/** Sets the rotation target of this */
public EndpointBuilder target(RotationName rotation) {
- if (cluster != null && zone != null) {
- throw new IllegalArgumentException("Cannot set both cluster and rotation target");
+ if ((cluster != null && zone != null) || endpointId != null) {
+ throw new IllegalArgumentException("Cannot set multiple target types");
}
this.rotation = rotation;
return this;
}
+ /** Sets the endpoint ID as defines in deployments.xml */
+ public EndpointBuilder named(EndpointId endpointId) {
+ if (rotation != null || cluster != null || zone != null) {
+ throw new IllegalArgumentException("Cannot set multiple target types");
+ }
+ this.endpointId = endpointId;
+ return this;
+ }
+
/** Sets the port of this */
public EndpointBuilder on(Port port) {
this.port = port;
@@ -269,6 +279,8 @@ public class Endpoint {
name = cluster.value();
} else if (rotation != null) {
name = rotation.value();
+ } else if (endpointId != null) {
+ name = endpointId.id();
} else {
throw new IllegalArgumentException("Must set either cluster or rotation target");
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java
index 0c04a1f099c..feedc1c8f9d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java
@@ -67,16 +67,14 @@ public class EndpointList {
}
/** Returns the default global endpoints in given system. Default endpoints are served by a pre-provisioned routing layer */
- public static EndpointList defaultGlobal(ApplicationId application, SystemName system) {
- // Rotation name is always default in the routing layer
- RotationName rotation = RotationName.from("default");
+ public static EndpointList create(ApplicationId application, EndpointId endpointId, SystemName system) {
switch (system) {
case cd:
case main:
return new EndpointList(List.of(
- Endpoint.of(application).target(rotation).on(Port.plain(4080)).legacy().in(system),
- Endpoint.of(application).target(rotation).on(Port.tls(4443)).legacy().in(system),
- Endpoint.of(application).target(rotation).on(Port.tls(4443)).in(system)
+ Endpoint.of(application).named(endpointId).on(Port.plain(4080)).legacy().in(system),
+ Endpoint.of(application).named(endpointId).on(Port.tls(4443)).legacy().in(system),
+ Endpoint.of(application).named(endpointId).on(Port.tls(4443)).in(system)
));
}
return EMPTY;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
index 6ecf60e7404..0c045eb7253 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
@@ -41,6 +41,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -49,6 +50,7 @@ import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.TreeMap;
+import java.util.stream.Collectors;
/**
* Serializes {@link Application} to/from slime.
@@ -551,23 +553,25 @@ public class ApplicationSerializer {
}
private List<AssignedRotation> assignedRotationsFromSlime(DeploymentSpec deploymentSpec, Inspector root) {
- final var assignedRotations = new LinkedHashSet<AssignedRotation>();
+ final var assignedRotations = new LinkedHashMap<EndpointId, AssignedRotation>();
// Add the legacy rotation field to the set - this needs to be first
// TODO: Remove when we retire the rotations field
final var legacyRotation = legacyRotationFromSlime(root.field(deprecatedRotationField));
if (legacyRotation.isPresent() && deploymentSpec.globalServiceId().isPresent()) {
final var clusterId = new ClusterSpec.Id(deploymentSpec.globalServiceId().get());
- assignedRotations.add(new AssignedRotation(clusterId, EndpointId.default_(), legacyRotation.get()));
+ final var regions = deploymentSpec.zones().stream().flatMap(zone -> zone.region().stream()).collect(Collectors.toSet());
+ assignedRotations.putIfAbsent(EndpointId.default_(), new AssignedRotation(clusterId, EndpointId.default_(), legacyRotation.get(), regions));
}
// Now add the same entries from "stupid" list of rotations
// TODO: Remove when we retire the rotations field
final var rotations = rotationListFromSlime(root.field(rotationsField));
for (var rotation : rotations) {
+ final var regions = deploymentSpec.zones().stream().flatMap(zone -> zone.region().stream()).collect(Collectors.toSet());
if (deploymentSpec.globalServiceId().isPresent()) {
final var clusterId = new ClusterSpec.Id(deploymentSpec.globalServiceId().get());
- assignedRotations.add(new AssignedRotation(clusterId, EndpointId.default_(), rotation));
+ assignedRotations.putIfAbsent(EndpointId.default_(), new AssignedRotation(clusterId, EndpointId.default_(), rotation, regions));
}
}
@@ -576,10 +580,14 @@ public class ApplicationSerializer {
final var clusterId = new ClusterSpec.Id(inspector.field(assignedRotationClusterField).asString());
final var endpointId = EndpointId.of(inspector.field(assignedRotationEndpointField).asString());
final var rotationId = new RotationId(inspector.field(assignedRotationRotationField).asString());
- assignedRotations.add(new AssignedRotation(clusterId, endpointId, rotationId));
+ final var regions = deploymentSpec.endpoints().stream()
+ .filter(endpoint -> endpoint.endpointId().equals(endpointId.id()))
+ .flatMap(endpoint -> endpoint.regions().stream())
+ .collect(Collectors.toSet());
+ assignedRotations.putIfAbsent(endpointId, new AssignedRotation(clusterId, endpointId, rotationId, regions));
});
- return List.copyOf(assignedRotations);
+ return List.copyOf(assignedRotations.values());
}
private List<RotationId> rotationListFromSlime(Inspector field) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java
index d2b16721503..f2bc50ec445 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java
@@ -1,18 +1,30 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.rotation;
+import com.yahoo.collections.Pair;
+import com.yahoo.config.application.api.Endpoint;
+import com.yahoo.config.model.api.ContainerEndpoint;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
+import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
+import com.yahoo.vespa.hosted.controller.application.EndpointId;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -50,6 +62,11 @@ public class RotationRepository {
return application.rotations().stream().map(allRotations::get).findFirst();
}
+ /** Get rotation for the given rotationId */
+ public Optional<Rotation> getRotation(RotationId rotationId) {
+ return Optional.of(allRotations.get(rotationId));
+ }
+
/**
* Returns a rotation for the given application
*
@@ -76,6 +93,116 @@ public class RotationRepository {
}
/**
+ * Returns rotation assignments for all endpoints in application.
+ *
+ * If rotations are already assigned, these will be returned.
+ * If rotations are not assigned, a new assignment will be created taking new rotations from the repository.
+ * This method supports both global-service-id as well as the new endpoints tag.
+ *
+ * @param application The application requesting rotations.
+ * @param lock Lock which by acquired by the caller
+ * @return List of rotation assignments - either new or existing.
+ */
+ public List<AssignedRotation> getOrAssignRotations(Application application, RotationLock lock) {
+ if (application.deploymentSpec().globalServiceId().isPresent() && ! application.deploymentSpec().endpoints().isEmpty()) {
+ throw new IllegalArgumentException("Cannot provision rotations with both global-service-id and 'endpoints'");
+ }
+
+ // Support the older case of setting global-service-id
+ if (application.deploymentSpec().globalServiceId().isPresent()) {
+ final var regions = application.deploymentSpec().zones().stream()
+ .filter(zone -> zone.environment().isProduction())
+ .flatMap(zone -> zone.region().stream())
+ .collect(Collectors.toSet());
+
+ final var rotation = getOrAssignRotation(application, lock);
+
+ return List.of(
+ new AssignedRotation(
+ new ClusterSpec.Id(application.deploymentSpec().globalServiceId().get()),
+ EndpointId.default_(),
+ rotation.id(),
+ regions
+ )
+ );
+ }
+
+ final Map<EndpointId, AssignedRotation> existingAssignments = existingEndpointAssignments(application);
+ final Map<EndpointId, AssignedRotation> updatedAssignments = assignRotationsToEndpoints(application, existingAssignments, lock);
+
+ existingAssignments.putAll(updatedAssignments);
+
+ return List.copyOf(existingAssignments.values());
+ }
+
+ private Map<EndpointId, AssignedRotation> assignRotationsToEndpoints(Application application, Map<EndpointId, AssignedRotation> existingAssignments, RotationLock lock) {
+ final var availableRotations = new ArrayList<>(availableRotations(lock).values());
+
+ final var neededRotations = application.deploymentSpec().endpoints().stream()
+ .filter(Predicate.not(endpoint -> existingAssignments.containsKey(EndpointId.of(endpoint.endpointId()))))
+ .collect(Collectors.toSet());
+
+ if (neededRotations.size() > availableRotations.size()) {
+ throw new IllegalStateException("Hosted Vespa ran out of rotations, unable to assign rotation: need " + neededRotations.size() + ", have " + availableRotations.size());
+ }
+
+ return neededRotations.stream()
+ .map(endpoint -> {
+ return new AssignedRotation(
+ new ClusterSpec.Id(endpoint.containerId()),
+ EndpointId.of(endpoint.endpointId()),
+ availableRotations.remove(0).id(),
+ endpoint.regions()
+ );
+ })
+ .collect(
+ Collectors.toMap(
+ AssignedRotation::endpointId,
+ Function.identity(),
+ (a, b) -> { throw new IllegalStateException("Duplicate entries:" + a + ", " + b); },
+ LinkedHashMap::new
+ )
+ );
+ }
+
+ private Map<EndpointId, AssignedRotation> existingEndpointAssignments(Application application) {
+ //
+ // Get the regions that has been configured for an endpoint. Empty set if the endpoint
+ // is no longer mentioned in the configuration file.
+ //
+ final Function<EndpointId, Set<RegionName>> configuredRegionsForEndpoint = endpointId -> {
+ return application.deploymentSpec().endpoints().stream()
+ .filter(endpoint -> endpointId.id().equals(endpoint.endpointId()))
+ .map(Endpoint::regions)
+ .findFirst()
+ .orElse(Set.of());
+ };
+
+ //
+ // Build a new AssignedRotation instance where we update set of regions from the configuration instead
+ // of using the one already mentioned in the assignment. This allows us to overwrite the set of regions
+ // when
+ final Function<AssignedRotation, AssignedRotation> assignedRotationWithConfiguredRegions = assignedRotation -> {
+ return new AssignedRotation(
+ assignedRotation.clusterId(),
+ assignedRotation.endpointId(),
+ assignedRotation.rotationId(),
+ configuredRegionsForEndpoint.apply(assignedRotation.endpointId())
+ );
+ };
+
+ return application.assignedRotations().stream()
+ .collect(
+ Collectors.toMap(
+ AssignedRotation::endpointId,
+ assignedRotationWithConfiguredRegions,
+ (a, b) -> { throw new IllegalStateException("Duplicate entries: " + a + ", " + b); },
+ LinkedHashMap::new
+ )
+ );
+ }
+
+ /**
* Returns all unassigned rotations
* @param lock Lock which must be acquired by the caller
*/
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
index de31f1f67f9..c26f1879f6a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
@@ -7,6 +7,7 @@ import com.yahoo.config.application.api.ValidationId;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
@@ -23,6 +24,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevisi
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingEndpoint;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
@@ -275,6 +277,8 @@ public class ControllerTest {
@Test
public void testDnsAliasRegistration() {
+ ((InMemoryFlagSource) tester.controller().flagSource()).withBooleanFlag(Flags.MULTIPLE_GLOBAL_ENDPOINTS.id(), true);
+
Application application = tester.createApplication("app1", "tenant1", 1, 1L);
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
@@ -290,12 +294,42 @@ public class ControllerTest {
for (Deployment deployment : deployments) {
assertEquals("Rotation names are passed to config server in " + deployment.zone(),
Set.of("rotation-id-01",
- "app1--tenant1.global.vespa.oath.cloud",
- "app1.tenant1.global.vespa.yahooapis.com",
- "app1--tenant1.global.vespa.yahooapis.com"),
+ "app1--tenant1.global.vespa.oath.cloud"),
tester.configServer().rotationNames().get(new DeploymentId(application.id(), deployment.zone())));
}
tester.flushDnsRequests();
+
+ assertEquals(1, tester.controllerTester().nameService().records().size());
+
+ var record = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud");
+ assertTrue(record.isPresent());
+ assertEquals("app1--tenant1.global.vespa.oath.cloud", record.get().name().asString());
+ assertEquals("rotation-fqdn-01.", record.get().data().asString());
+ }
+
+ @Test
+ public void testDnsAliasRegistrationLegacy() {
+ Application application = tester.createApplication("app1", "tenant1", 1, 1L);
+
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .globalServiceId("foo")
+ .region("us-west-1")
+ .region("us-central-1") // Two deployments should result in each DNS alias being registered once
+ .build();
+
+ tester.deployCompletely(application, applicationPackage);
+ Collection<Deployment> deployments = tester.application(application.id()).deployments().values();
+ assertFalse(deployments.isEmpty());
+ for (Deployment deployment : deployments) {
+ assertEquals("Rotation names are passed to config server in " + deployment.zone(),
+ Set.of("rotation-id-01",
+ "app1--tenant1.global.vespa.oath.cloud",
+ "app1.tenant1.global.vespa.yahooapis.com",
+ "app1--tenant1.global.vespa.yahooapis.com"),
+ tester.configServer().rotationNames().get(new DeploymentId(application.id(), deployment.zone())));
+ }
+ tester.flushDnsRequests();
assertEquals(3, tester.controllerTester().nameService().records().size());
Optional<Record> record = tester.controllerTester().findCname("app1--tenant1.global.vespa.yahooapis.com");
@@ -315,38 +349,134 @@ public class ControllerTest {
}
@Test
- public void testRedirectLegacyDnsNames() { // TODO: Remove together with Flags.REDIRECT_LEGACY_DNS_NAMES
+ public void testDnsAliasRegistrationWithEndpoints() {
+ ((InMemoryFlagSource) tester.controller().flagSource()).withBooleanFlag(Flags.MULTIPLE_GLOBAL_ENDPOINTS.id(), true);
+
Application application = tester.createApplication("app1", "tenant1", 1, 1L);
+
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.environment(Environment.prod)
- .globalServiceId("foo")
+ .endpoint("foobar", "qrs", "us-west-1", "us-central-1")
+ .endpoint("default", "qrs", "us-west-1", "us-central-1")
.region("us-west-1")
.region("us-central-1")
.build();
- ((InMemoryFlagSource) tester.controller().flagSource()).withBooleanFlag(Flags.REDIRECT_LEGACY_DNS_NAMES.id(), true);
+ tester.deployCompletely(application, applicationPackage);
+ Collection<Deployment> deployments = tester.application(application.id()).deployments().values();
+ assertFalse(deployments.isEmpty());
+ for (Deployment deployment : deployments) {
+ assertEquals("Rotation names are passed to config server in " + deployment.zone(),
+ Set.of(
+ "rotation-id-01",
+ "rotation-id-02",
+ "app1--tenant1.global.vespa.oath.cloud",
+ "foobar--app1--tenant1.global.vespa.oath.cloud"
+ ),
+ tester.configServer().rotationNames().get(new DeploymentId(application.id(), deployment.zone())));
+ }
+ tester.flushDnsRequests();
+
+ assertEquals(2, tester.controllerTester().nameService().records().size());
+
+ var record1 = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud");
+ assertTrue(record1.isPresent());
+ assertEquals("app1--tenant1.global.vespa.oath.cloud", record1.get().name().asString());
+ assertEquals("rotation-fqdn-02.", record1.get().data().asString());
+
+ var record2 = tester.controllerTester().findCname("foobar--app1--tenant1.global.vespa.oath.cloud");
+ assertTrue(record2.isPresent());
+ assertEquals("foobar--app1--tenant1.global.vespa.oath.cloud", record2.get().name().asString());
+ assertEquals("rotation-fqdn-01.", record2.get().data().asString());
+ }
+
+ @Test
+ public void testDnsAliasRegistrationWithChangingZones() {
+ ((InMemoryFlagSource) tester.controller().flagSource()).withBooleanFlag(Flags.MULTIPLE_GLOBAL_ENDPOINTS.id(), true);
+
+ Application application = tester.createApplication("app1", "tenant1", 1, 1L);
+
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .endpoint("default", "qrs", "us-west-1", "us-central-1")
+ .region("us-west-1")
+ .region("us-central-1")
+ .build();
tester.deployCompletely(application, applicationPackage);
- assertEquals(3, tester.controllerTester().nameService().records().size());
- Optional<Record> record = tester.controllerTester().findCname("app1--tenant1.global.vespa.yahooapis.com");
- assertTrue(record.isPresent());
- assertEquals("app1--tenant1.global.vespa.yahooapis.com", record.get().name().asString());
- assertEquals("app1--tenant1.global.vespa.oath.cloud.", record.get().data().asString());
+ assertEquals(
+ Set.of("rotation-id-01", "app1--tenant1.global.vespa.oath.cloud"),
+ tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-west-1")))
+ );
- record = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud");
- assertTrue(record.isPresent());
- assertEquals("app1--tenant1.global.vespa.oath.cloud", record.get().name().asString());
- assertEquals("rotation-fqdn-01.", record.get().data().asString());
+ assertEquals(
+ Set.of("rotation-id-01", "app1--tenant1.global.vespa.oath.cloud"),
+ tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-central-1")))
+ );
- record = tester.controllerTester().findCname("app1.tenant1.global.vespa.yahooapis.com");
- assertTrue(record.isPresent());
- assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name().asString());
- assertEquals("app1--tenant1.global.vespa.oath.cloud.", record.get().data().asString());
+
+ ApplicationPackage applicationPackage2 = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .endpoint("default", "qrs", "us-west-1")
+ .region("us-west-1")
+ .region("us-central-1")
+ .build();
+
+ tester.deployCompletely(application, applicationPackage2, BuildJob.defaultBuildNumber + 1);
+
+ assertEquals(
+ Set.of("rotation-id-01", "app1--tenant1.global.vespa.oath.cloud"),
+ tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-west-1")))
+ );
+
+ assertEquals(
+ Set.of(),
+ tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-central-1")))
+ );
+
+ assertEquals(Set.of(RegionName.from("us-west-1")), tester.application(application.id()).assignedRotations().get(0).regions());
}
@Test
+ public void testUnassignRotations() {
+ ((InMemoryFlagSource) tester.controller().flagSource()).withBooleanFlag(Flags.MULTIPLE_GLOBAL_ENDPOINTS.id(), true);
+
+ Application application = tester.createApplication("app1", "tenant1", 1, 1L);
+
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .endpoint("default", "qrs", "us-west-1", "us-central-1")
+ .region("us-west-1")
+ .region("us-central-1") // Two deployments should result in each DNS alias being registered once
+ .build();
+
+ tester.deployCompletely(application, applicationPackage);
+
+ ApplicationPackage applicationPackage2 = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .region("us-west-1")
+ .region("us-central-1") // Two deployments should result in each DNS alias being registered once
+ .build();
+
+ tester.deployCompletely(application, applicationPackage2, BuildJob.defaultBuildNumber + 1);
+
+
+ assertEquals(
+ List.of(AssignedRotation.fromStrings("qrs", "default", "rotation-id-01", Set.of())),
+ tester.application(application.id()).assignedRotations()
+ );
+
+ assertEquals(
+ Set.of(),
+ tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-west-1")))
+ );
+ }
+
+ @Test
public void testUpdatesExistingDnsAlias() {
+ ((InMemoryFlagSource) tester.controller().flagSource()).withBooleanFlag(Flags.MULTIPLE_GLOBAL_ENDPOINTS.id(), true);
+
// Application 1 is deployed and deleted
{
Application app1 = tester.createApplication("app1", "tenant1", 1, 1L);
@@ -358,16 +488,11 @@ public class ControllerTest {
.build();
tester.deployCompletely(app1, applicationPackage);
- assertEquals(3, tester.controllerTester().nameService().records().size());
-
- Optional<Record> record = tester.controllerTester().findCname("app1--tenant1.global.vespa.yahooapis.com");
- assertTrue(record.isPresent());
- assertEquals("app1--tenant1.global.vespa.yahooapis.com", record.get().name().asString());
- assertEquals("rotation-fqdn-01.", record.get().data().asString());
+ assertEquals(1, tester.controllerTester().nameService().records().size());
- record = tester.controllerTester().findCname("app1.tenant1.global.vespa.yahooapis.com");
+ Optional<Record> record = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud");
assertTrue(record.isPresent());
- assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name().asString());
+ assertEquals("app1--tenant1.global.vespa.oath.cloud", record.get().name().asString());
assertEquals("rotation-fqdn-01.", record.get().data().asString());
// Application is deleted and rotation is unassigned
@@ -408,22 +533,12 @@ public class ControllerTest {
.region("us-central-1")
.build();
tester.deployCompletely(app2, applicationPackage);
- assertEquals(3, tester.controllerTester().nameService().records().size());
-
- Optional<Record> record = tester.controllerTester().findCname("app2--tenant2.global.vespa.yahooapis.com");
- assertTrue(record.isPresent());
- assertEquals("app2--tenant2.global.vespa.yahooapis.com", record.get().name().asString());
- assertEquals("rotation-fqdn-01.", record.get().data().asString());
+ assertEquals(1, tester.controllerTester().nameService().records().size());
- record = tester.controllerTester().findCname("app2--tenant2.global.vespa.oath.cloud");
+ var record = tester.controllerTester().findCname("app2--tenant2.global.vespa.oath.cloud");
assertTrue(record.isPresent());
assertEquals("app2--tenant2.global.vespa.oath.cloud", record.get().name().asString());
assertEquals("rotation-fqdn-01.", record.get().data().asString());
-
- record = tester.controllerTester().findCname("app2.tenant2.global.vespa.yahooapis.com");
- assertTrue(record.isPresent());
- assertEquals("app2.tenant2.global.vespa.yahooapis.com", record.get().name().asString());
- assertEquals("rotation-fqdn-01.", record.get().data().asString());
}
// Application 1 is recreated, deployed and assigned a new rotation
@@ -441,19 +556,15 @@ public class ControllerTest {
assertEquals("rotation-id-02", app1.rotations().get(0).asString());
// DNS records are created for the newly assigned rotation
- assertEquals(6, tester.controllerTester().nameService().records().size());
+ assertEquals(2, tester.controllerTester().nameService().records().size());
- Optional<Record> record = tester.controllerTester().findCname("app1--tenant1.global.vespa.yahooapis.com");
- assertTrue(record.isPresent());
- assertEquals("rotation-fqdn-02.", record.get().data().asString());
-
- record = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud");
- assertTrue(record.isPresent());
- assertEquals("rotation-fqdn-02.", record.get().data().asString());
+ var record1 = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud");
+ assertTrue(record1.isPresent());
+ assertEquals("rotation-fqdn-02.", record1.get().data().asString());
- record = tester.controllerTester().findCname("app1.tenant1.global.vespa.yahooapis.com");
- assertTrue(record.isPresent());
- assertEquals("rotation-fqdn-02.", record.get().data().asString());
+ var record2 = tester.controllerTester().findCname("app2--tenant2.global.vespa.oath.cloud");
+ assertTrue(record2.isPresent());
+ assertEquals("rotation-fqdn-01.", record2.get().data().asString());
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java
index 16b875c1892..f5047a82e2f 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java
@@ -66,6 +66,50 @@ public class EndpointTest {
}
@Test
+ public void test_global_endpoints_with_endpoint_id() {
+ final var endpointId = EndpointId.default_();
+
+ Map<String, Endpoint> tests = Map.of(
+ // Legacy endpoint
+ "http://a1.t1.global.vespa.yahooapis.com:4080/",
+ Endpoint.of(app1).named(endpointId).on(Port.plain(4080)).legacy().in(SystemName.main),
+
+ // Legacy endpoint with TLS
+ "https://a1--t1.global.vespa.yahooapis.com:4443/",
+ Endpoint.of(app1).named(endpointId).on(Port.tls(4443)).legacy().in(SystemName.main),
+
+ // Main endpoint
+ "https://a1--t1.global.vespa.oath.cloud:4443/",
+ Endpoint.of(app1).named(endpointId).on(Port.tls(4443)).in(SystemName.main),
+
+ // Main endpoint in CD
+ "https://cd--a1--t1.global.vespa.oath.cloud:4443/",
+ Endpoint.of(app1).named(endpointId).on(Port.tls(4443)).in(SystemName.cd),
+
+ // Main endpoint with direct routing and default TLS port
+ "https://a1.t1.global.vespa.oath.cloud/",
+ Endpoint.of(app1).named(endpointId).on(Port.tls()).directRouting().in(SystemName.main),
+
+ // Main endpoint with custom rotation name
+ "https://r1.a1.t1.global.vespa.oath.cloud/",
+ Endpoint.of(app1).named(EndpointId.of("r1")).on(Port.tls()).directRouting().in(SystemName.main),
+
+ // Main endpoint for custom instance in default rotation
+ "https://a2.t2.global.vespa.oath.cloud/",
+ Endpoint.of(app2).named(endpointId).on(Port.tls()).directRouting().in(SystemName.main),
+
+ // Main endpoint for custom instance with custom rotation name
+ "https://r2.a2.t2.global.vespa.oath.cloud/",
+ Endpoint.of(app2).named(EndpointId.of("r2")).on(Port.tls()).directRouting().in(SystemName.main),
+
+ // Main endpoint in public system
+ "https://a1.t1.global.public.vespa.oath.cloud/",
+ Endpoint.of(app1).named(endpointId).on(Port.tls()).directRouting().in(SystemName.Public)
+ );
+ tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
+ }
+
+ @Test
public void test_zone_endpoints() {
ClusterSpec.Id cluster = ClusterSpec.Id.from("default"); // Always default for non-direct routing
ZoneId prodZone = ZoneId.from("prod", "us-north-1");
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 83b95ccc8b0..6635547e9be 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
@@ -17,6 +17,9 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import java.util.OptionalInt;
import java.util.StringJoiner;
import java.util.zip.ZipEntry;
@@ -35,6 +38,7 @@ public class ApplicationPackageBuilder {
private final StringJoiner notifications = new StringJoiner("/>\n <email ",
"<notifications>\n <email ",
"/>\n</notifications>\n").setEmptyValue("");
+ private final StringBuilder endpointsBody = new StringBuilder();
private OptionalInt majorVersion = OptionalInt.empty();
private String upgradePolicy = null;
@@ -63,6 +67,18 @@ public class ApplicationPackageBuilder {
return this;
}
+ public ApplicationPackageBuilder endpoint(String endpointId, String containerId, String... regions) {
+ endpointsBody.append(" <endpoint");
+ endpointsBody.append(" id='").append(endpointId).append("'");
+ endpointsBody.append(" container-id='").append(containerId).append("'");
+ endpointsBody.append(">\n");
+ for (var region : regions) {
+ endpointsBody.append(" <region>").append(region).append("</region>\n");
+ }
+ endpointsBody.append(" </endpoint>\n");
+ return this;
+ }
+
public ApplicationPackageBuilder region(RegionName regionName) {
return region(regionName.value());
}
@@ -157,7 +173,11 @@ public class ApplicationPackageBuilder {
xml.append(environmentBody);
xml.append(" </");
xml.append(environment.value());
- xml.append(">\n</deployment>");
+ xml.append(">\n");
+ xml.append(" <endpoints>\n");
+ xml.append(endpointsBody);
+ xml.append(" </endpoints>\n");
+ xml.append("</deployment>");
return xml.toString().getBytes(StandardCharsets.UTF_8);
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
index cdbc45c4d8f..a89c5988396 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
@@ -45,6 +45,7 @@ import java.util.Set;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
+import java.util.stream.Stream;
/**
* @author mortent
@@ -226,7 +227,7 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
@Override
public PreparedApplication deploy(DeploymentId deployment, DeployOptions deployOptions, Set<String> rotationNames,
- List<ContainerEndpoint> containerEndpoints, ApplicationCertificate applicationCertificate, byte[] content) {
+ Set<ContainerEndpoint> containerEndpoints, ApplicationCertificate applicationCertificate, byte[] content) {
lastPrepareVersion = deployOptions.vespaVersion.map(Version::fromString).orElse(null);
if (prepareException != null) {
RuntimeException prepareException = this.prepareException;
@@ -238,7 +239,13 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
if (nodeRepository().list(deployment.zoneId(), deployment.applicationId()).isEmpty())
provision(deployment.zoneId(), deployment.applicationId());
- this.rotationNames.put(deployment, Set.copyOf(rotationNames));
+ this.rotationNames.put(
+ deployment,
+ Stream.concat(
+ containerEndpoints.stream().flatMap(e -> e.names().stream()),
+ rotationNames.stream()
+ ).collect(Collectors.toSet())
+ );
return () -> {
Application application = applications.get(deployment.applicationId());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
index 347fe6064df..7b39b0d53a4 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
@@ -7,6 +7,7 @@ import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostName;
+import com.yahoo.config.provision.RegionName;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
@@ -46,6 +47,7 @@ import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
+import java.util.Set;
import java.util.TreeMap;
import static com.yahoo.config.provision.SystemName.main;
@@ -119,7 +121,7 @@ public class ApplicationSerializerTest {
OptionalInt.of(7),
new MetricsService.ApplicationMetrics(0.5, 0.9),
Optional.of("-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"),
- List.of(new AssignedRotation(new ClusterSpec.Id("foo"), EndpointId.default_(), new RotationId("my-rotation"))),
+ List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of())),
rotationStatus,
Optional.of(new ApplicationCertificate("vespa.certificate")));
@@ -258,6 +260,11 @@ public class ApplicationSerializerTest {
final var applicationJson = Files.readAllBytes(testData.resolve("complete-application.json"));
final var slime = SlimeUtils.jsonToSlime(applicationJson);
+ final var regions = Set.of(
+ RegionName.from("us-east-3"),
+ RegionName.from("us-west-1")
+ );
+
// Add the necessary fields to the Slime representation of the application
final var cursor = slime.get();
cursor.setString("rotation", "single-rotation");
@@ -275,11 +282,11 @@ public class ApplicationSerializerTest {
// Parse and test the output from parsing contains both legacy rotation and multiple rotations
final var application = applicationSerializer.fromSlime(slime);
+ // Since only one AssignedEndpoint can be "default", we make sure that we are ignoring the
+ // multiple-rotation entries as the globalServiceId will override them
assertEquals(
List.of(
new RotationId("single-rotation"),
- new RotationId("multiple-rotation-1"),
- new RotationId("multiple-rotation-2"),
new RotationId("assigned-rotation")
),
application.rotations()
@@ -289,12 +296,13 @@ public class ApplicationSerializerTest {
Optional.of(new RotationId("single-rotation")), application.legacyRotation()
);
+ // The same goes here for AssignedRotations with "default" EndpointId as in the .rotations() test above.
+ // Note that we are only using Set.of() on "assigned-rotation" because in this test we do not have access
+ // to a deployment.xml that describes the zones a rotation should map to.
assertEquals(
List.of(
- new AssignedRotation(new ClusterSpec.Id("foo"), EndpointId.of("default"), new RotationId("single-rotation")),
- new AssignedRotation(new ClusterSpec.Id("foo"), EndpointId.of("default"), new RotationId("multiple-rotation-1")),
- new AssignedRotation(new ClusterSpec.Id("foo"), EndpointId.of("default"), new RotationId("multiple-rotation-2")),
- new AssignedRotation(new ClusterSpec.Id("foobar"), EndpointId.of("nice-endpoint"), new RotationId("assigned-rotation"))
+ new AssignedRotation(new ClusterSpec.Id("foo"), EndpointId.of("default"), new RotationId("single-rotation"), regions),
+ new AssignedRotation(new ClusterSpec.Id("foobar"), EndpointId.of("nice-endpoint"), new RotationId("assigned-rotation"), Set.of())
),
application.assignedRotations()
);
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
index b969e328419..5e9c0e2543c 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
@@ -151,6 +151,12 @@ public class Flags {
"Takes effect on deployment through controller",
APPLICATION_ID);
+ public static final UnboundBooleanFlag MULTIPLE_GLOBAL_ENDPOINTS = defineFeatureFlag(
+ "multiple-global-endpoints", false,
+ "Allow applications to use new endpoints syntax in deployment.xml",
+ "Takes effect on deployment through controller",
+ APPLICATION_ID);
+
/** WARNING: public for testing: All flags should be defined in {@link Flags}. */
public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, String description,
String modificationEffect, FetchVector.Dimension... dimensions) {