diff options
Diffstat (limited to 'controller-server')
29 files changed, 744 insertions, 708 deletions
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 -> 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)); - } - -} |