summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2017-11-30 12:42:47 +0100
committerGitHub <noreply@github.com>2017-11-30 12:42:47 +0100
commitb46ab5dc9c298e855e47eea2a9a4f8e4f9012dcf (patch)
treed1399bbd04dd14cf4b71a9235db2fe0906d1fb46
parent6dfa53a9870e1fd61fff9b68be65c43019236a00 (diff)
parent5c0c4397759b944b5dcd2b8c645ba898f4567746 (diff)
Merge pull request #4309 from vespa-engine/mpolden/move-rotation-to-application
Move rotation to application
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RotationId.java1
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/rotation/Rotation.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java24
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java118
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java21
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java73
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/ApplicationAlias.java57
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationRotation.java51
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java14
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java40
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/Rotation.java49
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationId.java42
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java106
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java146
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/MemoryRotationRepository.java54
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/RotationRepository.java48
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java36
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java106
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java22
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationTest.java171
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java212
31 files changed, 747 insertions, 708 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RotationId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RotationId.java
index aab18595d20..2eeb0f60748 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RotationId.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RotationId.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.api.identifiers;
/**
* @author smorgrav
*/
+// TODO: Used in serialization (ConfigServerClient). Remove when no longer used by ControllerDb and ConfigServerClient
public class RotationId extends Identifier {
public RotationId(String id) {
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/rotation/Rotation.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/rotation/Rotation.java
index ed3e69bcac7..a1f78302e4b 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/rotation/Rotation.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/rotation/Rotation.java
@@ -10,6 +10,8 @@ import java.util.Objects;
*
* @author Oyvind Gronnesby
*/
+// TODO: Used in serialization (ConfigServerClient). This should be removed and config server client should use a
+// Set<String> instead, like it does for CNAMEs.
public class Rotation {
/** The ID of the allocated rotation. This value is generated by global routing system. */
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 e3400d76bce..9a7ce69f62a 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
@@ -10,11 +10,13 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRotation;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Change.VersionChange;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import java.time.Instant;
import java.util.Collections;
@@ -44,32 +46,38 @@ public class Application {
private final boolean outstandingChange;
private final Optional<IssueId> ownershipIssueId;
private final ApplicationMetrics metrics;
+ private final Optional<RotationId> rotation;
/** Creates an empty application */
public Application(ApplicationId id) {
- this(id, DeploymentSpec.empty, ValidationOverrides.empty, ImmutableMap.of(),
+ this(id, DeploymentSpec.empty, ValidationOverrides.empty, Collections.emptyMap(),
new DeploymentJobs(Optional.empty(), Collections.emptyList(), Optional.empty()),
- Optional.empty(), false, Optional.empty(), new ApplicationMetrics(0, 0));
+ Optional.empty(), false, Optional.empty(), new ApplicationMetrics(0, 0),
+ Optional.empty());
}
/** Used from persistence layer: Do not use */
public Application(ApplicationId id, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
List<Deployment> deployments, DeploymentJobs deploymentJobs, Optional<Change> deploying,
- boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics) {
+ boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics,
+ Optional<RotationId> rotation) {
this(id, deploymentSpec, validationOverrides,
deployments.stream().collect(Collectors.toMap(Deployment::zone, d -> d)),
- deploymentJobs, deploying, outstandingChange, ownershipIssueId, metrics);
+ deploymentJobs, deploying, outstandingChange, ownershipIssueId, metrics, rotation);
}
Application(ApplicationId id, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
Map<Zone, Deployment> deployments, DeploymentJobs deploymentJobs, Optional<Change> deploying,
- boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics) {
+ boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics,
+ Optional<RotationId> rotation) {
Objects.requireNonNull(id, "id cannot be null");
Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null");
Objects.requireNonNull(validationOverrides, "validationOverrides cannot be null");
Objects.requireNonNull(deployments, "deployments cannot be null");
Objects.requireNonNull(deploymentJobs, "deploymentJobs cannot be null");
Objects.requireNonNull(deploying, "deploying cannot be null");
+ Objects.requireNonNull(metrics, "metrics cannot be null");
+ Objects.requireNonNull(rotation, "rotation cannot be null");
this.id = id;
this.deploymentSpec = deploymentSpec;
this.validationOverrides = validationOverrides;
@@ -79,6 +87,7 @@ public class Application {
this.outstandingChange = outstandingChange;
this.ownershipIssueId = ownershipIssueId;
this.metrics = metrics;
+ this.rotation = rotation;
}
public ApplicationId id() { return id; }
@@ -168,6 +177,11 @@ public class Application {
return Optional.ofNullable(deployments().get(zone)).map(Deployment::revision);
}
+ /** Returns the global rotation of this, if present */
+ public Optional<ApplicationRotation> rotation() {
+ return rotation.map(rotation -> new ApplicationRotation(id, rotation));
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
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 841b9b4dd9f..c7755a87510 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
@@ -1,7 +1,6 @@
// 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;
-import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationId;
@@ -11,7 +10,6 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.ActivateResult;
-import com.yahoo.vespa.hosted.controller.api.ApplicationAlias;
import com.yahoo.vespa.hosted.controller.api.InstanceEndpoints;
import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
@@ -32,7 +30,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordId;
import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingEndpoint;
import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerator;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRotation;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
@@ -49,7 +47,10 @@ import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.maintenance.DeploymentExpirer;
import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.rotation.RotationRepository;
+import com.yahoo.vespa.hosted.controller.rotation.Rotation;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
+import com.yahoo.vespa.hosted.controller.rotation.RotationRepository;
+import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
import java.io.IOException;
import java.net.URI;
@@ -82,6 +83,7 @@ public class ApplicationController {
/** For permanent storage */
private final ControllerDb db;
+
/** For working memory storage and sharing between controllers */
private final CuratorDb curator;
@@ -93,26 +95,41 @@ public class ApplicationController {
private final Clock clock;
private final DeploymentTrigger deploymentTrigger;
+
+ private final Object monitor = new Object();
ApplicationController(Controller controller, ControllerDb db, CuratorDb curator,
- RotationRepository rotationRepository,
- AthenzClientFactory zmsClientFactory,
+ AthenzClientFactory zmsClientFactory, RotationsConfig rotationsConfig,
NameService nameService, ConfigServerClient configserverClient,
RoutingGenerator routingGenerator, Clock clock) {
this.controller = controller;
this.db = db;
this.curator = curator;
- this.rotationRepository = rotationRepository;
this.zmsClientFactory = zmsClientFactory;
this.nameService = nameService;
this.configserverClient = configserverClient;
this.routingGenerator = routingGenerator;
this.clock = clock;
+ this.rotationRepository = new RotationRepository(rotationsConfig, this);
this.deploymentTrigger = new DeploymentTrigger(controller, curator, clock);
- for (Application application : db.listApplications())
- lockedIfPresent(application.id(), this::store);
+ for (Application application : db.listApplications()) {
+ lockedIfPresent(application.id(), (app) -> {
+ // TODO: Remove after December 2017. Migrates rotations into application
+ if (!app.rotation().isPresent()) {
+ Set<com.yahoo.vespa.hosted.controller.api.identifiers.RotationId> rotations = db.getRotations(application.id());
+ if (rotations.size() > 1) {
+ log.warning("Application " + application.id() + " has more than 1 rotation: "
+ + rotations.size());
+ }
+ if (!rotations.isEmpty()) {
+ app = app.with(new RotationId(rotations.iterator().next().id()));
+ }
+ }
+ store(app);
+ });
+ }
}
/** Returns the application with the given id, or null if it is not present */
@@ -328,15 +345,37 @@ public class ApplicationController {
throw new IllegalArgumentException("Rejecting deployment of " + application + " to " + zone +
" as the requested version " + version + " is older than" +
" the current version " + existingDeployment.version());
- }
+ }
+
+ // Synchronize rotation assignment. Rotation can only be considered assigned once application has been
+ // persisted.
+ Optional<Rotation> rotation;
+ synchronized (monitor) {
+ rotation = getRotation(application, zone);
+ if (rotation.isPresent()) {
+ application = application.with(rotation.get().id());
+ store(application); // store assigned rotation even if deployment fails
+ registerRotationInDns(application.rotation().get(), rotation.get());
+ }
+ }
+
+ // TODO: Improve config server client interface and simplify
+ Set<String> cnames = application.rotation()
+ .map(ApplicationRotation::dnsName)
+ .map(Collections::singleton)
+ .orElseGet(Collections::emptySet);
+
+ Set<com.yahoo.vespa.hosted.controller.api.rotation.Rotation> rotations = rotation
+ .map(r -> new com.yahoo.vespa.hosted.controller.api.rotation.Rotation(
+ new com.yahoo.vespa.hosted.controller.api.identifiers.RotationId(
+ r.id().asString()), r.name()))
+ .map(Collections::singleton)
+ .orElseGet(Collections::emptySet);
// Carry out deployment
- DeploymentId deploymentId = new DeploymentId(applicationId, zone);
- ApplicationRotation rotationInDns = registerRotationInDns(deploymentId, getOrAssignRotation(deploymentId,
- applicationPackage));
- options = withVersion(version, options);
+ options = withVersion(version, options);
ConfigServerClient.PreparedApplication preparedApplication =
- configserverClient.prepare(deploymentId, options, rotationInDns.cnames(), rotationInDns.rotations(),
+ configserverClient.prepare(new DeploymentId(applicationId, zone), options, cnames, rotations,
applicationPackage.zippedContent());
preparedApplication.activate();
application = application.withNewDeployment(zone, revision, version, clock.instant());
@@ -430,35 +469,28 @@ public class ApplicationController {
gitRevision.commit.id()));
}
- private ApplicationRotation registerRotationInDns(DeploymentId deploymentId, ApplicationRotation applicationRotation) {
- ApplicationAlias alias = new ApplicationAlias(deploymentId.applicationId());
- if (applicationRotation.rotations().isEmpty()) return applicationRotation;
-
- Rotation rotation = applicationRotation.rotations().iterator().next(); // at this time there should be only one rotation assigned
- String endpointName = alias.toString();
+ /** Register a DNS name for rotation */
+ private void registerRotationInDns(ApplicationRotation applicationRotation, Rotation rotation) {
+ String dnsName = applicationRotation.dnsName();
try {
- Optional<Record> record = nameService.findRecord(Record.Type.CNAME, endpointName);
+ Optional<Record> record = nameService.findRecord(Record.Type.CNAME, dnsName);
if (!record.isPresent()) {
- RecordId recordId = nameService.createCname(endpointName, rotation.rotationName);
+ RecordId recordId = nameService.createCname(dnsName, rotation.name());
log.info("Registered mapping with record ID " + recordId.id() + ": " +
- endpointName + " -> " + rotation.rotationName);
+ dnsName + " -> " + rotation.name());
}
- }
- catch (RuntimeException e) {
+ } catch (RuntimeException e) {
log.log(Level.WARNING, "Failed to register CNAME", e);
}
- return new ApplicationRotation(Collections.singleton(endpointName), Collections.singleton(rotation));
}
- private ApplicationRotation getOrAssignRotation(DeploymentId deploymentId, ApplicationPackage applicationPackage) {
- if (deploymentId.zone().environment().equals(Environment.prod)) {
- return new ApplicationRotation(Collections.emptySet(),
- rotationRepository.getOrAssignRotation(deploymentId.applicationId(),
- applicationPackage.deploymentSpec()));
- } else {
- return new ApplicationRotation(Collections.emptySet(),
- Collections.emptySet());
+ /** Get an available rotation, if deploying to a production zone and a service ID is specified */
+ private Optional<Rotation> getRotation(Application application, Zone zone) {
+ if (zone.environment() != Environment.prod ||
+ !application.deploymentSpec().globalServiceId().isPresent()) {
+ return Optional.empty();
}
+ return Optional.of(rotationRepository.getRotation(application));
}
/** Returns the endpoints of the deployment, or empty if obtaining them failed */
@@ -640,20 +672,8 @@ public class ApplicationController {
});
}
-
- private static final class ApplicationRotation {
-
- private final ImmutableSet<String> cnames;
- private final ImmutableSet<Rotation> rotations;
-
- public ApplicationRotation(Set<String> cnames, Set<Rotation> rotations) {
- this.cnames = ImmutableSet.copyOf(cnames);
- this.rotations = ImmutableSet.copyOf(rotations);
- }
-
- public Set<String> cnames() { return cnames; }
- public Set<Rotation> rotations() { return rotations; }
-
+ public RotationRepository rotationRepository() {
+ return rotationRepository;
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
index b854ad3f771..c9f58b7222c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
@@ -6,7 +6,6 @@ import com.google.inject.Inject;
import com.yahoo.component.AbstractComponent;
import com.yahoo.component.Version;
import com.yahoo.component.Vtag;
-import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
@@ -30,7 +29,7 @@ import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.rotation.RotationRepository;
+import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
import com.yahoo.vespa.serviceview.bindings.ApplicationView;
import java.net.URI;
@@ -39,7 +38,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import java.util.logging.Logger;
/**
@@ -64,7 +62,6 @@ public class Controller extends AbstractComponent {
private final ApplicationController applicationController;
private final TenantController tenantController;
private final Clock clock;
- private final RotationRepository rotationRepository;
private final GitHub gitHub;
private final EntityService entityService;
private final GlobalRoutingService globalRoutingService;
@@ -82,19 +79,19 @@ public class Controller extends AbstractComponent {
* @param curator the curator instance storing working state shared between controller instances
*/
@Inject
- public Controller(ControllerDb db, CuratorDb curator, RotationRepository rotationRepository,
+ public Controller(ControllerDb db, CuratorDb curator, RotationsConfig rotationsConfig,
GitHub gitHub, EntityService entityService, Organization organization,
GlobalRoutingService globalRoutingService,
ZoneRegistry zoneRegistry, ConfigServerClient configServerClient,
MetricsService metricsService, NameService nameService,
RoutingGenerator routingGenerator, Chef chefClient, AthenzClientFactory athenzClientFactory) {
- this(db, curator, rotationRepository,
+ this(db, curator, rotationsConfig,
gitHub, entityService, organization, globalRoutingService, zoneRegistry,
configServerClient, metricsService, nameService, routingGenerator, chefClient,
Clock.systemUTC(), athenzClientFactory);
}
- public Controller(ControllerDb db, CuratorDb curator, RotationRepository rotationRepository,
+ public Controller(ControllerDb db, CuratorDb curator, RotationsConfig rotationsConfig,
GitHub gitHub, EntityService entityService, Organization organization,
GlobalRoutingService globalRoutingService,
ZoneRegistry zoneRegistry, ConfigServerClient configServerClient,
@@ -103,7 +100,7 @@ public class Controller extends AbstractComponent {
AthenzClientFactory athenzClientFactory) {
Objects.requireNonNull(db, "Controller db cannot be null");
Objects.requireNonNull(curator, "Curator cannot be null");
- Objects.requireNonNull(rotationRepository, "Rotation repository cannot be null");
+ Objects.requireNonNull(rotationsConfig, "RotationsConfig cannot be null");
Objects.requireNonNull(gitHub, "GitHubClient cannot be null");
Objects.requireNonNull(entityService, "EntityService cannot be null");
Objects.requireNonNull(organization, "Organization cannot be null");
@@ -117,7 +114,6 @@ public class Controller extends AbstractComponent {
Objects.requireNonNull(clock, "Clock cannot be null");
Objects.requireNonNull(athenzClientFactory, "Athens cannot be null");
- this.rotationRepository = rotationRepository;
this.curator = curator;
this.gitHub = gitHub;
this.entityService = entityService;
@@ -130,7 +126,8 @@ public class Controller extends AbstractComponent {
this.clock = clock;
this.athenzClientFactory = athenzClientFactory;
- applicationController = new ApplicationController(this, db, curator, rotationRepository, athenzClientFactory,
+ applicationController = new ApplicationController(this, db, curator, athenzClientFactory,
+ rotationsConfig,
nameService, configServerClient, routingGenerator, clock);
tenantController = new TenantController(this, db, curator, entityService, athenzClientFactory);
}
@@ -181,10 +178,6 @@ public class Controller extends AbstractComponent {
return kibanaHost.map(uri -> uri.resolve(kibanaPath)).orElse(null);
}
- public Set<URI> getRotationUris(ApplicationId id) {
- return rotationRepository.getRotationUris(id);
- }
-
public Map<String, RotationStatus> getHealthStatus(String hostname) {
return globalRoutingService.getHealthStatus(hostname);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
index e8c8f8a389c..d96739dd0a0 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
@@ -10,6 +10,7 @@ import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRotation;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
@@ -18,7 +19,7 @@ import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import java.time.Instant;
import java.util.LinkedHashMap;
@@ -34,12 +35,6 @@ import java.util.Optional;
*/
public class LockedApplication extends Application {
- private LockedApplication(Builder builder) {
- super(builder.applicationId, builder.deploymentSpec, builder.validationOverrides,
- builder.deployments, builder.deploymentJobs, builder.deploying,
- builder.hasOutstandingChange, builder.ownershipIssueId, builder.metrics);
- }
-
/**
* Used to create a locked application
*
@@ -50,6 +45,12 @@ public class LockedApplication extends Application {
this(new Builder(application));
}
+ private LockedApplication(Builder builder) {
+ super(builder.applicationId, builder.deploymentSpec, builder.validationOverrides,
+ builder.deployments, builder.deploymentJobs, builder.deploying,
+ builder.hasOutstandingChange, builder.ownershipIssueId, builder.metrics, builder.rotation);
+ }
+
public LockedApplication withProjectId(long projectId) {
return new LockedApplication(new Builder(this).with(deploymentJobs().withProjectId(projectId)));
}
@@ -130,6 +131,10 @@ public class LockedApplication extends Application {
return new LockedApplication(new Builder(this).with(metrics));
}
+ public LockedApplication with(RotationId rotation) {
+ return new LockedApplication(new Builder(this).with(rotation));
+ }
+
public Version deployVersionFor(DeploymentJobs.JobType jobType, Controller controller) {
return jobType == JobType.component
? controller.systemVersion()
@@ -161,6 +166,7 @@ public class LockedApplication extends Application {
private boolean hasOutstandingChange;
private Optional<IssueId> ownershipIssueId;
private ApplicationMetrics metrics;
+ private Optional<RotationId> rotation;
private Builder(Application application) {
this.applicationId = application.id();
@@ -172,16 +178,53 @@ public class LockedApplication extends Application {
this.hasOutstandingChange = application.hasOutstandingChange();
this.ownershipIssueId = application.ownershipIssueId();
this.metrics = application.metrics();
+ this.rotation = application.rotation().map(ApplicationRotation::id);
+ }
+
+ private Builder with(DeploymentSpec deploymentSpec) {
+ this.deploymentSpec = deploymentSpec;
+ return this;
}
- private Builder with(DeploymentSpec deploymentSpec) { this.deploymentSpec = deploymentSpec; return this; }
- private Builder with(ValidationOverrides validationOverrides) { this.validationOverrides = validationOverrides; return this; }
- private Builder with(Map<Zone, Deployment> deployments) { this.deployments = deployments; return this; }
- private Builder with(DeploymentJobs deploymentJobs) { this.deploymentJobs = deploymentJobs; return this; }
- private Builder withDeploying(Optional<Change> deploying) { this.deploying = deploying; return this; }
- private Builder with(boolean hasOutstandingChange) { this.hasOutstandingChange = hasOutstandingChange; return this; }
- private Builder withOwnershipIssueId(Optional<IssueId> ownershipIssueId) { this.ownershipIssueId = ownershipIssueId; return this; }
- private Builder with(ApplicationMetrics metrics) { this.metrics = metrics; return this; }
+ private Builder with(ValidationOverrides validationOverrides) {
+ this.validationOverrides = validationOverrides;
+ return this;
+ }
+
+ private Builder with(Map<Zone, Deployment> deployments) {
+ this.deployments = deployments;
+ return this;
+ }
+
+ private Builder with(DeploymentJobs deploymentJobs) {
+ this.deploymentJobs = deploymentJobs;
+ return this;
+ }
+
+ private Builder withDeploying(Optional<Change> deploying) {
+ this.deploying = deploying;
+ return this;
+ }
+
+ private Builder with(boolean hasOutstandingChange) {
+ this.hasOutstandingChange = hasOutstandingChange;
+ return this;
+ }
+
+ private Builder withOwnershipIssueId(Optional<IssueId> ownershipIssueId) {
+ this.ownershipIssueId = ownershipIssueId;
+ return this;
+ }
+
+ private Builder with(ApplicationMetrics metrics) {
+ this.metrics = metrics;
+ return this;
+ }
+
+ private Builder with(RotationId rotation) {
+ this.rotation = Optional.of(rotation);
+ return this;
+ }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/ApplicationAlias.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/ApplicationAlias.java
deleted file mode 100644
index a9e144a3227..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/ApplicationAlias.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// 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.api;
-
-import com.yahoo.config.provision.ApplicationId;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-
-/**
- * A DNS alias for an application endpoint.
- *
- * @author smorgrav
- */
-public class ApplicationAlias {
-
- private static final String dnsSuffix = "global.vespa.yahooapis.com";
-
- private final ApplicationId applicationId;
-
- public ApplicationAlias(ApplicationId applicationId) {
- this.applicationId = applicationId;
- }
-
- @Override
- public String toString() {
- return String.format("%s.%s.%s",
- toDns(applicationId.application().value()),
- toDns(applicationId.tenant().value()),
- dnsSuffix);
- }
-
- private String toDns(String id) {
- return id.replace('_', '-');
- }
-
- public URI toHttpUri() {
- try {
- return new URI("http://" + this + ":4080/");
- } catch(URISyntaxException use) {
- throw new RuntimeException("Illegal URI syntax");
- }
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- ApplicationAlias that = (ApplicationAlias) o;
-
- return applicationId.equals(that.applicationId);
- }
-
- @Override
- public int hashCode() { return applicationId.hashCode(); }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationRotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationRotation.java
new file mode 100644
index 00000000000..80be9e6676a
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationRotation.java
@@ -0,0 +1,51 @@
+// 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.application;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
+
+import java.net.URI;
+
+/**
+ * Represents an application's global rotation.
+ *
+ * @author mpolden
+ */
+public class ApplicationRotation {
+
+ private static final String dnsSuffix = "global.vespa.yahooapis.com";
+ private static final int port = 4080;
+
+ private final URI url;
+ private final RotationId id;
+
+ public ApplicationRotation(ApplicationId application, RotationId id) {
+ this.url = URI.create(String.format("http://%s.%s.%s:%d/",
+ sanitize(application.application().value()),
+ sanitize(application.tenant().value()),
+ dnsSuffix,
+ port));
+ this.id = id;
+ }
+
+ /** ID of the rotation */
+ public RotationId id() {
+ return id;
+ }
+
+ /** URL to this rotation */
+ public URI url() {
+ return url;
+ }
+
+ /** DNS name for this rotation */
+ public String dnsName() {
+ return url.getHost();
+ }
+
+ /** Sanitize by translating '_' to '-' as the former is not allowed in a DNS name */
+ private static String sanitize(String s) {
+ return s.replace('_', '-');
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java
index 7c06ef27ce9..aa4d07634db 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java
@@ -46,7 +46,6 @@ public class DeploymentOrder {
/** Returns a list of jobs to trigger after the given job */
// TODO: This does too much - should just tell us the order, as advertised
- // TODO: You're next!
public List<JobType> nextAfter(JobType job, LockedApplication application) {
if ( ! application.deploying().isPresent()) { // Change was cancelled
return Collections.emptyList();
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
index 01e53ce4f79..6bd50365feb 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
@@ -23,11 +23,14 @@ import java.util.Optional;
/**
* @author mortent
+ * @author mpolden
*/
public class MetricsReporter extends Maintainer {
public static final String convergeMetric = "seconds.since.last.chef.convergence";
public static final String deploymentFailMetric = "deployment.failurePercentage";
+ public static final String remainingRotations = "remaining_rotations";
+
private final Metric metric;
private final Chef chefClient;
private final Clock clock;
@@ -51,6 +54,12 @@ public class MetricsReporter extends Maintainer {
public void maintain() {
reportChefMetrics();
reportDeploymentMetrics();
+ reportRemainingRotations();
+ }
+
+ private void reportRemainingRotations() {
+ int availableRotations = controller().applications().rotationRepository().availableRotations().size();
+ metric.set(remainingRotations, availableRotations, metric.createContext(Collections.emptyMap()));
}
private void reportChefMetrics() {
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 23316a74aae..c71409aaba9 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
@@ -27,6 +27,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.SourceRevision;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import java.time.Instant;
import java.util.ArrayList;
@@ -55,6 +56,7 @@ public class ApplicationSerializer {
private final String ownershipIssueIdField = "ownershipIssueId";
private final String writeQualityField = "writeQuality";
private final String queryQualityField = "queryQuality";
+ private final String rotationField = "rotation";
// Deployment fields
private final String zoneField = "zone";
@@ -114,7 +116,7 @@ public class ApplicationSerializer {
private final String deploymentMetricsQueryLatencyField = "queryLatencyMillis";
private final String deploymentMetricsWriteLatencyField = "writeLatencyMillis";
-
+
// ------------------ Serialization
public Slime toSlime(Application application) {
@@ -130,6 +132,7 @@ public class ApplicationSerializer {
application.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value()));
root.setDouble(queryQualityField, application.metrics().queryServiceQuality());
root.setDouble(writeQualityField, application.metrics().writeServiceQuality());
+ application.rotation().ifPresent(rotation -> root.setString(rotationField, rotation.id().asString()));
return slime;
}
@@ -267,9 +270,10 @@ public class ApplicationSerializer {
Optional<IssueId> ownershipIssueId = optionalString(root.field(ownershipIssueIdField)).map(IssueId::from);
ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(),
root.field(writeQualityField).asDouble());
+ Optional<RotationId> rotation = rotationFromSlime(root.field(rotationField));
- return new Application(id, deploymentSpec, validationOverrides, deployments,
- deploymentJobs, deploying, outstandingChange, ownershipIssueId, metrics);
+ return new Application(id, deploymentSpec, validationOverrides, deployments, deploymentJobs, deploying,
+ outstandingChange, ownershipIssueId, metrics, rotation);
}
private List<Deployment> deploymentsFromSlime(Inspector array) {
@@ -403,6 +407,10 @@ public class ApplicationSerializer {
Instant.ofEpochMilli(object.field(atField).asLong())));
}
+ private Optional<RotationId> rotationFromSlime(Inspector field) {
+ return field.valid() ? optionalString(field).map(RotationId::new) : Optional.empty();
+ }
+
private Optional<Long> optionalLong(Inspector field) {
return field.valid() ? Optional.of(field.asLong()) : Optional.empty();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java
index 3fbfdd31808..34b3ae55c2c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java
@@ -19,55 +19,57 @@ import java.util.Set;
* @author Stian Kristoffersen
* @author bratseth
*/
-public abstract class ControllerDb {
+public interface ControllerDb {
// --------- Tenants
- public abstract void createTenant(Tenant tenant);
+ void createTenant(Tenant tenant);
- public abstract void updateTenant(Tenant tenant) throws PersistenceException;
+ void updateTenant(Tenant tenant) throws PersistenceException;
- public abstract void deleteTenant(TenantId tenantId) throws PersistenceException;
+ void deleteTenant(TenantId tenantId) throws PersistenceException;
- public abstract Optional<Tenant> getTenant(TenantId tenantId) throws PersistenceException;
+ Optional<Tenant> getTenant(TenantId tenantId) throws PersistenceException;
- public abstract List<Tenant> listTenants();
+ List<Tenant> listTenants();
// --------- Applications
// ONLY call this from ApplicationController.store()
- public abstract void store(Application application);
+ void store(Application application);
- public abstract void deleteApplication(ApplicationId applicationId);
+ void deleteApplication(ApplicationId applicationId);
- public abstract Optional<Application> getApplication(ApplicationId applicationId);
+ Optional<Application> getApplication(ApplicationId applicationId);
/** Returns all applications */
- public abstract List<Application> listApplications();
+ List<Application> listApplications();
/** Returns all applications of a tenant */
- public abstract List<Application> listApplications(TenantId tenantId);
+ List<Application> listApplications(TenantId tenantId);
// --------- Rotations
-
- public abstract Set<RotationId> getRotations();
- public abstract Set<RotationId> getRotations(ApplicationId applicationId);
+ // TODO: Remove all rotation methods after December 2017
+ Set<RotationId> getRotations();
+
+ Set<RotationId> getRotations(ApplicationId applicationId);
- public abstract boolean assignRotation(RotationId rotationId, ApplicationId applicationId);
+ boolean assignRotation(RotationId rotationId, ApplicationId applicationId);
- public abstract Set<RotationId> deleteRotations(ApplicationId applicationId);
+ Set<RotationId> deleteRotations(ApplicationId applicationId);
+ // end TODO
/** Returns the given elements joined by dot "." */
- protected String path(Identifier... elements) {
+ default String path(Identifier... elements) {
return Joiner.on(".").join(elements);
}
- protected String path(String... elements) {
+ default String path(String... elements) {
return Joiner.on(".").join(elements);
}
- protected String path(ApplicationId applicationId) {
+ default String path(ApplicationId applicationId) {
return applicationId.tenant().value() + "." + applicationId.application().value() + "." + applicationId.instance().value();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
index ab240b9dea9..0dffd7ee520 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
@@ -22,7 +22,7 @@ import java.util.stream.Collectors;
*
* @author Stian Kristoffersen
*/
-public class MemoryControllerDb extends ControllerDb {
+public class MemoryControllerDb implements ControllerDb {
private final Map<TenantId, Tenant> tenants = new HashMap<>();
private final Map<String, Application> applications = new HashMap<>();
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 82c607b89fc..947027710c8 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
@@ -91,7 +91,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
-import java.util.Set;
import java.util.concurrent.Executor;
import java.util.logging.Level;
@@ -374,13 +373,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
// Compile version. The version that should be used when building an application
object.setString("compileVersion", application.oldestDeployedVersion().orElse(controller.systemVersion()).toFullString());
- // Rotations
+ // Rotation
Cursor globalRotationsArray = object.setArray("globalRotations");
- Set<URI> rotations = controller.getRotationUris(application.id());
- Map<String, RotationStatus> rotationHealthStatus =
- rotations.isEmpty() ? Collections.emptyMap() : controller.getHealthStatus(rotations.iterator().next().getHost());
- for (URI rotation : rotations)
- globalRotationsArray.addString(rotation.toString());
+ Map<String, RotationStatus> rotationHealthStatus = application.rotation()
+ .map(rotation -> controller.getHealthStatus(rotation.dnsName()))
+ .orElse(Collections.emptyMap());
+ application.rotation().ifPresent(rotation -> globalRotationsArray.addString(rotation.url().toString()));
// Deployments sorted according to deployment spec
List<Deployment> deployments = controller.applications().deploymentTrigger()
@@ -393,7 +391,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
deploymentObject.setString("environment", deployment.zone().environment().value());
deploymentObject.setString("region", deployment.zone().region().value());
deploymentObject.setString("instance", application.id().instance().value()); // pointless
- if ( ! rotations.isEmpty())
+ if (application.rotation().isPresent())
setRotationStatus(deployment, rotationHealthStatus, deploymentObject);
if (recurseOverDeployments(request)) // List full deployment information when recursive.
@@ -547,17 +545,16 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse rotationStatus(String tenantName, String applicationName, String instanceName, String environment, String region) {
-
ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, instanceName);
- Set<URI> rotations = controller.getRotationUris(applicationId);
- if (rotations.isEmpty())
+ Application application = controller.applications().require(applicationId);
+ if (!application.rotation().isPresent()) {
throw new NotExistsException("global rotation does not exist for '" + environment + "." + region + "'");
+ }
Slime slime = new Slime();
Cursor response = slime.setObject();
- Map<String, RotationStatus> rotationHealthStatus = controller.getHealthStatus(rotations.iterator().next().getHost());
-
+ Map<String, RotationStatus> rotationHealthStatus = controller.getHealthStatus(application.rotation().get().dnsName());
for (String rotationEndpoint : rotationHealthStatus.keySet()) {
if (rotationEndpoint.contains(toDns(environment)) && rotationEndpoint.contains(toDns(region))) {
Cursor bcpStatusObject = response.setObject("bcpStatus");
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/Rotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/Rotation.java
new file mode 100644
index 00000000000..a638756a600
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/Rotation.java
@@ -0,0 +1,49 @@
+// 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 java.util.Objects;
+
+/**
+ * Represents a global routing rotation.
+ *
+ * @author mpolden
+ */
+public class Rotation {
+
+ private final RotationId id;
+ private final String name;
+
+ public Rotation(RotationId id, String name) {
+ this.id = Objects.requireNonNull(id);
+ this.name = Objects.requireNonNull(name);
+ }
+
+ /** The ID of the allocated rotation. This value is generated by global routing system */
+ public RotationId id() {
+ return id;
+ }
+
+ /** The global rotation FQDN */
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Rotation)) return false;
+ final Rotation rotation = (Rotation) o;
+ return id().equals(rotation.id()) && name().equals(rotation.name());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id(), name());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("rotation %s -> %s", id().asString(), name());
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationId.java
new file mode 100644
index 00000000000..10b15488f6e
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationId.java
@@ -0,0 +1,42 @@
+// 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 java.util.Objects;
+
+/**
+ * ID of a global rotation.
+ *
+ * @author mpolden
+ */
+public class RotationId {
+
+ private final String id;
+
+ public RotationId(String id) {
+ this.id = id;
+ }
+
+ /** Rotation ID, e.g. rotation-42.vespa.global.routing */
+ public String asString() {
+ return id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RotationId that = (RotationId) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+ @Override
+ public String toString() {
+ return "rotation ID " + id;
+ }
+
+}
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
new file mode 100644
index 00000000000..70836232417
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java
@@ -0,0 +1,106 @@
+// 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.config.application.api.DeploymentSpec;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.ApplicationController;
+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.logging.Logger;
+import java.util.stream.Collectors;
+
+import static java.util.stream.Collectors.collectingAndThen;
+
+/**
+ * The rotation repository offers global rotations to Vespa applications.
+ *
+ * The list of rotations comes from RotationsConfig, which is set in the controller's services.xml.
+ *
+ * @author Oyvind Gronnesby
+ * @author mpolden
+ */
+public class RotationRepository {
+
+ private static final Logger log = Logger.getLogger(RotationRepository.class.getName());
+
+ private final Map<RotationId, Rotation> allRotations;
+ private final ApplicationController applications;
+
+ public RotationRepository(RotationsConfig rotationsConfig, ApplicationController applications) {
+ this.allRotations = from(rotationsConfig);
+ this.applications = applications;
+ }
+
+ /**
+ * Returns a rotation for the given application
+ *
+ * If a rotation is already assigned to the application, that rotation will be returned.
+ * If no rotation is assigned, return an available rotation. The caller is responsible for assigning the rotation.
+ *
+ * @param application The application to get a rotation for
+ */
+ public Rotation getRotation(Application application) {
+ if (application.rotation().isPresent()) {
+ return allRotations.get(application.rotation().get().id());
+ }
+ if (!application.deploymentSpec().globalServiceId().isPresent()) {
+ throw new IllegalArgumentException("global-service-id is not set in deployment spec");
+ }
+ long productionZones = application.deploymentSpec().zones().stream()
+ .filter(zone -> zone.deploysTo(Environment.prod))
+ // Global rotations don't work for nodes in corp network
+ .filter(zone -> !isCorp(zone))
+ .count();
+ if (productionZones < 2) {
+ throw new IllegalArgumentException("global-service-id is set but less than 2 prod zones are defined");
+ }
+ return findAvailableRotation(application);
+ }
+
+ /** Returns all unassigned rotations */
+ public List<RotationId> availableRotations() {
+ List<RotationId> assignedRotations = applications.asList().stream()
+ .filter(application -> application.rotation().isPresent())
+ .map(application -> application.rotation().get().id())
+ .collect(Collectors.toList());
+ List<RotationId> allRotations = new ArrayList<>(this.allRotations.keySet());
+ allRotations.removeAll(assignedRotations);
+ return Collections.unmodifiableList(allRotations);
+ }
+
+ private Rotation findAvailableRotation(Application application) {
+ List<RotationId> availableRotations = availableRotations();
+ if (availableRotations.isEmpty()) {
+ throw new IllegalStateException("Unable to assign global rotation to " + application.id()
+ + " - no rotations available");
+ }
+ // Return first available rotation
+ RotationId rotation = availableRotations.get(0);
+ log.info(String.format("Offering %s to application %s", rotation, application.id()));
+ return allRotations.get(rotation);
+ }
+
+ private static boolean isCorp(DeploymentSpec.DeclaredZone zone) {
+ return zone.region().isPresent() && zone.region().get().value().contains("corp");
+ }
+
+ /** Returns a immutable map of rotation ID to rotation sorted by rotation ID */
+ private static Map<RotationId, Rotation> from(RotationsConfig rotationConfig) {
+ return rotationConfig.rotations().entrySet().stream()
+ .map(entry -> new Rotation(new RotationId(entry.getKey()), entry.getValue().trim()))
+ .sorted(Comparator.comparing(rotation -> rotation.id().asString()))
+ .collect(collectingAndThen(Collectors.toMap(Rotation::id,
+ rotation -> rotation,
+ (k, v) -> v,
+ LinkedHashMap::new),
+ Collections::unmodifiableMap));
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java
deleted file mode 100644
index 363a2ea19cd..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// 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.rotation;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.log.LogLevel;
-import com.yahoo.vespa.hosted.controller.api.ApplicationAlias;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
-import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import org.jetbrains.annotations.NotNull;
-
-import java.net.URI;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * A rotation repository.
- *
- * @author Oyvind Gronnesby
- */
-// TODO: Fold this into ApplicationController+Application
-public class ControllerRotationRepository implements RotationRepository {
-
- private static final Logger log = Logger.getLogger(ControllerRotationRepository.class.getName());
- public static final String REMAINING_ROTATIONS_METRIC_NAME = "remaining_rotations";
-
- private final ControllerDb controllerDb;
- private final Map<RotationId, Rotation> rotationsMap;
- private final Metric metric;
-
- public ControllerRotationRepository(RotationsConfig rotationConfig, ControllerDb controllerDb, Metric metric) {
- this.controllerDb = controllerDb;
- this.rotationsMap = buildRotationsMap(rotationConfig);
- this.metric = metric;
- }
-
- private static Map<RotationId, Rotation> buildRotationsMap(RotationsConfig rotationConfig) {
- return rotationConfig.rotations().entrySet().stream()
- .map(entry -> {
- RotationId rotationId = new RotationId(entry.getKey());
- return new Rotation(rotationId, entry.getValue().trim());
- })
- .collect(Collectors.toMap(
- rotation -> rotation.rotationId,
- rotation -> rotation
- ));
- }
-
- @Override
- @NotNull
- public Set<Rotation> getOrAssignRotation(ApplicationId applicationId, DeploymentSpec deploymentSpec) {
- reportRemainingRotations();
-
- Set<RotationId> rotations = controllerDb.getRotations(applicationId);
-
- if (rotations.size() > 1) {
- log.warning(String.format("Application %s has %d > 1 rotation", applicationId, rotations.size()));
- }
-
- if (!rotations.isEmpty()) {
- return rotations.stream()
- .map(rotationsMap::get)
- .collect(Collectors.toSet());
- }
-
- if (!deploymentSpec.globalServiceId().isPresent()) {
- return Collections.emptySet();
- }
-
- long productionZoneCount = deploymentSpec.zones().stream()
- .filter(zone -> zone.deploysTo(Environment.prod))
- .filter(zone -> ! isCorp(zone)) // Global rotations don't work for nodes in corp network
- .count();
-
- if (productionZoneCount >= 2) {
- return assignRotation(applicationId);
- } else {
- throw new IllegalArgumentException("global-service-id is set but less than 2 prod zones are defined");
- }
- }
-
- private static boolean isCorp(DeploymentSpec.DeclaredZone zone) {
- return zone.region().isPresent() && zone.region().get().value().contains("corp");
- }
-
- @Override
- @NotNull
- public Set<URI> getRotationUris(ApplicationId applicationId) {
- Set<RotationId> rotations = controllerDb.getRotations(applicationId);
- if (rotations.isEmpty()) {
- return Collections.emptySet();
- }
- else {
- ApplicationAlias applicationAlias = new ApplicationAlias(applicationId);
- return Collections.singleton(applicationAlias.toHttpUri());
- }
- }
-
- private Set<Rotation> assignRotation(ApplicationId applicationId) {
- Set<RotationId> availableRotations = availableRotations();
- if (availableRotations.isEmpty()) {
- String message = "Unable to assign global rotation to "
- + applicationId + " - no rotations available";
- log.info(message);
- throw new RuntimeException(message);
- }
-
- for (RotationId rotationId : availableRotations) {
- if (controllerDb.assignRotation(rotationId, applicationId)) {
- log.info(String.format("Assigned rotation %s to application %s", rotationId, applicationId));
- Rotation rotation = this.rotationsMap.get(rotationId);
- return Collections.singleton(rotation);
- }
- }
-
- log.info(String.format("Rotation: No rotations assigned with %s rotations available", availableRotations.size()));
- return Collections.emptySet();
- }
-
- private Set<RotationId> availableRotations() {
- Set<RotationId> assignedRotations = controllerDb.getRotations();
- Set<RotationId> allRotations = new HashSet<>(rotationsMap.keySet());
- allRotations.removeAll(assignedRotations);
- return allRotations;
- }
-
- private void reportRemainingRotations() {
- try {
- int freeRotationsCount = availableRotations().size();
- log.log(LogLevel.INFO, "Rotation: {0} global rotations remaining", freeRotationsCount);
- metric.set(REMAINING_ROTATIONS_METRIC_NAME, freeRotationsCount,
- metric.createContext(Collections.emptyMap()));
- } catch (Exception e) {
- log.log(LogLevel.INFO, "Failed to report rotations metric", e);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/MemoryRotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/MemoryRotationRepository.java
deleted file mode 100644
index 4e333f0268b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/MemoryRotationRepository.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// 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.rotation;
-
-import com.google.common.collect.ImmutableSet;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
-import org.jetbrains.annotations.NotNull;
-
-import java.net.URI;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.stream.Collectors;
-
-/**
- * A rotation repository backed by in-memory data structures
- *
- * @author bratseth
- */
-public class MemoryRotationRepository implements RotationRepository {
-
- private final Map<ApplicationId, Set<Rotation>> rotations = new HashMap<>();
-
- @NotNull
- @Override
- public Set<Rotation> getOrAssignRotation(ApplicationId application, DeploymentSpec deploymentSpec) {
- if (rotations.containsKey(application)) {
- return rotations.get(application);
- }
- Set<Rotation> rotations = ImmutableSet.of(new Rotation(
- new RotationId("generated-by-routing-service-" + UUID.randomUUID().toString()),
- "fake-global-rotation-" + application.toShortString())
- );
- this.rotations.put(application, rotations);
- return rotations;
- }
-
- @NotNull
- @Override
- public Set<URI> getRotationUris(ApplicationId applicationId) {
- Set<Rotation> rotations = this.rotations.get(applicationId);
- if (rotations == null) {
- return Collections.emptySet();
- }
- return rotations.stream()
- .map(rotation -> URI.create("http://" + rotation.rotationName))
- .collect(Collectors.toSet());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/RotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/RotationRepository.java
deleted file mode 100644
index b1f7b33e58e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/RotationRepository.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// 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.rotation;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
-import org.jetbrains.annotations.NotNull;
-
-import java.net.URI;
-import java.util.Set;
-
-/**
- * A rotation repository assigns global rotations to Vespa applications. It does not take into account
- * whether an application qualifies or not, but it assumes that each application should get only
- * one.
- *
- * The list of rotations comes from the RotationsConfig, set in the controller's services.xml.
- * Assignments are persisted with the RotationId as the primary key. When we assign the
- * rotation to an application we try to put the mapping RotationId -&gt; Application. If a
- * mapping already exists for that RotationId, the assignment will fail.
- *
- * @author Oyvind Gronnesby
- */
-public interface RotationRepository {
-
- // TODO: Change to use provision.ApplicationId
- // TODO: Move the persistence into ControllerDb (done), and then collapse the 2 implementations and the interface into one
-
- /**
- * If any rotations are assigned to the application, these will be returned.
- * If no rotations are assigned, assign one rotation to the application and return that.
- *
- * @param applicationId ID of the application to get or assign rotation for
- * @param deploymentSpec Spec of current application being deployed
- * @return Set of rotations assigned (may be empty)
- */
- @NotNull
- Set<Rotation> getOrAssignRotation(ApplicationId applicationId, DeploymentSpec deploymentSpec);
-
- /**
- * Get the external visible rotation URIs for this application.
- *
- * @param applicationId ID of the application to get or assign rotation for
- */
- @NotNull
- Set<URI> getRotationUris(ApplicationId applicationId);
-
-}
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 d0c1fd95427..b0fb820ca4c 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
@@ -11,9 +11,7 @@ import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.Zone;
-import com.yahoo.slime.Slime;
import com.yahoo.vespa.config.SlimeUtils;
-import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
@@ -601,6 +599,7 @@ public class ControllerTest {
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.environment(Environment.prod)
+ .globalServiceId("foo")
.region("us-west-1")
.region("us-central-1") // Two deployments should result in DNS alias being registered once
.build();
@@ -610,7 +609,7 @@ public class ControllerTest {
Optional<Record> record = tester.controllerTester().nameService().findRecord(Record.Type.CNAME, "app1.tenant1.global.vespa.yahooapis.com");
assertTrue(record.isPresent());
assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name());
- assertEquals("fake-global-rotation-tenant1.app1", record.get().value());
+ assertEquals("rotation-fqdn-01", record.get().value());
}
@Test
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
index 8f9c22f8b81..f202deafa8a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
@@ -41,7 +41,7 @@ import com.yahoo.vespa.hosted.controller.persistence.MemoryControllerDb;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
import com.yahoo.vespa.hosted.controller.routing.MockRoutingGenerator;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.rotation.MemoryRotationRepository;
+import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
import java.util.Optional;
@@ -63,22 +63,31 @@ public final class ControllerTester {
private final GitHubMock gitHub;
private final CuratorDb curator;
private final MemoryNameService nameService;
+ private final RotationsConfig rotationsConfig;
private Controller controller;
public ControllerTester() {
this(new MemoryControllerDb(), new AthenzDbMock(), new ManualClock(), new ConfigServerClientMock(),
- new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), new MemoryNameService());
+ new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), defaultRotationsConfig(),
+ new MemoryNameService());
}
public ControllerTester(ManualClock clock) {
this(new MemoryControllerDb(), new AthenzDbMock(), clock, new ConfigServerClientMock(),
- new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), new MemoryNameService());
+ new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), defaultRotationsConfig(),
+ new MemoryNameService());
+ }
+
+ public ControllerTester(RotationsConfig rotationsConfig) {
+ this(new MemoryControllerDb(), new AthenzDbMock(), new ManualClock(), new ConfigServerClientMock(),
+ new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), rotationsConfig, new MemoryNameService());
}
private ControllerTester(ControllerDb db, AthenzDbMock athenzDb, ManualClock clock,
ConfigServerClientMock configServer, ZoneRegistryMock zoneRegistry,
- GitHubMock gitHub, CuratorDb curator, MemoryNameService nameService) {
+ GitHubMock gitHub, CuratorDb curator, RotationsConfig rotationsConfig,
+ MemoryNameService nameService) {
this.db = db;
this.athenzDb = athenzDb;
this.clock = clock;
@@ -87,7 +96,8 @@ public final class ControllerTester {
this.gitHub = gitHub;
this.curator = curator;
this.nameService = nameService;
- this.controller = createController(db, curator, configServer, clock, gitHub, zoneRegistry,
+ this.rotationsConfig = rotationsConfig;
+ this.controller = createController(db, curator, rotationsConfig, configServer, clock, gitHub, zoneRegistry,
athenzDb, nameService);
}
@@ -109,7 +119,8 @@ public final class ControllerTester {
/** Create a new controller instance. Useful to verify that controller state is rebuilt from persistence */
public final void createNewController() {
- controller = createController(db, curator, configServer, clock, gitHub, zoneRegistry, athenzDb, nameService);
+ controller = createController(db, curator, rotationsConfig, configServer, clock, gitHub, zoneRegistry, athenzDb,
+ nameService);
}
/** Creates the given tenant and application and deploys it */
@@ -214,13 +225,13 @@ public final class ControllerTester {
return new LockedApplication(application, new Lock("/test", new MockCurator()));
}
- private static Controller createController(ControllerDb db, CuratorDb curator,
+ private static Controller createController(ControllerDb db, CuratorDb curator, RotationsConfig rotationsConfig,
ConfigServerClientMock configServerClientMock, ManualClock clock,
GitHubMock gitHubClientMock, ZoneRegistryMock zoneRegistryMock,
AthenzDbMock athensDb, MemoryNameService nameService) {
Controller controller = new Controller(db,
curator,
- new MemoryRotationRepository(),
+ rotationsConfig,
gitHubClientMock,
new MemoryEntityService(),
new MockOrganization(clock),
@@ -237,4 +248,13 @@ public final class ControllerTester {
return controller;
}
+ private static RotationsConfig defaultRotationsConfig() {
+ RotationsConfig.Builder builder = new RotationsConfig.Builder();
+ for (int i = 1; i <= 10; i++) {
+ String id = String.format("%02d", i);
+ builder = builder.rotations("rotation-id-" + id, "rotation-fqdn-" + id);
+ }
+ return new RotationsConfig(builder);
+ }
+
}
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 a2b24864d1e..3311cffa078 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
@@ -28,6 +28,7 @@ public class ApplicationPackageBuilder {
private String upgradePolicy = null;
private Environment environment = Environment.prod;
+ private String globalServiceId = null;
private final StringBuilder environmentBody = new StringBuilder();
private final StringBuilder validationOverridesBody = new StringBuilder();
private final StringBuilder blockChange = new StringBuilder();
@@ -44,6 +45,11 @@ public class ApplicationPackageBuilder {
return this;
}
+ public ApplicationPackageBuilder globalServiceId(String globalServiceId) {
+ this.globalServiceId = globalServiceId;
+ return this;
+ }
+
public ApplicationPackageBuilder region(String regionName) {
environmentBody.append(" <region active='true'>");
environmentBody.append(regionName);
@@ -112,6 +118,11 @@ public class ApplicationPackageBuilder {
xml.append(blockChange);
xml.append(" <");
xml.append(environment.value());
+ if (globalServiceId != null) {
+ xml.append(" global-service-id='");
+ xml.append(globalServiceId);
+ xml.append("'");
+ }
xml.append(">\n");
xml.append(environmentBody);
xml.append(" </");
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 2c1471b29b6..bf869230e8d 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
@@ -25,6 +25,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.SourceRevision;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import org.junit.Test;
import java.io.IOException;
@@ -86,7 +87,8 @@ public class ApplicationSerializerTest {
Optional.of(new Change.VersionChange(Version.fromString("6.7"))),
true,
Optional.of(IssueId.from("1234")),
- new MetricsService.ApplicationMetrics(0.5, 0.9));
+ new MetricsService.ApplicationMetrics(0.5, 0.9),
+ Optional.of(new RotationId("my-rotation")));
Application serialized = applicationSerializer.fromSlime(applicationSerializer.toSlime(original));
@@ -115,6 +117,7 @@ public class ApplicationSerializerTest {
assertEquals(original.ownershipIssueId(), serialized.ownershipIssueId());
assertEquals(original.deploying(), serialized.deploying());
+ assertEquals(original.rotation().get().id(), serialized.rotation().get().id());
// Test cluster utilization
assertEquals(0, serialized.deployments().get(zone1).clusterUtils().size());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
index 044c5d75d12..631ceab98a5 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
@@ -25,60 +25,70 @@ import static org.junit.Assert.assertEquals;
public class ControllerContainerTest {
protected JDisc container;
+
@Before
public void startContainer() { container = JDisc.fromServicesXml(controllerServicesXml, Networking.disable); }
+
@After
public void stopContainer() { container.close(); }
private final String controllerServicesXml =
- "<jdisc version='1.0'>" +
- " <config name='vespa.hosted.zone.config.zone'>" +
- " <system>main</system>" +
- " </config>" +
- " <component id='com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.chef.ChefMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.github.GitHubMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.organization.MockOrganization'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.ConfigServerClientMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.ZoneRegistryMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.Controller'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.ConfigServerProxyMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.integration.MockMetricsService'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.maintenance.JobControl'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.persistence.MemoryControllerDb'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.restapi.application.MockAuthorizer'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.routing.MockRoutingGenerator'/>" +
- " <component id='com.yahoo.vespa.hosted.rotation.MemoryRotationRepository'/>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.RootHandler'>" +
- " <binding>http://*/</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>" +
- " <binding>http://*/application/v4/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.DeploymentApiHandler'>" +
- " <binding>http://*/deployment/v1/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.controller.ControllerApiHandler'>" +
- " <binding>http://*/controller/v1/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.screwdriver.ScrewdriverApiHandler'>" +
- " <binding>http://*/screwdriver/v1/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v1.ZoneApiHandler'>" +
- " <binding>http://*/zone/v1</binding>" +
- " <binding>http://*/zone/v1/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v2.ZoneApiHandler'>" +
- " <binding>http://*/zone/v2</binding>" +
- " <binding>http://*/zone/v2/*</binding>" +
- " </handler>" +
+ "<jdisc version='1.0'>\n" +
+ " <config name='vespa.hosted.zone.config.zone'>\n" +
+ " <system>main</system>\n" +
+ " </config>\n" +
+ " <config name=\"vespa.hosted.rotation.config.rotations\">\n" +
+ " <rotations>\n" +
+ " <item key=\"rotation-id-1\">rotation-fqdn-1</item>\n" +
+ " <item key=\"rotation-id-2\">rotation-fqdn-2</item>\n" +
+ " <item key=\"rotation-id-3\">rotation-fqdn-3</item>\n" +
+ " <item key=\"rotation-id-4\">rotation-fqdn-4</item>\n" +
+ " <item key=\"rotation-id-5\">rotation-fqdn-5</item>\n" +
+ " </rotations>\n" +
+ " </config>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.chef.ChefMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.github.GitHubMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.organization.MockOrganization'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.ConfigServerClientMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.ZoneRegistryMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.Controller'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.ConfigServerProxyMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.integration.MockMetricsService'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.maintenance.JobControl'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.persistence.MemoryControllerDb'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.restapi.application.MockAuthorizer'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.routing.MockRoutingGenerator'/>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.RootHandler'>\n" +
+ " <binding>http://*/</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>\n" +
+ " <binding>http://*/application/v4/*</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.DeploymentApiHandler'>\n" +
+ " <binding>http://*/deployment/v1/*</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.controller.ControllerApiHandler'>\n" +
+ " <binding>http://*/controller/v1/*</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.screwdriver.ScrewdriverApiHandler'>\n" +
+ " <binding>http://*/screwdriver/v1/*</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v1.ZoneApiHandler'>\n" +
+ " <binding>http://*/zone/v1</binding>\n" +
+ " <binding>http://*/zone/v1/*</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v2.ZoneApiHandler'>\n" +
+ " <binding>http://*/zone/v2</binding>\n" +
+ " <binding>http://*/zone/v2/*</binding>\n" +
+ " </handler>\n" +
"</jdisc>";
protected void assertResponse(Request request, int responseStatus, String responseMessage) throws IOException {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
index 7902e49288c..540550e871d 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
@@ -64,13 +64,18 @@ import static com.yahoo.application.container.handler.Request.Method.PUT;
public class ApplicationApiTest extends ControllerContainerTest {
private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/";
+
private static final ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.environment(Environment.prod)
+ .globalServiceId("foo")
.region("corp-us-east-1")
+ .region("us-east-3")
+ .region("us-west-1")
.build();
+
private static final String athenzUserDomain = "domain1";
- private static final String athenzScrewdriverDomain = AthenzUtils.SCREWDRIVER_DOMAIN.id();
+ private static final String athenzScrewdriverDomain = AthenzUtils.SCREWDRIVER_DOMAIN.id();
@Test
public void testApplicationApi() throws Exception {
@@ -263,13 +268,6 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", DELETE),
"Deactivated tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default");
- // DELETE an application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE),
- "");
- // DELETE a tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
- new File("tenant-without-applications.json"));
-
// PUT (create) the authenticated user
byte[] data = new byte[0];
tester.assertResponse(request("/application/v4/user?user=newuser&domain=by", PUT)
@@ -303,6 +301,13 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/promote", POST),
"{\"message\":\"Successfully copied environment hosted-instance_tenant1_application1_placeholder_component_default to hosted-instance_tenant1_application1_us-west-1_prod_default\"}");
+ // DELETE an application
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE),
+ "");
+ // DELETE a tenant
+ tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
+ new File("tenant-without-applications.json"));
+
controllerTester.controller().deconstruct();
}
@@ -376,6 +381,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
// New zone is added before us-east-3
applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
// These decides the ordering of deploymentJobs and instances in the response
.region("us-west-1")
.region("us-east-3")
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json
index 6442ddf5c02..961e005bfbd 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json
@@ -205,7 +205,7 @@
],
"compileVersion": "(ignore)",
"globalRotations": [
- "http://fake-global-rotation-tenant1.application1"
+ "http://application1.tenant1.global.vespa.yahooapis.com:4080/"
],
"instances": [
{
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json
index fdd3dcc4d5c..3924cf51ca9 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json
@@ -146,7 +146,7 @@
],
"compileVersion": "(ignore)",
"globalRotations": [
- "http://fake-global-rotation-tenant1.application1"
+ "http://application1.tenant1.global.vespa.yahooapis.com:4080/"
],
"instances": [
{
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json
index 41556c04209..5030fc7d0a6 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json
@@ -146,7 +146,7 @@
],
"compileVersion": "6.1.0",
"globalRotations": [
- "http://fake-global-rotation-tenant1.application1"
+ "http://application1.tenant1.global.vespa.yahooapis.com:4080/"
],
"instances": [
@include(dev-us-west-1.json),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationTest.java
new file mode 100644
index 00000000000..8fc41b8daa6
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationTest.java
@@ -0,0 +1,171 @@
+// 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.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRotation;
+import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.net.URI;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * @author Oyvind Gronnesby
+ * @author mpolden
+ */
+public class RotationTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private final RotationsConfig rotationsConfig = new RotationsConfig(
+ new RotationsConfig.Builder()
+ .rotations("foo-1", "foo-1.com")
+ .rotations("foo-2", "foo-2.com")
+ );
+
+ private final RotationsConfig rotationsConfigWhitespaces = new RotationsConfig(
+ new RotationsConfig.Builder()
+ .rotations("foo-1", "\n foo-1.com \n")
+ .rotations("foo-2", "foo-2.com")
+ );
+
+ private final ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .region("us-west-1")
+ .build();
+
+ private DeploymentTester tester;
+ private RotationRepository repository;
+ private Application application;
+
+ @Before
+ public void before() {
+ tester = new DeploymentTester(new ControllerTester(rotationsConfig));
+ repository = tester.controller().applications().rotationRepository();
+ application = tester.createApplication("app1", "tenant1", 11L,1L);
+ }
+
+ @Test
+ public void assigns_and_reuses_rotation() {
+ // Deploying assigns a rotation
+ tester.deployCompletely(application, applicationPackage);
+ Rotation expected = new Rotation(new RotationId("foo-1"), "foo-1.com");
+
+ application = tester.applications().require(application.id());
+ assertEquals(expected.id(), application.rotation().get().id());
+ assertEquals(URI.create("http://app1.tenant1.global.vespa.yahooapis.com:4080/"),
+ application.rotation().get().url());
+ Rotation rotation = repository.getRotation(tester.applications().require(application.id()));
+ assertEquals(expected, rotation);
+
+ // Deploying once more assigns same rotation
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .region("us-west-1")
+ .searchDefinition("search foo { }") // Update application package so there is something to deploy
+ .build();
+ tester.deployCompletely(application, applicationPackage);
+ assertEquals(expected.id(), tester.applications().require(application.id()).rotation().get().id());
+ }
+
+ @Test
+ public void strips_whitespace_in_rotation_fqdn() {
+ DeploymentTester tester = new DeploymentTester(new ControllerTester(rotationsConfigWhitespaces));
+ RotationRepository repository = tester.controller().applications().rotationRepository();
+ Application application = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ tester.deployCompletely(application, applicationPackage);
+ application = tester.applications().require(application.id());
+
+ Rotation rotation = repository.getRotation(application);
+ Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com");
+ assertEquals(assignedRotation, rotation);
+ }
+
+
+ @Test
+ public void out_of_rotations() {
+ // Assigns 1 rotation
+ tester.deployCompletely(application, applicationPackage);
+
+ // Assigns 1 more
+ Application application2 = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ tester.deployCompletely(application2, applicationPackage);
+
+ // We're now out of rotations
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("no rotations available");
+ Application application3 = tester.createApplication("app3", "tenant3", 33L,
+ 3L);
+ tester.deployCompletely(application3, applicationPackage);
+ }
+
+ @Test
+ public void too_few_zones() {
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .build();
+ Application application = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("less than 2 prod zones are defined");
+ tester.deployCompletely(application, applicationPackage);
+ }
+
+ @Test
+ public void no_rotation_assigned_for_application_without_service_id() {
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .region("us-east-3")
+ .region("us-west-1")
+ .build();
+ tester.deployCompletely(application, applicationPackage);
+ Application app = tester.applications().require(application.id());
+ Optional<ApplicationRotation> rotation = app.rotation();
+ assertFalse(rotation.isPresent());
+ }
+
+ @Test
+ public void application_with_only_one_non_corp_region() {
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .region("corp-us-east-1")
+ .build();
+ Application application = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("less than 2 prod zones are defined");
+ tester.deployCompletely(application, applicationPackage);
+ }
+
+ @Test
+ public void application_with_corp_region_and_two_non_corp_region() {
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .region("corp-us-east-1")
+ .region("us-west-1")
+ .build();
+ Application application = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ tester.deployCompletely(application, applicationPackage);
+ assertEquals(new RotationId("foo-1"), tester.applications().require(application.id())
+ .rotation().get().id());
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java
deleted file mode 100644
index b4074fc1944..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java
+++ /dev/null
@@ -1,212 +0,0 @@
-// 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.rotation;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
-import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
-import com.yahoo.vespa.hosted.controller.persistence.MemoryControllerDb;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import java.io.StringReader;
-import java.net.URI;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-/**
- * @author Oyvind Gronnesby
- */
-public class ControllerRotationRepositoryTest {
-
- private final RotationsConfig rotationsConfig = new RotationsConfig(
- new RotationsConfig.Builder()
- .rotations("foo-1", "foo-1.com")
- .rotations("foo-2", "foo-2.com")
- );
- private final RotationsConfig rotationsConfigWhitespaces = new RotationsConfig(
- new RotationsConfig.Builder()
- .rotations("foo-1", "\n foo-1.com \n")
- .rotations("foo-2", "foo-2.com")
- );
- private final ControllerDb controllerDb = new MemoryControllerDb();
- private final ApplicationId applicationId = ApplicationId.from("msbe", "tumblr-search", "default");
-
- @Rule public ExpectedException thrown = ExpectedException.none();
-
- private final DeploymentSpec deploymentSpec = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod global-service-id='foo'>" +
- " <region active='true'>us-east</region>" +
- " <region active='true'>us-west</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private final DeploymentSpec deploymentSpecOneRegion = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod global-service-id='nalle'>" +
- " <region active='true'>us-east</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private final DeploymentSpec deploymentSpecNoServiceId = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod>" +
- " <region active='true'>us-east</region>" +
- " <region active='true'>us-west</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private final DeploymentSpec deploymentSpecOnlyOneNonCorpRegion = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod global-service-id='nalle'>" +
- " <region active='true'>us-east</region>" +
- " <region active='true'>corp-us-west</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private final DeploymentSpec deploymentSpecWithAdditionalCorpZone = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod global-service-id='nalle'>" +
- " <region active='true'>us-east</region>" +
- " <region active='true'>corp-us-west</region>" +
- " <region active='true'>us-west</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private ControllerRotationRepository repository;
- private ControllerRotationRepository repositoryWhitespaces;
- private Metric metric;
-
- @Before
- public void setup_repository() {
- metric = mock(Metric.class);
- repository = new ControllerRotationRepository(rotationsConfig, controllerDb, metric);
- repositoryWhitespaces = new ControllerRotationRepository(rotationsConfigWhitespaces, controllerDb, metric);
- controllerDb.assignRotation(new RotationId("foo-1"), applicationId);
- }
-
- @Test
- public void application_with_rotation_reused() {
- Set<Rotation> rotations = repository.getOrAssignRotation(applicationId, deploymentSpec);
- Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com");
- assertContainsOnly(assignedRotation, rotations);
- }
-
- @Test
- public void names_stripped() {
- Set<Rotation> rotations = repositoryWhitespaces.getOrAssignRotation(applicationId, deploymentSpec);
- Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com");
- assertContainsOnly(assignedRotation, rotations);
- }
-
- @Test
- public void application_without_rotation() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
- Set<Rotation> rotations = repository.getOrAssignRotation(other, deploymentSpec);
- Rotation assignedRotation = new Rotation(new RotationId("foo-2"), "foo-2.com");
- assertContainsOnly(assignedRotation, rotations);
- verify(metric).set(eq(ControllerRotationRepository.REMAINING_ROTATIONS_METRIC_NAME), eq(1), any());
- }
-
- @Test
- public void application_without_rotation_but_none_left() {
- application_without_rotation(); // run this test to assign last rotation
- ApplicationId third = ApplicationId.from("thirdtenant", "thirdapplication", "default");
-
- thrown.expect(RuntimeException.class);
- thrown.expectMessage("no rotations available");
-
- repository.getOrAssignRotation(third, deploymentSpec);
- verify(metric).set(eq(ControllerRotationRepository.REMAINING_ROTATIONS_METRIC_NAME), eq(0), any());
- }
-
- @Test
- public void application_without_rotation_but_does_not_qualify() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
-
- thrown.expect(RuntimeException.class);
- thrown.expectMessage("less than 2 prod zones are defined");
-
- repository.getOrAssignRotation(other, deploymentSpecOneRegion);
- }
-
- @Test
- public void application_with_rotation_but_does_not_qualify() {
- Set<Rotation> rotations = repository.getOrAssignRotation(applicationId, deploymentSpecOneRegion);
- Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com");
- assertContainsOnly(assignedRotation, rotations);
- }
-
- @Test
- public void application_with_rotation_is_listed() {
- repository.getOrAssignRotation(applicationId, deploymentSpec);
- Set<URI> uris = repository.getRotationUris(applicationId);
- assertEquals(Collections.singleton(URI.create("http://tumblr-search.msbe.global.vespa.yahooapis.com:4080/")), uris);
- }
-
- @Test
- public void application_without_rotation_is_empty() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
- Set<URI> uris = repository.getRotationUris(other);
- assertTrue(uris.isEmpty());
- }
-
- @Test
- public void application_without_serviceid_and_two_regions() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
- Set<Rotation> rotations = repository.getOrAssignRotation(other, deploymentSpecNoServiceId);
- assertTrue(rotations.isEmpty());
- }
-
- @Test
- public void application_with_only_one_non_corp_region() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
-
- thrown.expect(RuntimeException.class);
- thrown.expectMessage("less than 2 prod zones are defined");
-
- repository.getOrAssignRotation(other, deploymentSpecOnlyOneNonCorpRegion);
- }
-
- @Test
- public void application_with_corp_region_and_two_non_corp_region() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
- Set<Rotation> rotations = repository.getOrAssignRotation(other, deploymentSpecWithAdditionalCorpZone);
- assertContainsOnly(new Rotation(new RotationId("foo-2"), "foo-2.com"), rotations);
- }
-
- private static <T> void assertContainsOnly(T item, Collection<T> items) {
- assertTrue("Collection contains only " + item.toString(),
- items.size() == 1 && items.contains(item));
- }
-
-}