diff options
author | Jon Marius Venstad <venstad@gmail.com> | 2019-09-28 19:13:03 +0200 |
---|---|---|
committer | Jon Marius Venstad <venstad@gmail.com> | 2019-09-28 19:27:03 +0200 |
commit | 1091542c58fd3d8a0b19a8925e15bb4752fb5fcb (patch) | |
tree | c9159f189baf760100fda0ef9f6c2c02c528e2cc /controller-server | |
parent | 0e85eb225ee4e48d89db5b6c638356bd8f0dc696 (diff) |
Put Instances in Application, and replace ALL THE THINGS!!!
Diffstat (limited to 'controller-server')
67 files changed, 3151 insertions, 2317 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 43f9b851fa2..10ce3b93614 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 @@ -1,27 +1,24 @@ // Copyright 2018 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.ImmutableMap; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.Environment; -import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.config.provision.InstanceName; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.vespa.hosted.controller.application.ApplicationActivity; -import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; -import com.yahoo.vespa.hosted.controller.rotation.RotationStatus; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import java.time.Instant; -import java.util.Collections; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -29,7 +26,6 @@ import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.OptionalLong; -import java.util.function.Function; import java.util.stream.Collectors; /** @@ -41,66 +37,80 @@ import java.util.stream.Collectors; */ public class Application { - private final ApplicationId id; + private final TenantAndApplicationId id; private final Instant createdAt; private final DeploymentSpec deploymentSpec; private final ValidationOverrides validationOverrides; - private final Map<ZoneId, Deployment> deployments; - private final DeploymentJobs deploymentJobs; + private final OptionalLong projectId; + private final boolean internal; private final Change change; private final Change outstandingChange; + private final Optional<IssueId> deploymentIssueId; private final Optional<IssueId> ownershipIssueId; private final Optional<User> owner; private final OptionalInt majorVersion; private final ApplicationMetrics metrics; private final Optional<String> pemDeployKey; - private final List<AssignedRotation> rotations; - private final RotationStatus rotationStatus; + private final Map<InstanceName, Instance> instances; /** Creates an empty application. */ - public Application(ApplicationId id, Instant now) { - this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Collections.emptyMap(), - new DeploymentJobs(OptionalLong.empty(), Collections.emptyList(), Optional.empty(), false), - Change.empty(), Change.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), - new ApplicationMetrics(0, 0), - Optional.empty(), Collections.emptyList(), RotationStatus.EMPTY); - } - - /** Used from persistence layer: Do not use */ - public Application(ApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, - List<Deployment> deployments, DeploymentJobs deploymentJobs, Change change, - Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, - OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey, - List<AssignedRotation> rotations, RotationStatus rotationStatus) { - this(id, createdAt, deploymentSpec, validationOverrides, - deployments.stream().collect(Collectors.toMap(Deployment::zone, Function.identity())), - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, rotations, rotationStatus); - } - - Application(ApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, - Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change, - Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, - OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey, - List<AssignedRotation> rotations, RotationStatus rotationStatus) { + public Application(TenantAndApplicationId id, Instant now) { + this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Change.empty(), Change.empty(), + Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), + new ApplicationMetrics(0, 0), Optional.empty(), OptionalLong.empty(), true, List.of()); + } + + // DO NOT USE! For serialization purposes, only. + public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, + Change change, Change outstandingChange, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner, + OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey, OptionalLong projectId, + boolean internal, Collection<Instance> instances) { this.id = Objects.requireNonNull(id, "id cannot be null"); this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null"); this.deploymentSpec = Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null"); this.validationOverrides = Objects.requireNonNull(validationOverrides, "validationOverrides cannot be null"); - this.deployments = ImmutableMap.copyOf(Objects.requireNonNull(deployments, "deployments cannot be null")); - this.deploymentJobs = Objects.requireNonNull(deploymentJobs, "deploymentJobs cannot be null"); this.change = Objects.requireNonNull(change, "change cannot be null"); this.outstandingChange = Objects.requireNonNull(outstandingChange, "outstandingChange cannot be null"); + this.deploymentIssueId = Objects.requireNonNull(deploymentIssueId, "deploymentIssueId cannot be null"); this.ownershipIssueId = Objects.requireNonNull(ownershipIssueId, "ownershipIssueId cannot be null"); this.owner = Objects.requireNonNull(owner, "owner cannot be null"); this.majorVersion = Objects.requireNonNull(majorVersion, "majorVersion cannot be null"); this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null"); - this.pemDeployKey = pemDeployKey; - this.rotations = List.copyOf(Objects.requireNonNull(rotations, "rotations cannot be null")); - this.rotationStatus = Objects.requireNonNull(rotationStatus, "rotationStatus cannot be null"); + this.pemDeployKey = Objects.requireNonNull(pemDeployKey, "pemDeployKey cannot be null"); + this.projectId = Objects.requireNonNull(projectId, "projectId cannot be null"); + this.internal = internal; + this.instances = instances.stream().collect(Collectors.toUnmodifiableMap(instance -> instance.id().instance(), + instance -> instance)); + } + + /** Returns an aggregate application, from the given instances, if at least one. */ + public static Optional<Application> aggregate(List<Instance> instances) { + if (instances.isEmpty()) + return Optional.empty(); + + Instance base = instances.stream() + .filter(instance -> instance.id().instance().isDefault()) + .findFirst() + .orElse(instances.iterator().next()); + + return Optional.of(new Application(TenantAndApplicationId.from(base.id()), base.createdAt(), base.deploymentSpec(), + base.validationOverrides(), base.change(), base.outstandingChange(), + base.deploymentJobs().issueId(), base.ownershipIssueId(), base.owner(), + base.majorVersion(), base.metrics(), base.pemDeployKey(), + base.deploymentJobs().projectId(), base.deploymentJobs().deployedInternally(), instances)); + } + + /** Returns an old Instance representation of this and the given instance, for serialisation. */ + public Instance legacy(InstanceName instance) { + Instance base = require(instance); + + return new Instance(base.id(), createdAt, deploymentSpec, validationOverrides, base.deployments(), + new DeploymentJobs(projectId, base.deploymentJobs().jobStatus().values(), deploymentIssueId, internal), + change, outstandingChange, ownershipIssueId, owner, + majorVersion, metrics, pemDeployKey, base.rotations(), base.rotationStatus()); } - public ApplicationId id() { return id; } + public TenantAndApplicationId id() { return id; } public Instant createdAt() { return createdAt; } @@ -110,6 +120,13 @@ public class Application { */ public DeploymentSpec deploymentSpec() { return deploymentSpec; } + /** Returns the project id of this application, if it has any. */ + public OptionalLong projectId() { return projectId; } + + /** Returns whether this application is run on the internal deployment pipeline. */ + // TODO jonmv: Remove, as will be always true. + public boolean internal() { return internal; } + /** * Returns the last deployed validation overrides of this application, * or the empty validation overrides if it has never been deployed @@ -117,20 +134,16 @@ public class Application { */ public ValidationOverrides validationOverrides() { return validationOverrides; } - /** Returns an immutable map of the current deployments of this */ - public Map<ZoneId, Deployment> deployments() { return deployments; } + /** Returns the instances of this application */ + public Map<InstanceName, Instance> instances() { return instances; } - /** - * Returns an immutable map of the current *production* deployments of this - * (deployments also includes manually deployed environments) - */ - public Map<ZoneId, Deployment> productionDeployments() { - return ImmutableMap.copyOf(deployments.values().stream() - .filter(deployment -> deployment.zone().environment() == Environment.prod) - .collect(Collectors.toMap(Deployment::zone, Function.identity()))); - } + /** Returns the instance with the given name, if it exists. */ + public Optional<Instance> get(InstanceName instance) { return Optional.ofNullable(instances.get(instance)); } - public DeploymentJobs deploymentJobs() { return deploymentJobs; } + /** Returns the instance with the given name, or throws. */ + public Instance require(InstanceName instance) { + return get(instance).orElseThrow(() -> new IllegalArgumentException("Unknown instance '" + instance + "'")); + } /** * Returns base change for this application, i.e., the change that is deployed outside block windows. @@ -144,6 +157,11 @@ public class Application { */ public Change outstandingChange() { return outstandingChange; } + /** Returns ID of any open deployment issue filed for this */ + public Optional<IssueId> deploymentIssueId() { + return deploymentIssueId; + } + /** Returns ID of the last ownership issue filed for this */ public Optional<IssueId> ownershipIssueId() { return ownershipIssueId; @@ -166,19 +184,26 @@ public class Application { /** Returns activity for this */ public ApplicationActivity activity() { - return ApplicationActivity.from(deployments.values()); + return ApplicationActivity.from(instances.values().stream() + .flatMap(instance -> instance.deployments().values().stream()) + .collect(Collectors.toUnmodifiableList())); } + public Map<InstanceName, List<Deployment>> productionDeployments() { + return instances.values().stream() + .collect(Collectors.toUnmodifiableMap(Instance::name, + instance -> List.copyOf(instance.productionDeployments().values()))); + } /** * Returns the oldest platform version this has deployed in a permanent zone (not test or staging). * - * This is unfortunately quite similar to {@link ApplicationController#oldestInstalledPlatform(ApplicationId)}, + * This is unfortunately quite similar to {@link ApplicationController#oldestInstalledPlatform(TenantAndApplicationId)}, * but this checks only what the controller has deployed to the production zones, while that checks the node repository * to see what's actually installed on each node. Thus, this is the right choice for, e.g., target Vespa versions for * new deployments, while that is the right choice for version to compile against. */ public Optional<Version> oldestDeployedPlatform() { - return productionDeployments().values().stream() + return productionDeployments().values().stream().flatMap(List::stream) .map(Deployment::version) .min(Comparator.naturalOrder()); } @@ -187,23 +212,13 @@ public class Application { * Returns the oldest application version this has deployed in a permanent zone (not test or staging). */ public Optional<ApplicationVersion> oldestDeployedApplication() { - return productionDeployments().values().stream() + return productionDeployments().values().stream().flatMap(List::stream) .map(Deployment::applicationVersion) .min(Comparator.naturalOrder()); } - /** Returns all rotations assigned to this */ - public List<AssignedRotation> rotations() { - return rotations; - } - public Optional<String> pemDeployKey() { return pemDeployKey; } - /** Returns the status of the global rotation(s) assigned to this */ - public RotationStatus rotationStatus() { - return rotationStatus; - } - @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 5c629173f3d..ab0a110be29 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 @@ -10,6 +10,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; @@ -100,6 +101,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.active; +import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.inactive; import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.reserved; import static java.util.Comparator.naturalOrder; @@ -150,10 +152,11 @@ public class ApplicationController { // Update serialization format of all applications Once.after(Duration.ofMinutes(1), () -> { + curator.clearInstanceRoot(); Instant start = clock.instant(); int count = 0; - for (Instance instance : curator.readInstances()) { - lockIfPresent(instance.id(), this::store); + for (Application application : curator.readApplications()) { + lockApplicationIfPresent(application.id(), this::store); count++; } log.log(Level.INFO, String.format("Wrote %d applications in %s", count, @@ -164,13 +167,20 @@ public class ApplicationController { } /** Returns the application with the given id, or null if it is not present */ - public Optional<Application> getApplication(ApplicationId id) { + public Optional<Application> getApplication(TenantAndApplicationId id) { return curator.readApplication(id); } + /** Returns the application with the given id, or null if it is not present */ + // TODO jonmv: remove + public Optional<Application> getApplication(ApplicationId id) { + return getApplication(TenantAndApplicationId.from(id)); + } + /** Returns the instance with the given id, or null if it is not present */ - public Optional<Instance> get(ApplicationId id) { - return curator.readInstance(id); + // TODO jonmv: remove or inline + public Optional<Instance> getInstance(ApplicationId id) { + return getApplication(id).flatMap(application -> application.get(id.instance())); } /** @@ -178,7 +188,7 @@ public class ApplicationController { * * @throws IllegalArgumentException if it does not exist */ - public Application requireApplication(ApplicationId id) { + public Application requireApplication(TenantAndApplicationId id) { return getApplication(id).orElseThrow(() -> new IllegalArgumentException(id + " not found")); } @@ -187,48 +197,37 @@ public class ApplicationController { * * @throws IllegalArgumentException if it does not exist */ - public Instance require(ApplicationId id) { - return get(id).orElseThrow(() -> new IllegalArgumentException(id + " not found")); + // TODO jonvm: remove or inline + public Instance requireInstance(ApplicationId id) { + return getInstance(id).orElseThrow(() -> new IllegalArgumentException(id + " not found")); } /** Returns a snapshot of all applications */ - public List<Application> applicationList() { + public List<Application> asList() { return curator.readApplications(); } - /** Returns all applications of a tenant */ - public List<Application> applicationList(TenantName tenant) { + /** Returns a snapshot of all applications of a tenant */ + public List<Application> asList(TenantName tenant) { return curator.readApplications(tenant); } - /** Returns a snapshot of all instances */ - public List<Instance> asList() { - return curator.readInstances(); - } - - /** Returns all instances of a tenant */ - public List<Instance> asList(TenantName tenant) { - return curator.readInstances(tenant); - } - - /** Returns all instances of an application */ - public List<Instance> asList(TenantAndApplicationId id) { - return curator.readInstances(id); - } - public ArtifactRepository artifacts() { return artifactRepository; } public ApplicationStore applicationStore() { return applicationStore; } /** Returns the oldest Vespa version installed on any active or reserved production node for the given application. */ - public Version oldestInstalledPlatform(ApplicationId id) { - return asList(TenantAndApplicationId.from(id)).stream() - .flatMap(instance -> instance.productionDeployments().keySet().stream() - .flatMap(zone -> configServer.nodeRepository().list(zone, id, EnumSet.of(active, reserved)).stream()) - .map(Node::currentVersion) - .filter(version -> ! version.isEmpty())) - .min(naturalOrder()) - .orElse(controller.systemVersion()); + public Version oldestInstalledPlatform(TenantAndApplicationId id) { + return requireApplication(id).instances().values().stream() + .flatMap(instance -> instance.productionDeployments().keySet().stream() + .flatMap(zone -> configServer.nodeRepository().list(zone, + id.instance(instance.name()), + EnumSet.of(active, reserved)) + .stream()) + .map(Node::currentVersion) + .filter(version -> ! version.isEmpty())) + .min(naturalOrder()) + .orElse(controller.systemVersion()); } /** Change the global endpoint status for given deployment */ @@ -267,21 +266,21 @@ public class ApplicationController { * * @throws IllegalArgumentException if the application already exists */ + // TODO jonmv: split in create application and create instance public Application createApplication(ApplicationId id, Optional<Credentials> credentials) { if (id.instance().isTester()) throw new IllegalArgumentException("'" + id + "' is a tester application!"); - try (Lock lock = lock(TenantAndApplicationId.from(id)); - Lock oldLock = lock(id)) { + try (Lock lock = lock(TenantAndApplicationId.from(id))) { // Validate only application names which do not already exist. - if (asList(id.tenant()).stream().noneMatch(application -> application.id().application().equals(id.application()))) + if (getApplication(TenantAndApplicationId.from(id)).isEmpty()) com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId.validate(id.application().value()); Optional<Tenant> tenant = controller.tenants().get(id.tenant()); if (tenant.isEmpty()) throw new IllegalArgumentException("Could not create '" + id + "': This tenant does not exist"); - if (get(id).isPresent()) + if (getInstance(id).isPresent()) throw new IllegalArgumentException("Could not create '" + id + "': Application already exists"); - if (get(dashToUnderscore(id)).isPresent()) // VESPA-1945 + if (getInstance(dashToUnderscore(id)).isPresent()) // VESPA-1945 throw new IllegalArgumentException("Could not create '" + id + "': Application " + dashToUnderscore(id) + " already exists"); if (tenant.get().type() != Tenant.Type.user) { if (credentials.isEmpty()) @@ -290,7 +289,11 @@ public class ApplicationController { if ( ! id.instance().isTester()) // Only store the application permits for non-user applications. accessControl.createApplication(id, credentials.get()); } - LockedApplication application = new LockedApplication(new Application(id, clock.instant()), lock); + List<Instance> instances = getApplication(TenantAndApplicationId.from(id)).map(application -> application.instances().values()) + .map(ArrayList::new) + .orElse(new ArrayList<>()); + instances.add(new Instance(id, clock.instant())); + LockedApplication application = new LockedApplication(Application.aggregate(instances).get(), lock); store(application); log.info("Created " + application); return application.get(); @@ -316,7 +319,7 @@ public class ApplicationController { // TODO jonmv: Change this to create instances on demand. Tenant tenant = controller.tenants().require(applicationId.tenant()); - if (tenant.type() == Tenant.Type.user && get(applicationId).isEmpty()) + if (tenant.type() == Tenant.Type.user && getInstance(applicationId).isEmpty()) createApplication(applicationId, Optional.empty()); try (Lock deploymentLock = lockForDeployment(applicationId, zone)) { @@ -326,9 +329,9 @@ public class ApplicationController { Set<ContainerEndpoint> endpoints; Optional<ApplicationCertificate> applicationCertificate; - try (Lock lock = lock(TenantAndApplicationId.from(applicationId)); - Lock oldLock = lock(applicationId)) { - LockedInstance instance = new LockedInstance(require(applicationId), lock); + try (Lock lock = lock(TenantAndApplicationId.from(applicationId))) { + LockedApplication application = new LockedApplication(requireApplication(TenantAndApplicationId.from(applicationId)), lock); + InstanceName instance = applicationId.instance(); boolean manuallyDeployed = options.deployDirectly || zone.environment().isManuallyDeployed(); boolean preferOldestVersion = options.deployCurrentVersion; @@ -338,14 +341,15 @@ public class ApplicationController { applicationVersion = applicationVersionFromDeployer.orElse(ApplicationVersion.unknown); applicationPackage = applicationPackageFromDeployer.orElseThrow( () -> new IllegalArgumentException("Application package must be given when deploying to " + zone)); - platformVersion = options.vespaVersion.map(Version::new).orElse(applicationPackage.deploymentSpec().majorVersion() - .flatMap(this::lastCompatibleVersion) - .orElseGet(controller::systemVersion)); + platformVersion = options.vespaVersion.map(Version::new) + .orElse(applicationPackage.deploymentSpec().majorVersion() + .flatMap(this::lastCompatibleVersion) + .orElseGet(controller::systemVersion)); } else { JobType jobType = JobType.from(controller.system(), zone) .orElseThrow(() -> new IllegalArgumentException("No job is known for " + zone + ".")); - Optional<JobStatus> job = Optional.ofNullable(instance.get().deploymentJobs().jobStatus().get(jobType)); + Optional<JobStatus> job = Optional.ofNullable(application.get().require(instance).deploymentJobs().jobStatus().get(jobType)); if ( job.isEmpty() || job.get().lastTriggered().isEmpty() || job.get().lastCompleted().isPresent() && job.get().lastCompleted().get().at().isAfter(job.get().lastTriggered().get().at())) @@ -356,34 +360,32 @@ public class ApplicationController { applicationVersion = preferOldestVersion ? triggered.sourceApplication().orElse(triggered.application()) : triggered.application(); - applicationPackage = getApplicationPackage(instance.get(), applicationVersion); + applicationPackage = getApplicationPackage(applicationId, application.get().internal(), applicationVersion); applicationPackage = withTesterCertificate(applicationPackage, applicationId, jobType); - validateRun(instance.get(), zone, platformVersion, applicationVersion); + validateRun(application.get(), instance, zone, platformVersion, applicationVersion); } // TODO jonmv: Remove this when all packages are validated upon submission, as in ApplicationApiHandler.submit(...). verifyApplicationIdentityConfiguration(applicationId.tenant(), applicationPackage, deployingIdentity); - // Assign and register endpoints - instance = withRotation(instance, zone); - endpoints = registerEndpointsInDns(instance.get(), zone); + if (zone.environment().isProduction()) // Assign and register endpoints + application = withRotation(application, instance); + + endpoints = registerEndpointsInDns(application.get().deploymentSpec(), application.get().require(applicationId.instance()), zone); + if (controller.zoneRegistry().zones().directlyRouted().ids().contains(zone)) { - // Get application certificate (provisions a new certificate if missing) - List<? extends ZoneApi> zones = controller.zoneRegistry().zones().all().zones(); - applicationCertificate = getApplicationCertificate(instance.get()); + // Provisions a new certificate if missing + applicationCertificate = getApplicationCertificate(application.get().require(instance)); } else { applicationCertificate = Optional.empty(); } // TODO jonmv: REMOVE! This is now irrelevant for non-CD-test deployments and non-unit tests. if ( ! preferOldestVersion - && ! instance.get().deploymentJobs().deployedInternally() + && ! application.get().internal() && ! zone.environment().isManuallyDeployed()) { - // TODO(jvenstad): Store only on submissions - ApplicationPackage finalApplicationPackage = applicationPackage; - lockApplicationOrThrow(applicationId, lockedApplication -> - storeWithUpdatedConfig(lockedApplication, finalApplicationPackage)); + storeWithUpdatedConfig(application, applicationPackage); } } // Release application lock while doing the deployment, which is a lengthy task. @@ -392,9 +394,10 @@ public class ApplicationController { ActivateResult result = deploy(applicationId, applicationPackage, zone, options, endpoints, applicationCertificate.orElse(null)); - lockOrThrow(applicationId, instance -> - store(instance.withNewDeployment(zone, applicationVersion, platformVersion, clock.instant(), - warningsFrom(result)))); + lockApplicationOrThrow(TenantAndApplicationId.from(applicationId), application -> + store(application.with(applicationId.instance(), + instance -> instance.withNewDeployment(zone, applicationVersion, platformVersion, + clock.instant(), warningsFrom(result))))); return result; } } @@ -413,19 +416,19 @@ public class ApplicationController { } /** Fetches the requested application package from the artifact store(s). */ - public ApplicationPackage getApplicationPackage(Instance instance, ApplicationVersion version) { + public ApplicationPackage getApplicationPackage(ApplicationId id, boolean internal, ApplicationVersion version) { try { - return instance.deploymentJobs().deployedInternally() - ? new ApplicationPackage(applicationStore.get(instance.id(), version)) - : new ApplicationPackage(artifactRepository.getApplicationPackage(instance.id(), version.id())); + return internal + ? new ApplicationPackage(applicationStore.get(id, version)) + : new ApplicationPackage(artifactRepository.getApplicationPackage(id, version.id())); } catch (RuntimeException e) { // If application has switched deployment pipeline, artifacts stored prior to the switch are in the other artifact store. try { - log.info("Fetching application package for " + instance.id() + " from alternate repository; it is now deployed " - + (instance.deploymentJobs().deployedInternally() ? "internally" : "externally") + "\nException was: " + Exceptions.toMessageString(e)); - return instance.deploymentJobs().deployedInternally() - ? new ApplicationPackage(artifactRepository.getApplicationPackage(instance.id(), version.id())) - : new ApplicationPackage(applicationStore.get(instance.id(), version)); + log.info("Fetching application package for " + id + " from alternate repository; it is now deployed " + + (internal ? "internally" : "externally") + "\nException was: " + Exceptions.toMessageString(e)); + return internal + ? new ApplicationPackage(artifactRepository.getApplicationPackage(id, version.id())) + : new ApplicationPackage(applicationStore.get(id, version)); } catch (RuntimeException s) { // If this fails, too, the first failure is most likely the relevant one. e.addSuppressed(s); @@ -441,21 +444,17 @@ public class ApplicationController { application = application.with(applicationPackage.deploymentSpec()); application = application.with(applicationPackage.validationOverrides()); - store(application); - // Delete zones not listed in DeploymentSpec, if allowed // We do this at deployment time for externally built applications, and at submission time // for internally built ones, to be able to return a validation failure message when necessary - for (Instance instance : asList(TenantAndApplicationId.from(application.get().id()))) { - lockOrThrow(instance.id(), locked -> { - locked = withoutDeletedDeployments(locked, applicationPackage); + for (InstanceName instanceName : application.get().instances().keySet()) { + application = withoutDeletedDeployments(application, instanceName); // Clean up deployment jobs that are no longer referenced by deployment spec - locked = withoutUnreferencedDeploymentJobs(locked, applicationPackage); - - store(locked); - }); + DeploymentSpec deploymentSpec = application.get().deploymentSpec(); + application = application.with(instanceName, instance -> withoutUnreferencedDeploymentJobs(deploymentSpec, instance)); } + store(application); } /** Deploy a system application to given zone */ @@ -503,13 +502,13 @@ public class ApplicationController { } /** Makes sure the application has a global rotation, if eligible. */ - private LockedInstance withRotation(LockedInstance application, ZoneId zone) { - if (zone.environment() == Environment.prod) { - try (RotationLock rotationLock = rotationRepository.lock()) { - var rotations = rotationRepository.getOrAssignRotations(application.get(), rotationLock); - application = application.with(rotations); - store(application); // store assigned rotation even if deployment fails - } + private LockedApplication withRotation(LockedApplication application, InstanceName instanceName) { + try (RotationLock rotationLock = rotationRepository.lock()) { + var rotations = rotationRepository.getOrAssignRotations(application.get().deploymentSpec(), + application.get().require(instanceName), + rotationLock); + application = application.with(instanceName, instance -> instance.with(rotations)); + store(application); // store assigned rotation even if deployment fails } return application; } @@ -519,9 +518,9 @@ public class ApplicationController { * * @return the registered endpoints */ - private Set<ContainerEndpoint> registerEndpointsInDns(Instance instance, ZoneId zone) { + private Set<ContainerEndpoint> registerEndpointsInDns(DeploymentSpec deploymentSpec, Instance instance, ZoneId zone) { var containerEndpoints = new HashSet<ContainerEndpoint>(); - var registerLegacyNames = instance.deploymentSpec().globalServiceId().isPresent(); + var registerLegacyNames = deploymentSpec.globalServiceId().isPresent(); for (var assignedRotation : instance.rotations()) { var names = new ArrayList<String>(); var endpoints = instance.endpointsIn(controller.system(), assignedRotation.endpointId()) @@ -610,16 +609,17 @@ public class ApplicationController { return new ActivateResult(new RevisionId("0"), prepareResponse, 0); } - private LockedInstance withoutDeletedDeployments(LockedInstance instance, ApplicationPackage applicationPackage) { - List<Deployment> deploymentsToRemove = instance.get().productionDeployments().values().stream() - .filter(deployment -> ! applicationPackage.deploymentSpec().includes(deployment.zone().environment(), - Optional.of(deployment.zone().region()))) - .collect(Collectors.toList()); + private LockedApplication withoutDeletedDeployments(LockedApplication application, InstanceName instance) { + DeploymentSpec deploymentSpec = application.get().deploymentSpec(); + List<Deployment> deploymentsToRemove = application.get().require(instance).productionDeployments().values().stream() + .filter(deployment -> ! deploymentSpec.includes(deployment.zone().environment(), + Optional.of(deployment.zone().region()))) + .collect(Collectors.toList()); - if (deploymentsToRemove.isEmpty()) return instance; + if (deploymentsToRemove.isEmpty()) return application; - if ( ! applicationPackage.validationOverrides().allows(ValidationId.deploymentRemoval, clock.instant())) - throw new IllegalArgumentException(ValidationId.deploymentRemoval.value() + ": " + instance.get() + + if ( ! application.get().validationOverrides().allows(ValidationId.deploymentRemoval, clock.instant())) + throw new IllegalArgumentException(ValidationId.deploymentRemoval.value() + ": " + application.get().require(instance) + " is deployed in " + deploymentsToRemove.stream() .map(deployment -> deployment.zone().region().value()) @@ -629,16 +629,15 @@ public class ApplicationController { " in deployment.xml. " + ValidationOverrides.toAllowMessage(ValidationId.deploymentRemoval)); - LockedInstance instanceWithRemoval = instance; for (Deployment deployment : deploymentsToRemove) - instanceWithRemoval = deactivate(instanceWithRemoval, deployment.zone()); - return instanceWithRemoval; + application = deactivate(application, instance, deployment.zone()); + return application; } - private LockedInstance withoutUnreferencedDeploymentJobs(LockedInstance instance, ApplicationPackage applicationPackage) { - for (JobType job : JobList.from(instance.get()).production().mapToList(JobStatus::type)) { + private Instance withoutUnreferencedDeploymentJobs(DeploymentSpec deploymentSpec, Instance instance) { + for (JobType job : JobList.from(instance).production().mapToList(JobStatus::type)) { ZoneId zone = job.zone(controller.system()); - if (applicationPackage.deploymentSpec().includes(zone.environment(), Optional.of(zone.region()))) + if (deploymentSpec.includes(zone.environment(), Optional.of(zone.region()))) continue; instance = instance.withoutDeploymentJob(job); } @@ -654,7 +653,7 @@ public class ApplicationController { /** Returns the endpoints of the deployment, or empty if the request fails */ public List<URI> getDeploymentEndpoints(DeploymentId deploymentId) { - if ( ! get(deploymentId.applicationId()) + if ( ! getInstance(deploymentId.applicationId()) .map(application -> application.deployments().containsKey(deploymentId.zoneId())) .orElse(deploymentId.applicationId().instance().isTester())) throw new NotExistsException("Deployment", deploymentId.toString()); @@ -673,7 +672,7 @@ public class ApplicationController { /** Returns the non-empty endpoints per cluster in the given deployment, or empty if endpoints can't be found. */ public Map<ClusterSpec.Id, URI> clusterEndpoints(DeploymentId id) { - if ( ! get(id.applicationId()) + if ( ! getInstance(id.applicationId()) .map(application -> application.deployments().containsKey(id.zoneId())) .orElse(id.applicationId().instance().isTester())) throw new NotExistsException("Deployment", id.toString()); @@ -715,15 +714,16 @@ public class ApplicationController { throw new IllegalArgumentException("Could not delete application '" + tenantName + "." + applicationName + "': No credentials provided"); // Find all instances of the application - List<ApplicationId> instances = asList(tenantName).stream() - .map(Instance::id) - .filter(id -> id.application().equals(applicationName)) - .collect(Collectors.toList()); + TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName); + List<ApplicationId> instances = requireApplication(id).instances().keySet().stream() + .map(id::instance) + .collect(Collectors.toUnmodifiableList()); if (instances.size() > 1) throw new IllegalArgumentException("Could not delete application; more than one instance present: " + instances); // TODO: Make this one transaction when database is moved to ZooKeeper - instances.forEach(id -> deleteInstance(id, credentials)); + for (ApplicationId instance : instances) + deleteInstance(instance, credentials); } /** @@ -737,22 +737,22 @@ public class ApplicationController { if (tenant.type() != Tenant.Type.user && credentials.isEmpty()) throw new IllegalArgumentException("Could not delete application '" + applicationId + "': No credentials provided"); - if (controller.applications().get(applicationId).isEmpty()) { + if (getInstance(applicationId).isEmpty()) throw new NotExistsException("Could not delete application '" + applicationId + "': Application not found"); - } - lockOrThrow(applicationId, application -> { - if ( ! application.get().deployments().isEmpty()) + lockApplicationOrThrow(TenantAndApplicationId.from(applicationId), application -> { + if ( ! application.get().require(applicationId.instance()).deployments().isEmpty()) throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments in: " + - application.get().deployments().keySet().stream().map(ZoneId::toString) + application.get().require(applicationId.instance()).deployments().keySet().stream().map(ZoneId::toString) .sorted().collect(Collectors.joining(", "))); - curator.removeInstance(applicationId); + curator.removeApplication(applicationId); applicationStore.removeAll(applicationId); applicationStore.removeAll(TesterId.of(applicationId)); - application.get().rotations().forEach(assignedRotation -> { - var endpoints = application.get().endpointsIn(controller.system(), assignedRotation.endpointId()); + Instance instance = application.get().require(applicationId.instance()); + instance.rotations().forEach(assignedRotation -> { + var endpoints = instance.endpointsIn(controller.system(), assignedRotation.endpointId()); endpoints.asList().stream() .map(Endpoint::dnsName) .forEach(name -> { @@ -764,10 +764,7 @@ public class ApplicationController { }); - if ( tenant.type() != Tenant.Type.user - && controller.applications().asList(applicationId.tenant()).stream() - .map(application -> application.id().application()) - .noneMatch(applicationId.application()::equals)) + if (tenant.type() != Tenant.Type.user && getApplication(applicationId).isEmpty()) // TODO jonmv: Implementations ignore the instance — refactor to provide tenant and application names only. accessControl.deleteApplication(applicationId, credentials.get()); } @@ -782,41 +779,18 @@ public class ApplicationController { } /** - * Replace any previous version of this application by this instance - * - * @param application a locked application to store - */ - public void store(LockedInstance application) { - curator.writeInstance(application.get()); - } - - /** * Acquire a locked application to modify and store, if there is an application with the given id. * * @param applicationId ID of the application to lock and get. * @param action Function which acts on the locked application. */ - public void lockApplicationIfPresent(ApplicationId applicationId, Consumer<LockedApplication> action) { - try (Lock lock = lock(TenantAndApplicationId.from(applicationId)); - Lock oldLock = lock(applicationId)) { + public void lockApplicationIfPresent(TenantAndApplicationId applicationId, Consumer<LockedApplication> action) { + try (Lock lock = lock(applicationId)) { getApplication(applicationId).map(application -> new LockedApplication(application, lock)).ifPresent(action); } } /** - * Acquire a locked instance to modify and store, if there is an instance with the given id. - * - * @param applicationId ID of the instance to lock and get. - * @param action Function which acts on the locked instance. - */ - public void lockIfPresent(ApplicationId applicationId, Consumer<LockedInstance> action) { - try (Lock lock = lock(TenantAndApplicationId.from(applicationId)); - Lock oldLock = lock(applicationId)) { - get(applicationId).map(application -> new LockedInstance(application, lock)).ifPresent(action); - } - } - - /** * Acquire a locked application to modify and store, or throw an exception if no application has the given id. * * WARNING: Uses only the "default" instance. @@ -827,37 +801,8 @@ public class ApplicationController { */ public void lockApplicationOrThrow(TenantAndApplicationId applicationId, Consumer<LockedApplication> action) { ApplicationId instanceId = applicationId.defaultInstance(); - try (Lock lock = lock(applicationId); - Lock oldLock = lock(instanceId)) { - action.accept(new LockedApplication(requireApplication(instanceId), lock)); - } - } - - /** - * Acquire a locked application to modify and store, or throw an exception if no application has the given id. - * - * @param applicationId ID of the application to lock and require. - * @param action Function which acts on the locked application. - * @throws IllegalArgumentException when application does not exist. - */ - public void lockApplicationOrThrow(ApplicationId applicationId, Consumer<LockedApplication> action) { - try (Lock lock = lock(TenantAndApplicationId.from(applicationId)); - Lock oldLock = lock(applicationId)) { - action.accept(new LockedApplication(requireApplication(applicationId), lock)); - } - } - - /** - * Acquire a locked instance to modify and store, or throw an exception if no instance has the given id. - * - * @param applicationId ID of the instance to lock and require. - * @param action Function which acts on the locked instance. - * @throws IllegalArgumentException when instance does not exist. - */ - public void lockOrThrow(ApplicationId applicationId, Consumer<LockedInstance> action) { - try (Lock lock = lock(TenantAndApplicationId.from(applicationId)); - Lock oldLock = lock(applicationId)) { - action.accept(new LockedInstance(require(applicationId), lock)); + try (Lock lock = lock(applicationId)) { + action.accept(new LockedApplication(requireApplication(TenantAndApplicationId.from(instanceId)), lock)); } } @@ -886,8 +831,9 @@ public class ApplicationController { } /** Deactivate application in the given zone */ - public void deactivate(ApplicationId application, ZoneId zone) { - lockOrThrow(application, lockedInstance -> store(deactivate(lockedInstance, zone))); + public void deactivate(ApplicationId id, ZoneId zone) { + lockApplicationOrThrow(TenantAndApplicationId.from(id), + application -> store(deactivate(application, id.instance(), zone))); } /** @@ -895,15 +841,15 @@ public class ApplicationController { * * @return the application with the deployment in the given zone removed */ - private LockedInstance deactivate(LockedInstance instance, ZoneId zone) { + private LockedApplication deactivate(LockedApplication application, InstanceName instanceName, ZoneId zone) { try { - configServer.deactivate(new DeploymentId(instance.get().id(), zone)); + configServer.deactivate(new DeploymentId(application.get().id().instance(instanceName), zone)); } catch (NotFoundException ignored) { // ok; already gone } finally { - routingPolicies.refresh(instance.get().id(), instance.get().deploymentSpec(), zone); + routingPolicies.refresh(application.get().id().instance(instanceName), application.get().deploymentSpec(), zone); } - return instance.withoutDeploymentIn(zone); + return application.with(instanceName, instance -> instance.withoutDeploymentIn(zone)); } public DeploymentTrigger deploymentTrigger() { return deploymentTrigger; } @@ -924,15 +870,6 @@ public class ApplicationController { } /** - * Returns a lock which provides exclusive rights to changing this application. - * Any operation which stores an application need to first acquire this lock, then read, modify - * and store the application, and finally release (close) the lock. - */ - Lock lock(ApplicationId application) { - return curator.lock(application); - } - - /** * Returns a lock which provides exclusive rights to deploying this application to the given zone. */ private Lock lockForDeployment(ApplicationId application, ZoneId zone) { @@ -940,14 +877,14 @@ public class ApplicationController { } /** Verify that we don't downgrade an existing production deployment. */ - private void validateRun(Instance instance, ZoneId zone, Version platformVersion, ApplicationVersion applicationVersion) { - Deployment deployment = instance.deployments().get(zone); + private void validateRun(Application application, InstanceName instance, ZoneId zone, Version platformVersion, ApplicationVersion applicationVersion) { + Deployment deployment = application.require(instance).deployments().get(zone); if ( zone.environment().isProduction() && deployment != null - && ( platformVersion.compareTo(deployment.version()) < 0 && ! instance.change().isPinned() + && ( platformVersion.compareTo(deployment.version()) < 0 && ! application.change().isPinned() || applicationVersion.compareTo(deployment.applicationVersion()) < 0)) - throw new IllegalArgumentException(String.format("Rejecting deployment of %s to %s, as the requested versions (platform: %s, application: %s)" + + throw new IllegalArgumentException(String.format("Rejecting deployment of application %s to %s, as the requested versions (platform: %s, application: %s)" + " are older than the currently deployed (platform: %s, application: %s).", - instance, zone, platformVersion, applicationVersion, deployment.version(), deployment.applicationVersion())); + application.id().instance(instance), zone, platformVersion, applicationVersion, deployment.version(), deployment.applicationVersion())); } /** Returns the rotation repository, used for managing global rotation assignments */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java index 60d74e3719d..70815beea8b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java @@ -6,25 +6,33 @@ import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.vespa.hosted.controller.application.ApplicationActivity; import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Change; +import com.yahoo.vespa.hosted.controller.application.ClusterInfo; +import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; +import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.EndpointList; +import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.rotation.RotationStatus; import java.time.Instant; import java.util.Collections; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -68,6 +76,18 @@ public class Instance { Optional.empty(), Collections.emptyList(), RotationStatus.EMPTY); } + /** Creates an empty instance*/ + public Instance(ApplicationId id, List<Deployment> deployments, DeploymentJobs deploymentJobs, + List<AssignedRotation> rotations, RotationStatus rotationStatus) { + this(id, + Instant.EPOCH, DeploymentSpec.empty, ValidationOverrides.empty, + deployments.stream().collect(Collectors.toMap(Deployment::zone, Function.identity())), + deploymentJobs, + Change.empty(), Change.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), + new ApplicationMetrics(0, 0), Optional.empty(), + rotations, rotationStatus); + } + /** Used from persistence layer: Do not use */ public Instance(ApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, List<Deployment> deployments, DeploymentJobs deploymentJobs, Change change, @@ -102,8 +122,119 @@ public class Instance { this.rotationStatus = Objects.requireNonNull(rotationStatus, "rotationStatus cannot be null"); } + public Instance withJobPause(JobType jobType, OptionalLong pausedUntil) { + return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs.withPause(jobType, pausedUntil), change, outstandingChange, + ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + rotations, rotationStatus); + } + + public Instance withJobCompletion(JobType jobType, JobStatus.JobRun completion, + Optional<DeploymentJobs.JobError> jobError) { + return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs.withCompletion(jobType, completion, jobError), + change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, + pemDeployKey, rotations, rotationStatus); + } + + public Instance withJobTriggering(JobType jobType, JobStatus.JobRun job) { + return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs.withTriggering(jobType, job), change, outstandingChange, + ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + rotations, rotationStatus); + } + + public Instance withNewDeployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, + Instant instant, Map<DeploymentMetrics.Warning, Integer> warnings) { + // Use info from previous deployment if available, otherwise create a new one. + Deployment previousDeployment = deployments.getOrDefault(zone, new Deployment(zone, applicationVersion, + version, instant)); + Deployment newDeployment = new Deployment(zone, applicationVersion, version, instant, + previousDeployment.clusterUtils(), + previousDeployment.clusterInfo(), + previousDeployment.metrics().with(warnings), + previousDeployment.activity()); + return with(newDeployment); + } + + public Instance withClusterUtilization(ZoneId zone, Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization) { + Deployment deployment = deployments.get(zone); + if (deployment == null) return this; // No longer deployed in this zone. + return with(deployment.withClusterUtils(clusterUtilization)); + } + + public Instance withClusterInfo(ZoneId zone, Map<ClusterSpec.Id, ClusterInfo> clusterInfo) { + Deployment deployment = deployments.get(zone); + if (deployment == null) return this; // No longer deployed in this zone. + return with(deployment.withClusterInfo(clusterInfo)); + + } + + public Instance recordActivityAt(Instant instant, ZoneId zone) { + Deployment deployment = deployments.get(zone); + if (deployment == null) return this; + return with(deployment.recordActivityAt(instant)); + } + + public Instance with(ZoneId zone, DeploymentMetrics deploymentMetrics) { + Deployment deployment = deployments.get(zone); + if (deployment == null) return this; // No longer deployed in this zone. + return with(deployment.withMetrics(deploymentMetrics)); + } + + public Instance withoutDeploymentIn(ZoneId zone) { + Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(this.deployments); + deployments.remove(zone); + return with(deployments); + } + + public Instance withoutDeploymentJob(JobType jobType) { + return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs.without(jobType), change, outstandingChange, + ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + rotations, rotationStatus); + } + + public Instance with(ApplicationMetrics metrics) { + return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, + metrics, pemDeployKey, rotations, rotationStatus); + } + + public Instance withPemDeployKey(String pemDeployKey) { + return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, + metrics, Optional.ofNullable(pemDeployKey), rotations, rotationStatus); + } + + public Instance with(List<AssignedRotation> assignedRotations) { + return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, + metrics, pemDeployKey, assignedRotations, rotationStatus); + } + + public Instance with(RotationStatus rotationStatus) { + return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, + metrics, pemDeployKey, rotations, rotationStatus); + } + + private Instance with(Deployment deployment) { + Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(this.deployments); + deployments.put(deployment.zone(), deployment); + return with(deployments); + } + + private Instance with(Map<ZoneId, Deployment> deployments) { + return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, + metrics, pemDeployKey, rotations, rotationStatus); + } + public ApplicationId id() { return id; } + public InstanceName name() { return id.instance(); } + public Instant createdAt() { return createdAt; } /** 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 cd5552ed62e..9aba8921860 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 @@ -6,7 +6,9 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.searchlib.rankingexpression.rule.Function; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; @@ -20,10 +22,12 @@ import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.JobStatus; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.rotation.RotationStatus; import java.time.Instant; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -31,6 +35,8 @@ import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.OptionalLong; +import java.util.function.BinaryOperator; +import java.util.function.UnaryOperator; /** * An application that has been locked for modification. Provides methods for modifying an application's fields. @@ -40,21 +46,21 @@ import java.util.OptionalLong; public class LockedApplication { private final Lock lock; - private final ApplicationId id; + private final TenantAndApplicationId id; private final Instant createdAt; private final DeploymentSpec deploymentSpec; private final ValidationOverrides validationOverrides; - private final Map<ZoneId, Deployment> deployments; - private final DeploymentJobs deploymentJobs; private final Change change; private final Change outstandingChange; + private final Optional<IssueId> deploymentIssueId; private final Optional<IssueId> ownershipIssueId; private final Optional<User> owner; private final OptionalInt majorVersion; private final ApplicationMetrics metrics; private final Optional<String> pemDeployKey; - private final List<AssignedRotation> rotations; - private final RotationStatus rotationStatus; + private final OptionalLong projectId; + private final boolean internal; + private final Map<InstanceName, Instance> instances; /** * Used to create a locked application @@ -64,129 +70,130 @@ public class LockedApplication { */ LockedApplication(Application application, Lock lock) { this(Objects.requireNonNull(lock, "lock cannot be null"), application.id(), application.createdAt(), - application.deploymentSpec(), application.validationOverrides(), - application.deployments(), - application.deploymentJobs(), application.change(), application.outstandingChange(), - application.ownershipIssueId(), application.owner(), application.majorVersion(), application.metrics(), - application.pemDeployKey(), application.rotations(), application.rotationStatus()); + application.deploymentSpec(), application.validationOverrides(), application.change(), + application.outstandingChange(), application.deploymentIssueId(), application.ownershipIssueId(), + application.owner(), application.majorVersion(), application.metrics(), application.pemDeployKey(), + application.projectId(), application.internal(), application.instances()); } - private LockedApplication(Lock lock, ApplicationId id, Instant createdAt, - DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, - Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change, - Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, + private LockedApplication(Lock lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, + ValidationOverrides validationOverrides, Change change, Change outstandingChange, + Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey, - List<AssignedRotation> rotations, RotationStatus rotationStatus) { + OptionalLong projectId, boolean internal, Map<InstanceName, Instance> instances) { this.lock = lock; this.id = id; this.createdAt = createdAt; this.deploymentSpec = deploymentSpec; this.validationOverrides = validationOverrides; - this.deployments = deployments; - this.deploymentJobs = deploymentJobs; this.change = change; this.outstandingChange = outstandingChange; + this.deploymentIssueId = deploymentIssueId; this.ownershipIssueId = ownershipIssueId; this.owner = owner; this.majorVersion = majorVersion; this.metrics = metrics; this.pemDeployKey = pemDeployKey; - this.rotations = rotations; - this.rotationStatus = rotationStatus; + this.projectId = projectId; + this.internal = internal; + this.instances = Map.copyOf(instances); } /** Returns a read-only copy of this */ public Application get() { - return new Application(id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, - outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); + return new Application(id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances.values()); + } + + public LockedApplication with(InstanceName instance, UnaryOperator<Instance> modification) { + var instances = new HashMap<>(this.instances); + instances.put(instance, modification.apply(instances.get(instance))); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); + } + + private LockedApplication without(InstanceName instance) { + var instances = new HashMap<>(this.instances); + instances.remove(instance); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } public LockedApplication withBuiltInternally(boolean builtInternally) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs.withBuiltInternally(builtInternally), change, outstandingChange, - ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, builtInternally, instances); } public LockedApplication withProjectId(OptionalLong projectId) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs.withProjectId(projectId), change, outstandingChange, - ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } public LockedApplication withDeploymentIssueId(IssueId issueId) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs.with(issueId), change, outstandingChange, - ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); - } - - public LockedApplication withJobCompletion(long projectId, JobType jobType) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs.withProjectId(jobType == JobType.component ? OptionalLong.of(projectId) - : deploymentJobs.projectId()), - change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, - pemDeployKey, rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + Optional.ofNullable(issueId), ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } public LockedApplication with(DeploymentSpec deploymentSpec) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, - ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } public LockedApplication with(ValidationOverrides validationOverrides) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } public LockedApplication withChange(Change change) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } public LockedApplication withOutstandingChange(Change outstandingChange) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } public LockedApplication withOwnershipIssueId(IssueId issueId) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, Optional.ofNullable(issueId), owner, - majorVersion, metrics, pemDeployKey, rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, Optional.of(issueId), owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } public LockedApplication withOwner(User owner) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, - Optional.ofNullable(owner), majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, Optional.of(owner), majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } /** Set a major version for this, or set to null to remove any major version override */ public LockedApplication withMajorVersion(Integer majorVersion) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion == null ? OptionalInt.empty() : OptionalInt.of(majorVersion), - metrics, pemDeployKey, rotations, rotationStatus); + metrics, pemDeployKey, projectId, internal, instances); } public LockedApplication with(ApplicationMetrics metrics) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, + projectId, internal, instances); } public LockedApplication withPemDeployKey(String pemDeployKey) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, Optional.ofNullable(pemDeployKey), rotations, rotationStatus); + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, Optional.ofNullable(pemDeployKey), + projectId, internal, instances); } @Override diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedInstance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedInstance.java deleted file mode 100644 index f1f11be4f4d..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedInstance.java +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2018 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.yahoo.component.Version; -import com.yahoo.config.application.api.DeploymentSpec; -import com.yahoo.config.application.api.ValidationOverrides; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.ClusterSpec; -import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.vespa.curator.Lock; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; -import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; -import com.yahoo.vespa.hosted.controller.api.integration.organization.User; -import com.yahoo.vespa.hosted.controller.application.AssignedRotation; -import com.yahoo.vespa.hosted.controller.application.Change; -import com.yahoo.vespa.hosted.controller.application.ClusterInfo; -import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; -import com.yahoo.vespa.hosted.controller.application.Deployment; -import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; -import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; -import com.yahoo.vespa.hosted.controller.application.JobStatus; -import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; -import com.yahoo.vespa.hosted.controller.rotation.RotationStatus; - -import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.OptionalLong; - -/** - * An application that has been locked for modification. Provides methods for modifying an application's fields. - * - * @author mpolden - * @author jonmv - */ -public class LockedInstance { - - private final Lock lock; - private final ApplicationId id; - private final Instant createdAt; - private final DeploymentSpec deploymentSpec; - private final ValidationOverrides validationOverrides; - private final Map<ZoneId, Deployment> deployments; - private final DeploymentJobs deploymentJobs; - private final Change change; - private final Change outstandingChange; - private final Optional<IssueId> ownershipIssueId; - private final Optional<User> owner; - private final OptionalInt majorVersion; - private final ApplicationMetrics metrics; - private final Optional<String> pemDeployKey; - private final List<AssignedRotation> rotations; - private final RotationStatus rotationStatus; - - /** - * Used to create a locked application - * - * @param instance The application to lock. - * @param lock The lock for the application. - */ - LockedInstance(Instance instance, Lock lock) { - this(Objects.requireNonNull(lock, "lock cannot be null"), instance.id(), instance.createdAt(), - instance.deploymentSpec(), instance.validationOverrides(), - instance.deployments(), - instance.deploymentJobs(), instance.change(), instance.outstandingChange(), - instance.ownershipIssueId(), instance.owner(), instance.majorVersion(), instance.metrics(), - instance.pemDeployKey(), instance.rotations(), instance.rotationStatus()); - } - - private LockedInstance(Lock lock, ApplicationId id, Instant createdAt, - DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, - Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change, - Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, - OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey, - List<AssignedRotation> rotations, RotationStatus rotationStatus) { - this.lock = lock; - this.id = id; - this.createdAt = createdAt; - this.deploymentSpec = deploymentSpec; - this.validationOverrides = validationOverrides; - this.deployments = deployments; - this.deploymentJobs = deploymentJobs; - this.change = change; - this.outstandingChange = outstandingChange; - this.ownershipIssueId = ownershipIssueId; - this.owner = owner; - this.majorVersion = majorVersion; - this.metrics = metrics; - this.pemDeployKey = pemDeployKey; - this.rotations = rotations; - this.rotationStatus = rotationStatus; - } - - /** Returns a read-only copy of this */ - public Instance get() { - return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, - outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); - } - - public LockedInstance withJobPause(JobType jobType, OptionalLong pausedUntil) { - return new LockedInstance(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs.withPause(jobType, pausedUntil), change, outstandingChange, - ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); - } - - public LockedInstance withJobCompletion(JobType jobType, JobStatus.JobRun completion, - Optional<DeploymentJobs.JobError> jobError) { - return new LockedInstance(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs.withCompletion(jobType, completion, jobError), - change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, - pemDeployKey, rotations, rotationStatus); - } - - public LockedInstance withJobTriggering(JobType jobType, JobStatus.JobRun job) { - return new LockedInstance(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs.withTriggering(jobType, job), change, outstandingChange, - ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); - } - - public LockedInstance withNewDeployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, - Instant instant, Map<DeploymentMetrics.Warning, Integer> warnings) { - // Use info from previous deployment if available, otherwise create a new one. - Deployment previousDeployment = deployments.getOrDefault(zone, new Deployment(zone, applicationVersion, - version, instant)); - Deployment newDeployment = new Deployment(zone, applicationVersion, version, instant, - previousDeployment.clusterUtils(), - previousDeployment.clusterInfo(), - previousDeployment.metrics().with(warnings), - previousDeployment.activity()); - return with(newDeployment); - } - - public LockedInstance withClusterUtilization(ZoneId zone, Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization) { - Deployment deployment = deployments.get(zone); - if (deployment == null) return this; // No longer deployed in this zone. - return with(deployment.withClusterUtils(clusterUtilization)); - } - - public LockedInstance withClusterInfo(ZoneId zone, Map<ClusterSpec.Id, ClusterInfo> clusterInfo) { - Deployment deployment = deployments.get(zone); - if (deployment == null) return this; // No longer deployed in this zone. - return with(deployment.withClusterInfo(clusterInfo)); - - } - - public LockedInstance recordActivityAt(Instant instant, ZoneId zone) { - Deployment deployment = deployments.get(zone); - if (deployment == null) return this; - return with(deployment.recordActivityAt(instant)); - } - - public LockedInstance with(ZoneId zone, DeploymentMetrics deploymentMetrics) { - Deployment deployment = deployments.get(zone); - if (deployment == null) return this; // No longer deployed in this zone. - return with(deployment.withMetrics(deploymentMetrics)); - } - - public LockedInstance withoutDeploymentIn(ZoneId zone) { - Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(this.deployments); - deployments.remove(zone); - return with(deployments); - } - - public LockedInstance withoutDeploymentJob(JobType jobType) { - return new LockedInstance(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs.without(jobType), change, outstandingChange, - ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - rotations, rotationStatus); - } - - public LockedInstance with(ApplicationMetrics metrics) { - return new LockedInstance(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, rotations, rotationStatus); - } - - public LockedInstance withPemDeployKey(String pemDeployKey) { - return new LockedInstance(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, Optional.ofNullable(pemDeployKey), rotations, rotationStatus); - } - - public LockedInstance with(List<AssignedRotation> assignedRotations) { - return new LockedInstance(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, assignedRotations, rotationStatus); - } - - public LockedInstance with(RotationStatus rotationStatus) { - return new LockedInstance(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, rotations, rotationStatus); - } - - /** Don't expose non-leaf sub-objects. */ - private LockedInstance with(Deployment deployment) { - Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(this.deployments); - deployments.put(deployment.zone(), deployment); - return with(deployments); - } - - private LockedInstance with(Map<ZoneId, Deployment> deployments) { - return new LockedInstance(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, - deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, rotations, rotationStatus); - } - - @Override - public String toString() { - return "application '" + id + "'"; - } - -} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java index 22274975104..6a15e49134a 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java @@ -7,6 +7,7 @@ import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy; import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.ApplicationController; +import com.yahoo.vespa.hosted.controller.Instance; import java.time.Instant; import java.util.Collection; @@ -14,7 +15,8 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.stream.Stream; +import java.util.function.Predicate; +import java.util.stream.Collectors; /** * A list of applications which can be filtered in various ways. @@ -23,20 +25,24 @@ import java.util.stream.Stream; */ public class ApplicationList { - private final ImmutableList<Application> list; + private final List<Application> list; - private ApplicationList(Iterable<Application> applications) { - this.list = ImmutableList.copyOf(applications); + private ApplicationList(List<Application> applications) { + this.list = applications; } // ----------------------------------- Factories - public static ApplicationList from(Iterable<Application> applications) { - return new ApplicationList(applications); + public static ApplicationList from(Collection<Application> applications) { + return new ApplicationList(List.copyOf(applications)); } public static ApplicationList from(Collection<ApplicationId> ids, ApplicationController applications) { - return listOf(ids.stream().map(applications::requireApplication)); + return new ApplicationList(ids.stream() + .map(TenantAndApplicationId::from) + .distinct() + .map(applications::requireApplication) + .collect(Collectors.toUnmodifiableList())); } // ----------------------------------- Accessors @@ -45,7 +51,7 @@ public class ApplicationList { public List<Application> asList() { return list; } /** Returns the ids of the applications in this as an immutable list */ - public List<ApplicationId> idList() { return ImmutableList.copyOf(list.stream().map(Application::id)::iterator); } + public List<TenantAndApplicationId> idList() { return list.stream().map(Application::id).collect(Collectors.toUnmodifiableList()); } public boolean isEmpty() { return list.isEmpty(); } @@ -55,17 +61,17 @@ public class ApplicationList { /** Returns the subset of applications which are upgrading (to any version), not considering block windows. */ public ApplicationList upgrading() { - return listOf(list.stream().filter(application -> application.change().platform().isPresent())); + return filteredOn(application -> application.change().platform().isPresent()); } /** Returns the subset of applications which are currently upgrading to the given version */ public ApplicationList upgradingTo(Version version) { - return listOf(list.stream().filter(application -> isUpgradingTo(version, application))); + return filteredOn(application -> isUpgradingTo(version, application)); } /** Returns the subset of applications which are not pinned to a certain Vespa version. */ public ApplicationList unpinned() { - return listOf(list.stream().filter(application -> ! application.change().isPinned())); + return filteredOn(application -> ! application.change().isPinned()); } /** Returns the subset of applications which are currently not upgrading to the given version */ @@ -75,7 +81,7 @@ public class ApplicationList { /** Returns the subset of applications which are currently not upgrading to any of the given versions */ public ApplicationList notUpgradingTo(Collection<Version> versions) { - return listOf(list.stream().filter(application -> versions.stream().noneMatch(version -> isUpgradingTo(version, application)))); + return filteredOn(application -> versions.stream().noneMatch(version -> isUpgradingTo(version, application))); } /** @@ -89,79 +95,87 @@ public class ApplicationList { /** Returns the subset of applications which have changes left to deploy; blocked, or deploying */ public ApplicationList withChanges() { - return listOf(list.stream().filter(application -> application.change().hasTargets() || application.outstandingChange().hasTargets())); + return filteredOn(application -> application.change().hasTargets() || application.outstandingChange().hasTargets()); } /** Returns the subset of applications which are currently not deploying a change */ public ApplicationList notDeploying() { - return listOf(list.stream().filter(application -> ! application.change().hasTargets())); + return filteredOn(application -> ! application.change().hasTargets()); } /** Returns the subset of applications which currently does not have any failing jobs */ public ApplicationList notFailing() { - return listOf(list.stream().filter(application -> ! application.deploymentJobs().hasFailures())); + return filteredOn(application -> application.instances().values().stream() + .noneMatch(instance -> instance.deploymentJobs().hasFailures())); } /** Returns the subset of applications which currently have failing jobs */ public ApplicationList failing() { - return listOf(list.stream().filter(application -> application.deploymentJobs().hasFailures())); + return filteredOn(application -> application.instances().values().stream() + .anyMatch(instance -> instance.deploymentJobs().hasFailures())); } /** Returns the subset of applications which have been failing an upgrade to the given version since the given instant */ public ApplicationList failingUpgradeToVersionSince(Version version, Instant threshold) { - return listOf(list.stream().filter(application -> failingUpgradeToVersionSince(application, version, threshold))); + return filteredOn(application -> application.instances().values().stream() + .anyMatch(instance -> failingUpgradeToVersionSince(instance, version, threshold))); } /** Returns the subset of applications which have been failing an application change since the given instant */ public ApplicationList failingApplicationChangeSince(Instant threshold) { - return listOf(list.stream().filter(application -> failingApplicationChangeSince(application, threshold))); + return filteredOn(application -> application.instances().values().stream() + .anyMatch(instance -> failingApplicationChangeSince(instance, threshold))); } /** Returns the subset of applications which currently does not have any failing jobs on the given version */ public ApplicationList notFailingOn(Version version) { - return listOf(list.stream().filter(application -> ! failingOn(version, application))); + return filteredOn(application -> application.instances().values().stream() + .noneMatch(instance -> failingOn(version, instance))); } /** Returns the subset of applications which have at least one production deployment */ - public ApplicationList hasDeployment() { - return listOf(list.stream().filter(a -> !a.productionDeployments().isEmpty())); + public ApplicationList withProductionDeployment() { + return filteredOn(application -> application.instances().values().stream() + .anyMatch(instance -> instance.productionDeployments().size() > 0)); } /** Returns the subset of applications which started failing on the given version */ public ApplicationList startedFailingOn(Version version) { - return listOf(list.stream().filter(application -> ! JobList.from(application).firstFailing().on(version).isEmpty())); + return filteredOn(application -> application.instances().values().stream() + .anyMatch(instance -> ! JobList.from(instance).firstFailing().on(version).isEmpty())); } /** Returns the subset of applications which has the given upgrade policy */ public ApplicationList with(UpgradePolicy policy) { - return listOf(list.stream().filter(a -> a.deploymentSpec().upgradePolicy() == policy)); + return filteredOn(application -> application.deploymentSpec().upgradePolicy() == policy); } /** Returns the subset of applications which does not have the given upgrade policy */ public ApplicationList without(UpgradePolicy policy) { - return listOf(list.stream().filter(a -> a.deploymentSpec().upgradePolicy() != policy)); + return filteredOn(application -> application.deploymentSpec().upgradePolicy() != policy); } /** Returns the subset of applications which have at least one deployment on a lower version than the given one */ public ApplicationList onLowerVersionThan(Version version) { - return listOf(list.stream() - .filter(a -> a.productionDeployments().values().stream() - .anyMatch(d -> d.version().isBefore(version)))); + return filteredOn(application -> application.instances().values().stream() + .flatMap(instance -> instance.productionDeployments().values().stream()) + .anyMatch(deployment -> deployment.version().isBefore(version))); } /** Returns the subset of applications which have a project ID */ public ApplicationList withProjectId() { - return listOf(list.stream().filter(a -> a.deploymentJobs().projectId().isPresent())); - } - - /** Returns the subset of applications which have at least one production deployment */ - public ApplicationList hasProductionDeployment() { - return listOf(list.stream().filter(a -> ! a.productionDeployments().isEmpty())); + return filteredOn(application -> application.projectId().isPresent()); } /** Returns the subset of applications that are allowed to upgrade at the given time */ public ApplicationList canUpgradeAt(Instant instant) { - return listOf(list.stream().filter(a -> a.deploymentSpec().canUpgradeAt(instant))); + return filteredOn(application -> application.deploymentSpec().canUpgradeAt(instant)); + } + + /** Returns the subset of applications that have at least one assigned rotation */ + public ApplicationList hasRotation() { + return filteredOn(application -> application.instances().values().stream() + .anyMatch(instance -> ! instance.rotations().isEmpty())); } /** @@ -171,8 +185,9 @@ public class ApplicationList { * @param defaultMajorVersion the default major version to assume for applications not specifying one */ public ApplicationList allowMajorVersion(int targetMajorVersion, int defaultMajorVersion) { - return listOf(list.stream().filter(a -> a.deploymentSpec().majorVersion().orElse(a.majorVersion().orElse(defaultMajorVersion)) - >= targetMajorVersion)); + return filteredOn(application -> targetMajorVersion <= application.deploymentSpec().majorVersion() + .orElse(application.majorVersion() + .orElse(defaultMajorVersion))); } /** Returns the first n application in this (or all, if there are less than n). */ @@ -189,7 +204,10 @@ public class ApplicationList { * Applications without any deployments are ordered first. */ public ApplicationList byIncreasingDeployedVersion() { - return listOf(list.stream().sorted(Comparator.comparing(application -> application.oldestDeployedPlatform().orElse(Version.emptyVersion)))); + return new ApplicationList(list.stream() + .sorted(Comparator.comparing(application -> application.oldestDeployedPlatform() + .orElse(Version.emptyVersion))) + .collect(Collectors.toUnmodifiableList())); } // ----------------------------------- Internal helpers @@ -198,31 +216,30 @@ public class ApplicationList { return application.change().platform().equals(Optional.of(version)); } - private static boolean failingOn(Version version, Application application) { - return ! JobList.from(application) - .failing() - .lastCompleted().on(version) - .isEmpty(); + private static boolean failingOn(Version version, Instance instance) { + return ! JobList.from(instance) + .failing() + .lastCompleted().on(version) + .isEmpty(); } - private static boolean failingUpgradeToVersionSince(Application application, Version version, Instant threshold) { - return ! JobList.from(application) - .not().failingApplicationChange() - .firstFailing().before(threshold) - .lastCompleted().on(version) - .isEmpty(); + private static boolean failingUpgradeToVersionSince(Instance instance, Version version, Instant threshold) { + return ! JobList.from(instance) + .not().failingApplicationChange() + .firstFailing().before(threshold) + .lastCompleted().on(version) + .isEmpty(); } - private static boolean failingApplicationChangeSince(Application application, Instant threshold) { - return ! JobList.from(application) - .failingApplicationChange() - .firstFailing().before(threshold) - .isEmpty(); + private static boolean failingApplicationChangeSince(Instance instance, Instant threshold) { + return ! JobList.from(instance) + .failingApplicationChange() + .firstFailing().before(threshold) + .isEmpty(); } - /** Convenience converter from a stream to an ApplicationList */ - private static ApplicationList listOf(Stream<Application> applications) { - return from(applications::iterator); + private ApplicationList filteredOn(Predicate<Application> condition) { + return new ApplicationList(list.stream().filter(condition).collect(Collectors.toUnmodifiableList())); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java index 809e0a8f0fb..ea020010e3d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java @@ -37,7 +37,7 @@ public class InstanceList { } public static InstanceList from(Collection<ApplicationId> ids, ApplicationController applications) { - return listOf(ids.stream().map(applications::require)); + return listOf(ids.stream().map(applications::requireInstance)); } // ----------------------------------- Accessors diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java index 497edf18dfc..5c2df9f039b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java @@ -40,10 +40,6 @@ public class JobList { return new JobList(jobs); } - public static JobList from(Application application) { - return from(application.deploymentJobs().jobStatus().values()); - } - public static JobList from(Instance instance) { return from(instance.deploymentJobs().jobStatus().values()); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java index 081ce02e52d..0b537535315 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java @@ -45,7 +45,11 @@ public class TenantAndApplicationId implements Comparable<TenantAndApplicationId } public ApplicationId defaultInstance() { - return ApplicationId.from(tenant, application, InstanceName.defaultName()); + return instance(InstanceName.defaultName()); + } + + public ApplicationId instance(InstanceName instance) { + return ApplicationId.from(tenant, application, instance); } public String serialized() { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java index c9234b87a8b..91f9e2d56d7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java @@ -16,7 +16,7 @@ import com.yahoo.vespa.athenz.api.OktaAccessToken; import com.yahoo.vespa.athenz.client.zms.RoleAction; import com.yahoo.vespa.athenz.client.zms.ZmsClient; import com.yahoo.vespa.athenz.client.zts.ZtsClient; -import com.yahoo.vespa.hosted.controller.Instance; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory; import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction; import com.yahoo.vespa.hosted.controller.security.AccessControl; @@ -91,7 +91,7 @@ public class AthenzFacade implements AccessControl { } @Override - public Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Instance> instances) { + public Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Application> applications) { AthenzTenantSpec spec = (AthenzTenantSpec) tenantSpec; AthenzCredentials athenzCredentials = (AthenzCredentials) credentials; AthenzDomain newDomain = spec.domain(); @@ -121,12 +121,12 @@ public class AthenzFacade implements AccessControl { else { // Delete and recreate tenant, and optionally application, resources in Athenz otherwise. log("createTenancy(tenantDomain=%s, service=%s)", newDomain, service); zmsClient.createTenancy(newDomain, service, athenzCredentials.token()); - for (Instance instance : instances) - createApplication(newDomain, instance.id().application(), athenzCredentials.token()); + for (Application application : applications) + createApplication(newDomain, application.id().application(), athenzCredentials.token()); log("deleteTenancy(tenantDomain=%s, service=%s)", oldDomain, service); - for (Instance instance : instances) - deleteApplication(oldDomain, instance.id().application(), athenzCredentials.token()); + for (Application application : applications) + deleteApplication(oldDomain, application.id().application(), athenzCredentials.token()); zmsClient.deleteTenancy(oldDomain, service, athenzCredentials.token()); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java index 92b5a5c54cb..1bba1baa91b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java @@ -15,7 +15,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.vespa.hosted.controller.application.InstanceList; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobReport; @@ -39,9 +39,11 @@ import java.util.OptionalLong; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.logging.Logger; +import java.util.stream.Collectors; import java.util.stream.Stream; import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.BuildJob; +import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.disabled; import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.idle; import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.queued; import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.running; @@ -59,7 +61,7 @@ import static java.util.stream.Collectors.toList; /** * Responsible for scheduling deployment jobs in a build system and keeping - * {@link Instance#change()} in sync with what is scheduled. + * {@link Application#change()} in sync with what is scheduled. * * This class is multi-thread safe. * @@ -100,23 +102,25 @@ public class DeploymentTrigger { report.jobType(), report.applicationId(), report.projectId())); - if (applications().get(report.applicationId()).isEmpty()) { + if (applications().getInstance(report.applicationId()).isEmpty()) { log.log(LogLevel.WARNING, "Ignoring completion of job of project '" + report.projectId() + "': Unknown application '" + report.applicationId() + "'"); return; } - applications().lockApplicationOrThrow(report.applicationId(), application -> { + applications().lockApplicationOrThrow(TenantAndApplicationId.from(report.applicationId()), application -> { JobRun triggering; + // TODO jonmv: Remove this, and replace with a simple application version counter. if (report.jobType() == component) { ApplicationVersion applicationVersion = report.version().get(); - triggering = JobRun.triggering(applications().oldestInstalledPlatform(report.applicationId()), applicationVersion, + triggering = JobRun.triggering(applications().oldestInstalledPlatform(TenantAndApplicationId.from(report.applicationId())), + applicationVersion, Optional.empty(), Optional.empty(), "Application commit", clock.instant()); if (report.success()) { if (acceptNewApplicationVersion(application.get())) { application = application.withChange(application.get().change().with(applicationVersion)) .withOutstandingChange(Change.empty()); - if (application.get().deploymentJobs().deployedInternally()) + if (application.get().internal()) for (Run run : jobs.active()) if (run.id().application().equals(report.applicationId())) jobs.abort(run.id()); @@ -124,9 +128,11 @@ public class DeploymentTrigger { else application = application.withOutstandingChange(Change.of(applicationVersion)); } + application = application.withProjectId(OptionalLong.of(report.projectId())); } else { - Optional<JobStatus> status = application.get().deploymentJobs().statusOf(report.jobType()); + Optional<JobStatus> status = application.get().require(report.applicationId().instance()) + .deploymentJobs().statusOf(report.jobType()); triggering = status.filter(job -> job.lastTriggered().isPresent() && job.lastCompleted() .map(completion -> ! completion.at().isAfter(job.lastTriggered().get().at())) @@ -137,16 +143,13 @@ public class DeploymentTrigger { .orElse("never"))) .lastTriggered().get(); } - application = application.withJobCompletion(report.projectId(), report.jobType()); - applications().store(application); - applications().lockOrThrow(report.applicationId(), instance -> - applications().store(instance.withJobCompletion(report.jobType(), - triggering.completion(report.buildNumber(), clock.instant()), - report.jobError()))); + application = application.with(report.applicationId().instance(), + instance -> instance.withJobCompletion(report.jobType(), + triggering.completion(report.buildNumber(), clock.instant()), + report.jobError())); + applications().store(application.withChange(remainingChange(application.get()))); }); - applications().lockApplicationOrThrow(report.applicationId(), application -> - applications().store(application.withChange(remainingChange(application.get())))); } /** Returns a map of jobs that are scheduled to be run, grouped by the job type */ @@ -190,8 +193,8 @@ public class DeploymentTrigger { public boolean trigger(Job job) { log.log(LogLevel.DEBUG, String.format("Triggering %s: %s", job, job.triggering)); try { - applications().lockOrThrow(job.applicationId(), application -> { - if (application.get().deploymentJobs().deployedInternally()) + applications().lockApplicationOrThrow(TenantAndApplicationId.from(job.applicationId()), application -> { + if (application.get().internal()) jobs.start(job.applicationId(), job.jobType, new Versions(job.triggering.platform(), job.triggering.application(), job.triggering.sourcePlatform(), @@ -199,35 +202,37 @@ public class DeploymentTrigger { else buildService.trigger(job); - applications().store(application.withJobTriggering(job.jobType, job.triggering)); + applications().store(application.with(job.applicationId().instance(), + instance -> instance.withJobTriggering(job.jobType, job.triggering))); }); return true; } catch (RuntimeException e) { log.log(LogLevel.WARNING, "Exception triggering " + job + ": " + e); if (e instanceof NoSuchElementException || e instanceof IllegalArgumentException) - applications().lockApplicationOrThrow(job.applicationId(), application -> + applications().lockApplicationOrThrow(TenantAndApplicationId.from(job.applicationId()), application -> applications().store(application.withProjectId(OptionalLong.empty()))); return false; } } - /** Force triggering of a job for given application. */ + /** Force triggering of a job for given instance. */ public List<JobType> forceTrigger(ApplicationId applicationId, JobType jobType, String user) { - Instance instance = applications().require(applicationId); + Application application = applications().requireApplication(TenantAndApplicationId.from(applicationId)); + Instance instance = application.require(applicationId.instance()); if (jobType == component) { - if (instance.deploymentJobs().deployedInternally()) + if (application.internal()) throw new IllegalArgumentException(applicationId + " has no component job we can trigger."); - buildService.trigger(BuildJob.of(applicationId, instance.deploymentJobs().projectId().getAsLong(), jobType.jobName())); + buildService.trigger(BuildJob.of(applicationId, application.projectId().getAsLong(), jobType.jobName())); return singletonList(component); } - Versions versions = Versions.from(instance.change(), instance, deploymentFor(instance, jobType), + Versions versions = Versions.from(application.change(), application, deploymentFor(instance, jobType), controller.systemVersion()); String reason = "Job triggered manually by " + user; return (jobType.isProduction() && ! isTested(instance, versions) - ? testJobs(instance, versions, reason, clock.instant(), __ -> true).stream() - : Stream.of(deploymentJob(instance, versions, instance.change(), jobType, reason, clock.instant()))) + ? testJobs(application.deploymentSpec(), application.change(), instance, versions, reason, clock.instant(), __ -> true).stream() + : Stream.of(deploymentJob(instance, versions, application.change(), jobType, reason, clock.instant()))) .peek(this::trigger) .map(Job::jobType).collect(toList()); } @@ -237,20 +242,21 @@ public class DeploymentTrigger { if (until.isAfter(clock.instant().plus(maxPause))) throw new IllegalArgumentException("Pause only allowed for up to " + maxPause); - applications().lockOrThrow(id, application -> - applications().store(application.withJobPause(jobType, OptionalLong.of(until.toEpochMilli())))); + applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> + applications().store(application.with(id.instance(), + instance -> instance.withJobPause(jobType, OptionalLong.of(until.toEpochMilli()))))); } /** Triggers a change of this application, unless it already has a change. */ - public void triggerChange(ApplicationId applicationId, Change change) { - applications().lockOrThrow(applicationId, application -> { + public void triggerChange(TenantAndApplicationId applicationId, Change change) { + applications().lockApplicationOrThrow(applicationId, application -> { if ( ! application.get().change().hasTargets()) forceChange(applicationId, change); }); } /** Overrides the given application's platform and application changes with any contained in the given change. */ - public void forceChange(ApplicationId applicationId, Change change) { + public void forceChange(TenantAndApplicationId applicationId, Change change) { applications().lockApplicationOrThrow(applicationId, application -> { if (change.application().isPresent()) application = application.withOutstandingChange(Change.empty()); @@ -259,7 +265,7 @@ public class DeploymentTrigger { } /** Cancels the indicated part of the given application's change. */ - public void cancelChange(ApplicationId applicationId, ChangesToCancel cancellation) { + public void cancelChange(TenantAndApplicationId applicationId, ChangesToCancel cancellation) { applications().lockApplicationOrThrow(applicationId, application -> { Change change; switch (cancellation) { @@ -299,7 +305,14 @@ public class DeploymentTrigger { /** Returns the set of all jobs which have changes to propagate from the upstream steps. */ private List<Job> computeReadyJobs() { - return InstanceList.from(applications().asList()) + ApplicationList applications = ApplicationList.from(applications().asList()); + applications = applications.withProjectId(); + applications = applications.withChanges(); + var jobs = applications.idList().stream() + .map(this::computeReadyJobs) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + return ApplicationList.from(applications().asList()) .withProjectId() .withChanges() .idList().stream() @@ -311,70 +324,74 @@ public class DeploymentTrigger { /** * Finds the next step to trigger for the given application, if any, and returns these as a list. */ - private List<Job> computeReadyJobs(ApplicationId id) { + private List<Job> computeReadyJobs(TenantAndApplicationId id) { List<Job> jobs = new ArrayList<>(); - applications().get(id).ifPresent(application -> { - Change change = application.change(); - Optional<Instant> completedAt = max(application.deploymentJobs().statusOf(systemTest) - .<Instant>flatMap(job -> job.lastSuccess().map(JobRun::at)), - application.deploymentJobs().statusOf(stagingTest) - .<Instant>flatMap(job -> job.lastSuccess().map(JobRun::at))); - String reason = "New change available"; - List<Job> testJobs = null; // null means "uninitialised", while empty means "don't run any jobs". - DeploymentSteps steps = steps(application.deploymentSpec()); - - if (change.hasTargets()) { - for (Step step : steps.production()) { - List<JobType> stepJobs = steps.toJobs(step); - List<JobType> remainingJobs = stepJobs.stream().filter(job -> ! isComplete(change, application, job)).collect(toList()); - if ( ! remainingJobs.isEmpty()) { // Change is incomplete; trigger remaining jobs if ready, or their test jobs if untested. - for (JobType job : remainingJobs) { - Versions versions = Versions.from(change, application, deploymentFor(application, job), - controller.systemVersion()); - if (isTested(application, versions)) { - if (completedAt.isPresent() && canTrigger(job, versions, application, stepJobs)) { - jobs.add(deploymentJob(application, versions, change, job, reason, completedAt.get())); + applications().getApplication(id).ifPresent(application -> { + for (Instance instance : application.instances().values()) { + Change change = application.change(); + Optional<Instant> completedAt = max(instance.deploymentJobs().statusOf(systemTest) + .<Instant>flatMap(job -> job.lastSuccess().map(JobRun::at)), + instance.deploymentJobs().statusOf(stagingTest) + .<Instant>flatMap(job -> job.lastSuccess().map(JobRun::at))); + String reason = "New change available"; + List<Job> testJobs = null; // null means "uninitialised", while empty means "don't run any jobs". + DeploymentSteps steps = steps(application.deploymentSpec()); + + if (change.hasTargets()) { + for (Step step : steps.production()) { + List<JobType> stepJobs = steps.toJobs(step); + List<JobType> remainingJobs = stepJobs.stream().filter(job -> ! isComplete(change, change, instance, job)).collect(toList()); + if (!remainingJobs.isEmpty()) { // Change is incomplete; trigger remaining jobs if ready, or their test jobs if untested. + for (JobType job : remainingJobs) { + Versions versions = Versions.from(change, application, deploymentFor(instance, job), + controller.systemVersion()); + if (isTested(instance, versions)) { + if (completedAt.isPresent() && canTrigger(job, versions, instance, application.deploymentSpec(), stepJobs)) { + jobs.add(deploymentJob(instance, versions, change, job, reason, completedAt.get())); + } + if ( ! alreadyTriggered(instance, versions) && testJobs == null) { + testJobs = emptyList(); + } } - if ( ! alreadyTriggered(application, versions) && testJobs == null) { - testJobs = emptyList(); + else if (testJobs == null) { + testJobs = testJobs(application.deploymentSpec(), + change, instance, versions, + String.format("Testing deployment for %s (%s)", + job.jobName(), versions.toString()), + completedAt.orElseGet(clock::instant)); } } - else if (testJobs == null) { - testJobs = testJobs(application, versions, - String.format("Testing deployment for %s (%s)", - job.jobName(), versions.toString()), - completedAt.orElseGet(clock::instant)); - } + completedAt = Optional.empty(); } - completedAt = Optional.empty(); - } - else { // All jobs are complete; find the time of completion of this step. - if (stepJobs.isEmpty()) { // No jobs means this is a delay step. - Duration delay = ((DeploymentSpec.Delay) step).duration(); - completedAt = completedAt.map(at -> at.plus(delay)).filter(at -> !at.isAfter(clock.instant())); - reason += " after a delay of " + delay; - } - else { - completedAt = stepJobs.stream().map(job -> application.deploymentJobs().statusOf(job).get().lastCompleted().get().at()).max(naturalOrder()); - reason = "Available change in " + stepJobs.stream().map(JobType::jobName).collect(joining(", ")); + else { // All jobs are complete; find the time of completion of this step. + if (stepJobs.isEmpty()) { // No jobs means this is a delay step. + Duration delay = ((DeploymentSpec.Delay) step).duration(); + completedAt = completedAt.map(at -> at.plus(delay)).filter(at -> !at.isAfter(clock.instant())); + reason += " after a delay of " + delay; + } + else { + completedAt = stepJobs.stream().map(job -> instance.deploymentJobs().statusOf(job).get().lastCompleted().get().at()).max(naturalOrder()); + reason = "Available change in " + stepJobs.stream().map(JobType::jobName).collect(joining(", ")); + } } } } + if (testJobs == null) { // If nothing to test, but outstanding commits, test those. + testJobs = testJobs(application.deploymentSpec(), change, instance, + Versions.from(application.outstandingChange().onTopOf(change), + application, + steps.sortedDeployments(instance.productionDeployments().values()).stream().findFirst(), + controller.systemVersion()), + "Testing last changes outside prod", clock.instant()); + } + jobs.addAll(testJobs); } - if (testJobs == null) { // If nothing to test, but outstanding commits, test those. - testJobs = testJobs(application, Versions.from(application.outstandingChange().onTopOf(application.change()), - application, - steps.sortedDeployments(application.productionDeployments().values()).stream().findFirst(), - controller.systemVersion()), - "Testing last changes outside prod", clock.instant()); - } - jobs.addAll(testJobs); }); return Collections.unmodifiableList(jobs); } /** Returns whether given job should be triggered */ - private boolean canTrigger(JobType job, Versions versions, Instance instance, List<JobType> parallelJobs) { + private boolean canTrigger(JobType job, Versions versions, Instance instance, DeploymentSpec deploymentSpec, List<JobType> parallelJobs) { if (jobStateOf(instance, job) != idle) return false; // Are we already running jobs which are not in the set which can run in parallel with this? @@ -383,12 +400,12 @@ public class DeploymentTrigger { // Are there another suspended deployment such that we shouldn't simultaneously change this? if (job.isProduction() && isSuspendedInAnotherZone(instance, job.zone(controller.system()))) return false; - return triggerAt(clock.instant(), job, versions, instance); + return triggerAt(clock.instant(), job, versions, instance, deploymentSpec); } /** Returns whether given job should be triggered */ - private boolean canTrigger(JobType job, Versions versions, Instance instance) { - return canTrigger(job, versions, instance, null); + private boolean canTrigger(JobType job, Versions versions, Instance instance, DeploymentSpec deploymentSpec) { + return canTrigger(job, versions, instance, deploymentSpec, null); } private boolean isSuspendedInAnotherZone(Instance instance, ZoneId zone) { @@ -401,7 +418,7 @@ public class DeploymentTrigger { } /** Returns whether the given job can trigger at the given instant */ - public boolean triggerAt(Instant instant, JobType job, Versions versions, Instance instance) { + public boolean triggerAt(Instant instant, JobType job, Versions versions, Instance instance, DeploymentSpec deploymentSpec) { Optional<JobStatus> jobStatus = instance.deploymentJobs().statusOf(job); if (jobStatus.isEmpty()) return true; if (jobStatus.get().pausedUntil().isPresent() && jobStatus.get().pausedUntil().getAsLong() > clock.instant().toEpochMilli()) return false; @@ -409,7 +426,7 @@ public class DeploymentTrigger { if (jobStatus.get().lastCompleted().isEmpty()) return true; // Never completed if (jobStatus.get().firstFailing().isEmpty()) return true; // Should not happen as firstFailing should be set for an unsuccessful job if ( ! versions.targetsMatch(jobStatus.get().lastCompleted().get())) return true; // Always trigger as targets have changed - if (instance.deploymentSpec().upgradePolicy() == DeploymentSpec.UpgradePolicy.canary) return true; // Don't throttle canaries + if (deploymentSpec.upgradePolicy() == DeploymentSpec.UpgradePolicy.canary) return true; // Don't throttle canaries Instant firstFailing = jobStatus.get().firstFailing().get().at(); Instant lastCompleted = jobStatus.get().lastCompleted().get().at(); @@ -447,13 +464,11 @@ public class DeploymentTrigger { } private JobState jobStateOf(Instance instance, JobType jobType) { - if (instance.deploymentJobs().deployedInternally()) { + if (controller.applications().requireApplication(TenantAndApplicationId.from(instance.id())).internal()) { Optional<Run> run = controller.jobController().last(instance.id(), jobType); return run.isPresent() && ! run.get().hasEnded() ? JobState.running : JobState.idle; } - return buildService.stateOf(BuildJob.of(instance.id(), - instance.deploymentJobs().projectId().getAsLong(), - jobType.jobName())); + return buildService.stateOf(BuildJob.of(instance.id(), 0, jobType.jobName())); } // ---------- Completion logic ---------- @@ -470,7 +485,7 @@ public class DeploymentTrigger { * Additionally, if the application is pinned to a Vespa version, and the given change has a (this) platform, * the deployment for the job must be on the pinned version. */ - public boolean isComplete(Change change, Instance instance, JobType jobType) { + public boolean isComplete(Change change, Change fullChange, Instance instance, JobType jobType) { Optional<Deployment> existingDeployment = deploymentFor(instance, jobType); if ( change.isPinned() && change.platform().isPresent() @@ -479,10 +494,10 @@ public class DeploymentTrigger { return instance.deploymentJobs().statusOf(jobType).flatMap(JobStatus::lastSuccess) .map(job -> change.platform().map(job.platform()::equals).orElse(true) - && change.application().map(job.application()::equals).orElse(true)) + && change.application().map(job.application()::equals).orElse(true)) .orElse(false) || jobType.isProduction() - && existingDeployment.map(deployment -> ! isUpgrade(change, deployment) && isDowngrade(instance.change(), deployment)) + && existingDeployment.map(deployment -> ! isUpgrade(change, deployment) && isDowngrade(fullChange, deployment)) .orElse(false); } @@ -522,7 +537,7 @@ public class DeploymentTrigger { private boolean acceptNewApplicationVersion(Application application) { if ( ! application.deploymentSpec().canChangeRevisionAt(clock.instant())) return false; if (application.change().application().isPresent()) return true; // Replacing a previous application change is ok. - for (Instance instance : controller.applications().asList(TenantAndApplicationId.from(application.id()))) + for (Instance instance : application.instances().values()) if (instance.deploymentJobs().hasFailures()) return true; // Allow changes to fix upgrade problems. return application.change().platform().isEmpty(); } @@ -533,14 +548,14 @@ public class DeploymentTrigger { ? steps.testJobs() : steps.productionJobs(); - // TODO jonmv: Replace with checking of all instance-jobs, when this is available. - Instance instance = controller.applications().require(application.id()); Change change = application.change(); - if (jobs.stream().allMatch(job -> isComplete(application.change().withoutApplication(), instance, job))) - change = change.withoutPlatform(); + for (Instance instance : application.instances().values()) { + if (jobs.stream().allMatch(job -> isComplete(application.change().withoutApplication(), application.change(), instance, job))) + change = change.withoutPlatform(); - if (jobs.stream().allMatch(job -> isComplete(application.change().withoutPlatform(), instance, job))) - change = change.withoutApplication(); + if (jobs.stream().allMatch(job -> isComplete(application.change().withoutPlatform(), application.change(), instance, job))) + change = change.withoutApplication(); + } return change; } @@ -550,20 +565,23 @@ public class DeploymentTrigger { /** * Returns the list of test jobs that should run now, and that need to succeed on the given versions for it to be considered tested. */ - private List<Job> testJobs(Instance instance, Versions versions, String reason, Instant availableSince) { - return testJobs(instance, versions, reason, availableSince, jobType -> canTrigger(jobType, versions, instance)); + private List<Job> testJobs(DeploymentSpec deploymentSpec, Change change, Instance instance, Versions versions, + String reason, Instant availableSince) { + return testJobs(deploymentSpec, change, instance, versions, reason, availableSince, + jobType -> canTrigger(jobType, versions, instance, deploymentSpec)); } /** * Returns the list of test jobs that need to succeed on the given versions for it to be considered tested, filtered by the given condition. */ - private List<Job> testJobs(Instance instance, Versions versions, String reason, Instant availableSince, Predicate<JobType> condition) { + private List<Job> testJobs(DeploymentSpec deploymentSpec, Change change, Instance instance, Versions versions, + String reason, Instant availableSince, Predicate<JobType> condition) { List<Job> jobs = new ArrayList<>(); - for (JobType jobType : steps(instance.deploymentSpec()).testJobs()) { + for (JobType jobType : steps(deploymentSpec).testJobs()) { Optional<JobRun> completion = successOn(instance, jobType, versions) .filter(run -> versions.sourcesMatchIfPresent(run) || jobType == systemTest); if (completion.isEmpty() && condition.test(jobType)) - jobs.add(deploymentJob(instance, versions, instance.change(), jobType, reason, availableSince)); + jobs.add(deploymentJob(instance, versions, change, jobType, reason, availableSince)); } return jobs; } @@ -593,7 +611,7 @@ public class DeploymentTrigger { private Job(Instance instance, JobRun triggering, JobType jobType, Instant availableSince, boolean isRetry, boolean isApplicationUpgrade) { - super(instance.id(), instance.deploymentJobs().projectId().getAsLong(), jobType.jobName()); + super(instance.id(), 0L, jobType.jobName()); this.jobType = jobType; this.triggering = triggering; this.availableSince = availableSince; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java index ecec123145e..1828a189cad 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java @@ -19,8 +19,9 @@ import com.yahoo.security.KeyUtils; import com.yahoo.security.SignatureAlgorithm; import com.yahoo.security.X509CertificateBuilder; import com.yahoo.security.X509CertificateUtils; -import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; +import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.ActivateResult; import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; @@ -37,6 +38,7 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobReport; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.yolean.Exceptions; import javax.security.auth.x500.X500Principal; @@ -573,9 +575,9 @@ public class InternalStepRunner implements StepRunner { /** Sends a mail with a notification of a failed run, if one should be sent. */ private void sendNotification(Run run, DualLogger logger) { - Instance instance = controller.applications().require(run.id().application()); - Notifications notifications = instance.deploymentSpec().notifications(); - boolean newCommit = instance.change().application() + Application application = controller.applications().requireApplication(TenantAndApplicationId.from(run.id().application())); + Notifications notifications = application.deploymentSpec().notifications(); + boolean newCommit = application.change().application() .map(run.versions().targetApplication()::equals) .orElse(false); When when = newCommit ? failingCommit : failing; @@ -611,8 +613,8 @@ public class InternalStepRunner implements StepRunner { /** Returns the real application with the given id. */ private Instance application(ApplicationId id) { - controller.applications().lockOrThrow(id, __ -> { }); // Memory fence. - return controller.applications().require(id); + controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), __ -> { }); // Memory fence. + return controller.applications().requireInstance(id); } /** @@ -638,7 +640,7 @@ public class InternalStepRunner implements StepRunner { /** Returns the application package for the tester application, assembled from a generated config, fat-jar and services.xml. */ private ApplicationPackage testerPackage(RunId id) { ApplicationVersion version = controller.jobController().run(id).get().versions().targetApplication(); - DeploymentSpec spec = controller.applications().require(id.application()).deploymentSpec(); + DeploymentSpec spec = controller.applications().requireApplication(TenantAndApplicationId.from(id.application())).deploymentSpec(); ZoneId zone = id.type().zone(controller.system()); boolean useTesterCertificate = controller.system().isPublic() && id.type().isTest(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java index cb90d2bf31e..0ecce359a02 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java @@ -10,6 +10,7 @@ import com.yahoo.vespa.flags.BooleanFlag; import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.FlagSource; import com.yahoo.vespa.flags.Flags; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.LockedApplication; @@ -99,7 +100,7 @@ public class JobController { /** Rewrite all job data with the newest format. */ public void updateStorage() { - for (ApplicationId id : applications()) + for (ApplicationId id : instances()) for (JobType type : jobs(id)) { locked(id, type, runs -> { // runs is not modified here, and is written as it was. curator.readLastRun(id, type).ifPresent(curator::writeLastRun); @@ -152,7 +153,7 @@ public class JobController { return run; ZoneId zone = id.type().zone(controller.system()); - Optional<Deployment> deployment = Optional.ofNullable(controller.applications().require(id.application()) + Optional<Deployment> deployment = Optional.ofNullable(controller.applications().requireInstance(id.application()) .deployments().get(zone)); if (deployment.isEmpty() || deployment.get().at().isBefore(run.start())) return run; @@ -194,10 +195,11 @@ public class JobController { locked(id, run -> run.with(testerCertificate)); } - /** Returns a list of all application which have registered. */ - public List<ApplicationId> applications() { + /** Returns a list of all instances of applications which have registered. */ + public List<ApplicationId> instances() { return copyOf(controller.applications().asList().stream() - .filter(application -> application.deploymentJobs().deployedInternally()) + .filter(Application::internal) + .flatMap(application -> application.instances().values().stream()) .map(Instance::id) .iterator()); } @@ -237,12 +239,12 @@ public class JobController { /** Returns a list of all active runs. */ public List<Run> active() { - return copyOf(applications().stream() - .flatMap(id -> Stream.of(JobType.values()) + return copyOf(instances().stream() + .flatMap(id -> Stream.of(JobType.values()) .map(type -> last(id, type)) .flatMap(Optional::stream) .filter(run -> ! run.hasEnded())) - .iterator()); + .iterator()); } /** Changes the status of the given step, for the given run, provided it is still active. */ @@ -282,8 +284,8 @@ public class JobController { public ApplicationVersion submit(ApplicationId id, SourceRevision revision, String authorEmail, long projectId, ApplicationPackage applicationPackage, byte[] testPackageBytes) { AtomicReference<ApplicationVersion> version = new AtomicReference<>(); - controller.applications().lockApplicationOrThrow(id, application -> { - if ( ! application.get().deploymentJobs().deployedInternally()) + controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { // TODO jonmv: change callers + if ( ! application.get().internal()) application = registered(application); long run = nextBuild(id); @@ -311,18 +313,17 @@ public class JobController { /** Registers the given application, copying necessary application packages, and returns the modified version. */ private LockedApplication registered(LockedApplication application) { - controller.applications().asList(TenantAndApplicationId.from(application.get().id())).stream() - .map(Instance::id) - .forEach(id -> controller.applications().lockOrThrow(id, instance -> - // TODO jvenstad: Remove when everyone has migrated off SDv3 pipelines. Real soon now! - // Copy all current packages to the new application store - instance.get().productionDeployments().values().stream() - .map(Deployment::applicationVersion) - .distinct() - .forEach(appVersion -> { - byte[] content = controller.applications().artifacts().getApplicationPackage(application.get().id(), appVersion.id()); - controller.applications().applicationStore().put(application.get().id(), appVersion, content); - }))); + for (Instance instance : application.get().instances().values()) { + // TODO jvenstad: Remove when everyone has migrated off SDv3 pipelines. Real soon now! + // Copy all current packages to the new application store + instance.productionDeployments().values().stream() + .map(Deployment::applicationVersion) + .distinct() + .forEach(appVersion -> { + byte[] content = controller.applications().artifacts().getApplicationPackage(instance.id(), appVersion.id()); + controller.applications().applicationStore().put(instance.id(), appVersion, content); + }); + } // Make sure any ongoing upgrade is cancelled, since future jobs will require the tester artifact. return application.withChange(application.get().change().withoutPlatform().withoutApplication()) .withBuiltInternally(true); @@ -333,8 +334,8 @@ public class JobController { if ( ! type.environment().isManuallyDeployed() && versions.targetApplication().isUnknown()) throw new IllegalArgumentException("Target application must be a valid reference."); - controller.applications().lockIfPresent(id, application -> { - if ( ! application.get().deploymentJobs().deployedInternally()) + controller.applications().lockApplicationIfPresent(TenantAndApplicationId.from(id), application -> { + if ( ! application.get().internal()) throw new IllegalArgumentException(id + " is not built here!"); locked(id, type, __ -> { @@ -350,8 +351,8 @@ public class JobController { /** Stores the given package and starts a deployment of it, after aborting any such ongoing deployment. */ public void deploy(ApplicationId id, JobType type, Optional<Version> platform, ApplicationPackage applicationPackage) { - controller.applications().lockApplicationOrThrow(id, application -> { - if ( ! application.get().deploymentJobs().deployedInternally()) + controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { + if ( ! application.get().internal()) controller.applications().store(registered(application)); }); if ( ! type.environment().isManuallyDeployed()) @@ -387,7 +388,7 @@ public class JobController { /** Unregisters the given application and makes all associated data eligible for garbage collection. */ public void unregister(ApplicationId id) { - controller.applications().lockApplicationIfPresent(id, application -> { + controller.applications().lockApplicationIfPresent(TenantAndApplicationId.from(id), application -> { // TODO jonmv: change callers. controller.applications().store(application.withBuiltInternally(false)); jobs(id).forEach(type -> last(id, type).ifPresent(last -> abort(last.id()))); }); @@ -395,7 +396,7 @@ public class JobController { /** Deletes run data and tester deployments for applications which are unknown, or no longer built internally. */ public void collectGarbage() { - Set<ApplicationId> applicationsToBuild = new HashSet<>(applications()); + Set<ApplicationId> applicationsToBuild = new HashSet<>(instances()); curator.applicationsWithJobs().stream() .filter(id -> ! applicationsToBuild.contains(id)) .forEach(id -> { @@ -440,7 +441,7 @@ public class JobController { /** Returns a URI which points at a badge showing current status for all jobs for the given application. */ public URI overviewBadge(ApplicationId id) { - DeploymentSteps steps = new DeploymentSteps(controller.applications().require(id).deploymentSpec(), controller::system); + DeploymentSteps steps = new DeploymentSteps(controller.applications().requireApplication(TenantAndApplicationId.from(id)).deploymentSpec(), controller::system); return badges.overview(id, steps.jobs().stream() .map(type -> last(id, type)) @@ -471,13 +472,13 @@ public class JobController { /** Returns a set containing the zone of the deployment tested in the given run, and all production zones for the application. */ public Set<ZoneId> testedZoneAndProductionZones(ApplicationId id, JobType type) { return Stream.concat(Stream.of(type.zone(controller.system())), - controller.applications().require(id).productionDeployments().keySet().stream()) + controller.applications().requireInstance(id).productionDeployments().keySet().stream()) .collect(Collectors.toSet()); } // TODO jvenstad: Find a more appropriate way of doing this, at least when this is the only build service. private long nextBuild(ApplicationId id) { - return 1 + controller.applications().require(id).deploymentJobs() + return 1 + controller.applications().requireInstance(id).deploymentJobs() .statusOf(JobType.component) .flatMap(JobStatus::lastCompleted) .map(JobStatus.JobRun::id) @@ -485,8 +486,8 @@ public class JobController { } private void prunePackages(ApplicationId id) { - controller.applications().lockIfPresent(id, application -> { - application.get().productionDeployments().values().stream() + controller.applications().lockApplicationIfPresent(TenantAndApplicationId.from(id), application -> { + application.get().require(id.instance()).productionDeployments().values().stream() .map(Deployment::applicationVersion) .min(Comparator.comparingLong(applicationVersion -> applicationVersion.buildNumber().getAsLong())) .ifPresent(oldestDeployed -> { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java index 70424cf9813..385eaae8215 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.deployment; import com.yahoo.component.Version; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; @@ -112,40 +113,43 @@ public class Versions { } /** Create versions using change contained in application */ - public static Versions from(Instance instance, Version defaultPlatformVersion) { - return from(instance.change(), instance, Optional.empty(), defaultPlatformVersion); + public static Versions from(Application application, Version defaultPlatformVersion) { + return from(application.change(), application, Optional.empty(), defaultPlatformVersion); } /** Create versions using given change and application */ - public static Versions from(Change change, Instance instance, Optional<Deployment> deployment, + public static Versions from(Change change, Application application, Optional<Deployment> deployment, Version defaultPlatformVersion) { - return new Versions(targetPlatform(instance, change, deployment, defaultPlatformVersion), - targetApplication(instance, change, deployment), + return new Versions(targetPlatform(application, change, deployment, defaultPlatformVersion), + targetApplication(application, change, deployment), deployment.map(Deployment::version), deployment.map(Deployment::applicationVersion)); } - private static Version targetPlatform(Instance instance, Change change, Optional<Deployment> deployment, + private static Version targetPlatform(Application application, Change change, Optional<Deployment> deployment, Version defaultVersion) { if (change.isPinned() && change.platform().isPresent()) return change.platform().get(); return max(change.platform(), deployment.map(Deployment::version)) - .orElseGet(() -> instance.oldestDeployedPlatform().orElse(defaultVersion)); + .orElseGet(() -> application.oldestDeployedPlatform().orElse(defaultVersion)); } - private static ApplicationVersion targetApplication(Instance instance, Change change, + private static ApplicationVersion targetApplication(Application application, Change change, Optional<Deployment> deployment) { return max(change.application(), deployment.map(Deployment::applicationVersion)) - .orElseGet(() -> defaultApplicationVersion(instance)); - } - - private static ApplicationVersion defaultApplicationVersion(Instance instance) { - return instance.oldestDeployedApplication() - .orElseGet(() -> Optional.ofNullable(instance.deploymentJobs().jobStatus().get(JobType.component)) - .flatMap(JobStatus::lastSuccess) - .map(JobStatus.JobRun::application) - .orElse(ApplicationVersion.unknown)); + .orElseGet(() -> defaultApplicationVersion(application)); + } + + private static ApplicationVersion defaultApplicationVersion(Application application) { + return application.oldestDeployedApplication() + .or(() -> application.instances().values().stream() + .flatMap(instance -> instance.deploymentJobs().statusOf(JobType.component) + .flatMap(JobStatus::lastSuccess) + .map(JobStatus.JobRun::application) + .stream()) + .findAny()) + .orElse(ApplicationVersion.unknown); } private static <T extends Comparable<T>> Optional<T> max(Optional<T> o1, Optional<T> o2) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java index d829364a421..ca4e59812a0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.Controller; @@ -10,7 +11,9 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.Applicatio import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.InstanceList; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.tenant.UserTenant; import com.yahoo.yolean.Exceptions; @@ -47,13 +50,13 @@ public class ApplicationOwnershipConfirmer extends Maintainer { /** File an ownership issue with the owners of all applications we know about. */ private void confirmApplicationOwnerships() { - InstanceList.from(controller().applications().asList()) - .withProjectId() - .hasProductionDeployment() - .asList() - .stream() - .filter(application -> application.createdAt().isBefore(controller().clock().instant().minus(Duration.ofDays(90)))) - .forEach(application -> { + ApplicationList.from(controller().applications().asList()) + .withProjectId() + .withProductionDeployment() + .asList() + .stream() + .filter(application -> application.createdAt().isBefore(controller().clock().instant().minus(Duration.ofDays(90)))) + .forEach(application -> { try { Tenant tenant = tenantOf(application.id()); tenant.contact().ifPresent(contact -> { // TODO jvenstad: Makes sense to require, and run this only in main? @@ -71,25 +74,27 @@ public class ApplicationOwnershipConfirmer extends Maintainer { } - private ApplicationSummary summaryOf(ApplicationId application) { - var app = applications.require(application); + private ApplicationSummary summaryOf(TenantAndApplicationId application) { + var app = applications.requireApplication(application); var metrics = new HashMap<ZoneId, ApplicationSummary.Metric>(); - for (var kv : app.deployments().entrySet()) { - var zone = kv.getKey(); - var deploymentMetrics = kv.getValue().metrics(); - metrics.put(zone, new ApplicationSummary.Metric(deploymentMetrics.documentCount(), - deploymentMetrics.queriesPerSecond(), - deploymentMetrics.writesPerSecond())); - } - return new ApplicationSummary(app.id(), app.activity().lastQueried(), app.activity().lastWritten(), metrics); + for (Instance instance : app.instances().values()) + for (var kv : instance.deployments().entrySet()) { + var zone = kv.getKey(); + var deploymentMetrics = kv.getValue().metrics(); + metrics.put(zone, new ApplicationSummary.Metric(deploymentMetrics.documentCount(), + deploymentMetrics.queriesPerSecond(), + deploymentMetrics.writesPerSecond())); + } + // TODO jonmv: Default instance should really be replaced with something better. + return new ApplicationSummary(app.id().defaultInstance(), app.activity().lastQueried(), app.activity().lastWritten(), metrics); } /** Escalate ownership issues which have not been closed before a defined amount of time has passed. */ private void ensureConfirmationResponses() { - for (Instance instance : controller().applications().asList()) - instance.ownershipIssueId().ifPresent(issueId -> { + for (Application application : controller().applications().asList()) + application.ownershipIssueId().ifPresent(issueId -> { try { - Tenant tenant = tenantOf(instance.id()); + Tenant tenant = tenantOf(application.id()); ownershipIssues.ensureResponse(issueId, tenant.type() == Tenant.Type.athenz ? tenant.contact() : Optional.empty()); } catch (RuntimeException e) { @@ -99,28 +104,26 @@ public class ApplicationOwnershipConfirmer extends Maintainer { } private void updateConfirmedApplicationOwners() { - InstanceList.from(controller().applications().asList()) - .withProjectId() - .hasProductionDeployment() - .asList() - .stream() - .filter(application -> application.ownershipIssueId().isPresent()) - .forEach(application -> { - IssueId ownershipIssueId = application.ownershipIssueId().get(); - ownershipIssues.getConfirmedOwner(ownershipIssueId).ifPresent(owner -> { - controller().applications().lockApplicationIfPresent(application.id(), lockedApplication -> - controller().applications().store(lockedApplication.withOwner(owner))); - }); - }); + ApplicationList.from(controller().applications().asList()) + .withProjectId() + .withProductionDeployment() + .asList() + .stream() + .filter(application -> application.ownershipIssueId().isPresent()) + .forEach(application -> { + IssueId ownershipIssueId = application.ownershipIssueId().get(); + ownershipIssues.getConfirmedOwner(ownershipIssueId).ifPresent(owner -> { + controller().applications().lockApplicationIfPresent(application.id(), lockedApplication -> + controller().applications().store(lockedApplication.withOwner(owner))); + }); + }); } - private User determineAssignee(Tenant tenant, Instance instance) { - return instance.owner().orElse( - tenant instanceof UserTenant ? userFor(tenant) : null - ); + private User determineAssignee(Tenant tenant, Application application) { + return application.owner().orElse(tenant instanceof UserTenant ? userFor(tenant) : null); } - private Tenant tenantOf(ApplicationId applicationId) { + private Tenant tenantOf(TenantAndApplicationId applicationId) { return controller().tenants().get(applicationId.tenant()) .orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId)); } @@ -129,7 +132,7 @@ public class ApplicationOwnershipConfirmer extends Maintainer { return User.from(tenant.name().value().replaceFirst(Tenant.userPrefix, "")); } - protected void store(IssueId issueId, ApplicationId applicationId) { + protected void store(IssueId issueId, TenantAndApplicationId applicationId) { controller().applications().lockApplicationIfPresent(applicationId, application -> controller().applications().store(application.withOwnershipIssueId(issueId))); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingMaintainer.java index 37d19986cef..2ab1b4c93ed 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingMaintainer.java @@ -28,10 +28,9 @@ public class BillingMaintainer extends Maintainer { .filter(tenant -> tenant instanceof CloudTenant) .map(tenant -> (CloudTenant) tenant) .forEach(cloudTenant -> controller().applications().asList(cloudTenant.name()) - .stream() - .forEach(application -> { - billing.handleBilling(application.id(), cloudTenant.billingInfo().customerId()); - }) + .forEach(application -> { + billing.handleBilling(application.id().defaultInstance(), cloudTenant.billingInfo().customerId()); + }) ); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java index c005cec598c..3b76aba94ba 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.HostName; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; @@ -72,16 +73,20 @@ public class ClusterInfoMaintainer extends Maintainer { @Override protected void maintain() { - for (Instance instance : controller().applications().asList()) { - for (Deployment deployment : instance.deployments().values()) { - DeploymentId deploymentId = new DeploymentId(instance.id(), deployment.zone()); - try { - var nodes = nodeRepository.list(deploymentId.zoneId(), deploymentId.applicationId()); - Map<ClusterSpec.Id, ClusterInfo> clusterInfo = getClusterInfo(nodes); - controller().applications().lockIfPresent(instance.id(), lockedApplication -> - controller.applications().store(lockedApplication.withClusterInfo(deployment.zone(), clusterInfo))); - } catch (Exception e) { - log.log(Level.WARNING, "Failing getting cluster information for " + deploymentId, e); + for (Application application : controller().applications().asList()) { + for (Instance instance : application.instances().values()) { + for (Deployment deployment : instance.deployments().values()) { + DeploymentId deploymentId = new DeploymentId(instance.id(), deployment.zone()); + try { + var nodes = nodeRepository.list(deploymentId.zoneId(), deploymentId.applicationId()); + Map<ClusterSpec.Id, ClusterInfo> clusterInfo = getClusterInfo(nodes); + controller().applications().lockApplicationIfPresent(application.id(), lockedApplication -> + controller.applications().store(lockedApplication.with(instance.name(), + locked -> locked.withClusterInfo(deployment.zone(), clusterInfo)))); + } + catch (Exception e) { + log.log(Level.WARNING, "Failing getting cluster information for " + deploymentId, e); + } } } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java index 44e0c9fd393..7756e6b23a7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java @@ -1,6 +1,7 @@ // 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.maintenance; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.application.Deployment; @@ -23,20 +24,20 @@ public class DeploymentExpirer extends Maintainer { @Override protected void maintain() { - for (Instance instance : controller().applications().asList()) { - for (Deployment deployment : instance.deployments().values()) { - if (!isExpired(deployment)) continue; - - try { - log.log(Level.INFO, "Expiring deployment of " + instance.id() + " in " + deployment.zone()); - controller().applications().deactivate(instance.id(), deployment.zone()); - } catch (Exception e) { - log.log(Level.WARNING, "Could not expire " + deployment + " of " + instance + - ": " + Exceptions.toMessageString(e) + ". Retrying in " + - maintenanceInterval()); + for (Application application : controller().applications().asList()) + for (Instance instance : application.instances().values()) + for (Deployment deployment : instance.deployments().values()) { + if ( ! isExpired(deployment)) continue; + + try { + log.log(Level.INFO, "Expiring deployment of " + instance.id() + " in " + deployment.zone()); + controller().applications().deactivate(instance.id(), deployment.zone()); + } catch (Exception e) { + log.log(Level.WARNING, "Could not expire " + deployment + " of " + instance + + ": " + Exceptions.toMessageString(e) + ". Retrying in " + + maintenanceInterval()); + } } - } - } } /** Returns whether given deployment has expired according to its TTL */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java index 56df43d707c..7ae00fe51e8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java @@ -4,12 +4,15 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.SystemName; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.InstanceList; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.yolean.Exceptions; @@ -50,10 +53,10 @@ public class DeploymentIssueReporter extends Maintainer { } /** Returns the applications to maintain issue status for. */ - private List<Instance> applications() { - return InstanceList.from(controller().applications().asList()) - .withProjectId() - .asList(); + private List<Application> applications() { + return ApplicationList.from(controller().applications().asList()) + .withProjectId() + .asList(); } /** @@ -61,18 +64,18 @@ public class DeploymentIssueReporter extends Maintainer { * and store the issue id for the filed issues. Also, clear the issueIds of applications * where deployment has not failed for this amount of time. */ - private void maintainDeploymentIssues(List<Instance> instances) { - Set<ApplicationId> failingApplications = InstanceList.from(instances) - .failingApplicationChangeSince(controller().clock().instant().minus(maxFailureAge)) - .asList().stream() - .map(Instance::id) - .collect(Collectors.toSet()); - - for (Instance instance : instances) - if (failingApplications.contains(instance.id())) - fileDeploymentIssueFor(instance.id()); + private void maintainDeploymentIssues(List<Application> applications) { + Set<TenantAndApplicationId> failingApplications = ApplicationList.from(applications) + .failingApplicationChangeSince(controller().clock().instant().minus(maxFailureAge)) + .asList().stream() + .map(Application::id) + .collect(Collectors.toSet()); + + for (Application application : applications) + if (failingApplications.contains(application.id())) + fileDeploymentIssueFor(application.id()); else - store(instance.id(), null); + store(application.id(), null); } /** @@ -80,7 +83,7 @@ public class DeploymentIssueReporter extends Maintainer { * applications that have been failing the upgrade to the system version for * longer than the set grace period, or update this list if the issue already exists. */ - private void maintainPlatformIssue(List<Instance> instances) { + private void maintainPlatformIssue(List<Application> applications) { if (controller().system() == SystemName.cd) return; @@ -89,21 +92,25 @@ public class DeploymentIssueReporter extends Maintainer { if ((controller().versionStatus().version(systemVersion).confidence() != broken)) return; - if (InstanceList.from(instances) - .failingUpgradeToVersionSince(systemVersion, controller().clock().instant().minus(upgradeGracePeriod)) - .isEmpty()) + if (ApplicationList.from(applications) + .failingUpgradeToVersionSince(systemVersion, controller().clock().instant().minus(upgradeGracePeriod)) + .isEmpty()) return; - List<ApplicationId> failingApplications = InstanceList.from(instances) - .failingUpgradeToVersionSince(systemVersion, controller().clock().instant()) - .idList(); + List<ApplicationId> failingApplications = ApplicationList.from(applications) + .failingUpgradeToVersionSince(systemVersion, controller().clock().instant()) + .idList() + .stream() + .map(TenantAndApplicationId::defaultInstance) + .collect(Collectors.toUnmodifiableList()); + // TODO jonmv: Send only tenant and application, here and elsewhere in this. deploymentIssues.fileUnlessOpen(failingApplications, systemVersion); } - private Tenant ownerOf(ApplicationId applicationId) { + private Tenant ownerOf(TenantAndApplicationId applicationId) { return controller().tenants().get(applicationId.tenant()) - .orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId)); + .orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId)); } private User userFor(Tenant tenant) { @@ -111,13 +118,13 @@ public class DeploymentIssueReporter extends Maintainer { } /** File an issue for applicationId, if it doesn't already have an open issue associated with it. */ - private void fileDeploymentIssueFor(ApplicationId applicationId) { + private void fileDeploymentIssueFor(TenantAndApplicationId applicationId) { try { Tenant tenant = ownerOf(applicationId); tenant.contact().ifPresent(contact -> { User assignee = tenant.type() == Tenant.Type.user ? userFor(tenant) : null; - Optional<IssueId> ourIssueId = controller().applications().require(applicationId).deploymentJobs().issueId(); - IssueId issueId = deploymentIssues.fileUnlessOpen(ourIssueId, applicationId, assignee, contact); + Optional<IssueId> ourIssueId = controller().applications().requireApplication(applicationId).deploymentIssueId(); + IssueId issueId = deploymentIssues.fileUnlessOpen(ourIssueId, applicationId.defaultInstance(), assignee, contact); store(applicationId, issueId); }); } @@ -127,8 +134,8 @@ public class DeploymentIssueReporter extends Maintainer { } /** Escalate issues for which there has been no activity for a certain amount of time. */ - private void escalateInactiveDeploymentIssues(Collection<Instance> instances) { - instances.forEach(application -> application.deploymentJobs().issueId().ifPresent(issueId -> { + private void escalateInactiveDeploymentIssues(Collection<Application> applications) { + applications.forEach(application -> application.deploymentIssueId().ifPresent(issueId -> { try { Tenant tenant = ownerOf(application.id()); deploymentIssues.escalateIfInactive(issueId, @@ -141,7 +148,7 @@ public class DeploymentIssueReporter extends Maintainer { })); } - private void store(ApplicationId id, IssueId issueId) { + private void store(TenantAndApplicationId id, IssueId issueId) { controller().applications().lockApplicationIfPresent(id, application -> controller().applications().store(application.withDeploymentIssueId(issueId))); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java index d77856a1661..162e46e19d2 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.provision.SystemName; import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.Controller; @@ -41,40 +42,40 @@ public class DeploymentMetricsMaintainer extends Maintainer { @Override protected void maintain() { AtomicInteger failures = new AtomicInteger(0); + AtomicInteger attempts = new AtomicInteger(0); AtomicReference<Exception> lastException = new AtomicReference<>(null); - List<Instance> instanceList = applications.asList(); // Run parallel stream inside a custom ForkJoinPool so that we can control the number of threads used ForkJoinPool pool = new ForkJoinPool(applicationsToUpdateInParallel); pool.submit(() -> - instanceList.parallelStream().forEach(application -> { - applications.lockIfPresent(application.id(), locked -> - applications.store(locked.with(controller().metrics().getApplicationMetrics(application.id())))); + applications.asList().parallelStream().forEach(application -> { + for (Instance instance : application.instances().values()) + for (Deployment deployment : instance.deployments().values()) { + attempts.incrementAndGet(); + try { + if (deployment.version().getMajor() < 7) continue; + var collectedMetrics = controller().metrics().getDeploymentMetrics(instance.id(), deployment.zone()); + var now = controller().clock().instant(); + applications.lockApplicationIfPresent(application.id(), locked -> { + Deployment existingDeployment = locked.get().require(instance.name()).deployments().get(deployment.zone()); + if (existingDeployment == null) return; // Deployment removed since we started collecting metrics + DeploymentMetrics newMetrics = existingDeployment.metrics() + .withQueriesPerSecond(collectedMetrics.queriesPerSecond()) + .withWritesPerSecond(collectedMetrics.writesPerSecond()) + .withDocumentCount(collectedMetrics.documentCount()) + .withQueryLatencyMillis(collectedMetrics.queryLatencyMillis()) + .withWriteLatencyMillis(collectedMetrics.writeLatencyMillis()) + .at(now); + applications.store(locked.with(instance.name(), + lockedInstance -> lockedInstance.with(existingDeployment.zone(), newMetrics) + .recordActivityAt(now, existingDeployment.zone()))); - for (Deployment deployment : application.deployments().values()) { - try { - if (deployment.version().getMajor() < 7) continue; - var collectedMetrics = controller().metrics().getDeploymentMetrics(application.id(), deployment.zone()); - var now = controller().clock().instant(); - applications.lockIfPresent(application.id(), locked -> { - Deployment existingDeployment = locked.get().deployments().get(deployment.zone()); - if (existingDeployment == null) return; // Deployment removed since we started collecting metrics - DeploymentMetrics newMetrics = existingDeployment.metrics() - .withQueriesPerSecond(collectedMetrics.queriesPerSecond()) - .withWritesPerSecond(collectedMetrics.writesPerSecond()) - .withDocumentCount(collectedMetrics.documentCount()) - .withQueryLatencyMillis(collectedMetrics.queryLatencyMillis()) - .withWriteLatencyMillis(collectedMetrics.writeLatencyMillis()) - .at(now); - applications.store(locked.with(existingDeployment.zone(), newMetrics) - .recordActivityAt(now, existingDeployment.zone())); - - }); - } catch (Exception e) { - failures.incrementAndGet(); - lastException.set(e); + }); + } catch (Exception e) { + failures.incrementAndGet(); + lastException.set(e); + } } - } }) ); pool.shutdown(); @@ -84,7 +85,7 @@ public class DeploymentMetricsMaintainer extends Maintainer { log.log(LogLevel.WARNING, String.format("Failed to gather metrics for %d/%d applications. Retrying in %s. Last error: %s", failures.get(), - instanceList.size(), + attempts.get(), maintenanceInterval(), Exceptions.toMessageString(lastException.get()))); } 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 ba3b2118e9f..073a9e43b57 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 @@ -4,9 +4,11 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.google.common.collect.ImmutableMap; import com.yahoo.config.provision.ApplicationId; import com.yahoo.jdisc.Metric; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.InstanceList; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; @@ -61,24 +63,26 @@ public class MetricsReporter extends Maintainer { } private void reportDeploymentMetrics() { - InstanceList applications = InstanceList.from(controller().applications().asList()) - .hasProductionDeployment(); + List<Instance> instances = ApplicationList.from(controller().applications().asList()) + .withProductionDeployment().asList().stream() + .flatMap(application -> application.instances().values().stream()) + .collect(Collectors.toUnmodifiableList()); - metric.set(DEPLOYMENT_FAIL_METRIC, deploymentFailRatio(applications) * 100, metric.createContext(Collections.emptyMap())); + metric.set(DEPLOYMENT_FAIL_METRIC, deploymentFailRatio(instances) * 100, metric.createContext(Collections.emptyMap())); - averageDeploymentDurations(applications, clock.instant()).forEach((application, duration) -> { + averageDeploymentDurations(instances, clock.instant()).forEach((application, duration) -> { metric.set(DEPLOYMENT_AVERAGE_DURATION, duration.getSeconds(), metric.createContext(dimensions(application))); }); - deploymentsFailingUpgrade(applications).forEach((application, failingJobs) -> { + deploymentsFailingUpgrade(instances).forEach((application, failingJobs) -> { metric.set(DEPLOYMENT_FAILING_UPGRADES, failingJobs, metric.createContext(dimensions(application))); }); - deploymentWarnings(applications).forEach((application, warnings) -> { + deploymentWarnings(instances).forEach((application, warnings) -> { metric.set(DEPLOYMENT_WARNINGS, warnings, metric.createContext(dimensions(application))); }); - for (Instance instance : applications.asList()) + for (Instance instance : instances) instance.deploymentJobs().statusOf(JobType.component) .flatMap(JobStatus::lastSuccess) .flatMap(run -> run.application().buildTime()) @@ -92,24 +96,21 @@ public class MetricsReporter extends Maintainer { metric.createContext(Map.of())); } - private static double deploymentFailRatio(InstanceList instanceList) { - List<Instance> instances = instanceList.asList(); - if (instances.isEmpty()) return 0; - - return (double) instances.stream().filter(a -> a.deploymentJobs().hasFailures()).count() / - (double) instances.size(); + private static double deploymentFailRatio(List<Instance> instances) { + return instances.stream() + .mapToInt(instance -> instance.deploymentJobs().hasFailures() ? 1 : 0) + .average().orElse(0); } - private static Map<ApplicationId, Duration> averageDeploymentDurations(InstanceList applications, Instant now) { - return applications.asList().stream() - .collect(Collectors.toMap(Instance::id, - application -> averageDeploymentDuration(application, now))); + private static Map<ApplicationId, Duration> averageDeploymentDurations(List<Instance> instances, Instant now) { + return instances.stream() + .collect(Collectors.toMap(Instance::id, + instance -> averageDeploymentDuration(instance, now))); } - private static Map<ApplicationId, Integer> deploymentsFailingUpgrade(InstanceList applications) { - return applications.asList() - .stream() - .collect(Collectors.toMap(Instance::id, MetricsReporter::deploymentsFailingUpgrade)); + private static Map<ApplicationId, Integer> deploymentsFailingUpgrade(List<Instance> instances) { + return instances.stream() + .collect(Collectors.toMap(Instance::id, MetricsReporter::deploymentsFailingUpgrade)); } private static int deploymentsFailingUpgrade(Instance instance) { @@ -134,9 +135,9 @@ public class MetricsReporter extends Maintainer { .orElse(Duration.ZERO); } - private static Map<ApplicationId, Integer> deploymentWarnings(InstanceList applications) { - return applications.asList().stream() - .collect(Collectors.toMap(Instance::id, a -> maxWarningCountOf(a.deployments().values()))); + private static Map<ApplicationId, Integer> deploymentWarnings(List<Instance> instances) { + return instances.stream() + .collect(Collectors.toMap(Instance::id, a -> maxWarningCountOf(a.deployments().values()))); } private static int maxWarningCountOf(Collection<Deployment> deployments) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java index 047905c0841..95e1c53f10c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java @@ -1,6 +1,7 @@ // 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.maintenance; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; @@ -19,11 +20,11 @@ public class OutstandingChangeDeployer extends Maintainer { @Override protected void maintain() { - for (Instance instance : controller().applications().asList()) { - if (instance.outstandingChange().hasTargets() - && instance.deploymentSpec().canChangeRevisionAt(controller().clock().instant())) { - controller().applications().deploymentTrigger().triggerChange(instance.id(), - instance.outstandingChange()); + for (Application application : controller().applications().asList()) { + if (application.outstandingChange().hasTargets() + && application.deploymentSpec().canChangeRevisionAt(controller().clock().instant())) { + controller().applications().deploymentTrigger().triggerChange(application.id(), + application.outstandingChange()); } } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdater.java index 6c7d4cab908..9503167360e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdater.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdater.java @@ -7,7 +7,9 @@ import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.InstanceList; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.rotation.RotationId; import com.yahoo.vespa.hosted.controller.rotation.RotationState; import com.yahoo.vespa.hosted.controller.rotation.RotationStatus; @@ -42,16 +44,22 @@ public class RotationStatusUpdater extends Maintainer { @Override protected void maintain() { var failures = new AtomicInteger(0); + var attempts = new AtomicInteger(0); var lastException = new AtomicReference<Exception>(null); - var applicationList = InstanceList.from(applications.asList()).hasRotation(); + var instancesWithRotations = ApplicationList.from(applications.asList()).hasRotation().asList().stream() + .flatMap(application -> application.instances().values().stream()) + .filter(instance -> ! instance.rotations().isEmpty()); // Run parallel stream inside a custom ForkJoinPool so that we can control the number of threads used var pool = new ForkJoinPool(applicationsToUpdateInParallel); pool.submit(() -> { - applicationList.asList().parallelStream().forEach(application -> { + instancesWithRotations.parallel().forEach(instance -> { + attempts.incrementAndGet(); try { - applications.lockIfPresent(application.id(), (app) -> applications.store(app.with(getStatus(app.get())))); + RotationStatus status = getStatus(instance); + applications.lockApplicationIfPresent(TenantAndApplicationId.from(instance.id()), app -> + applications.store(app.with(instance.name(), locked -> locked.with(status)))); } catch (Exception e) { failures.incrementAndGet(); lastException.set(e); @@ -64,7 +72,7 @@ public class RotationStatusUpdater extends Maintainer { if (lastException.get() != null) { log.log(LogLevel.WARNING, String.format("Failed to get global routing status of %d/%d applications. Retrying in %s. Last error: ", failures.get(), - applicationList.size(), + attempts.get(), maintenanceInterval()), lastException.get()); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java index c4ca94e169d..d32a10dfbbf 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java @@ -4,8 +4,10 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy; import com.yahoo.vespa.curator.Lock; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.InstanceList; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; @@ -70,7 +72,7 @@ public class Upgrader extends Maintainer { cancelUpgradesOf(applications().with(UpgradePolicy.conservative).upgrading().failing().notUpgradingTo(conservativeTargets), reason); // Schedule the right upgrades - InstanceList applications = applications(); + ApplicationList applications = applications(); canaryTarget.ifPresent(target -> upgrade(applications.with(UpgradePolicy.canary), target)); defaultTargets.forEach(target -> upgrade(applications.with(UpgradePolicy.defaultPolicy), target)); conservativeTargets.forEach(target -> upgrade(applications.with(UpgradePolicy.conservative), target)); @@ -90,12 +92,12 @@ public class Upgrader extends Maintainer { } /** Returns a list of all applications, except those which are pinned — these should not be manipulated by the Upgrader */ - private InstanceList applications() { - return InstanceList.from(controller().applications().asList()).unpinned(); + private ApplicationList applications() { + return ApplicationList.from(controller().applications().asList()).unpinned(); } - private void upgrade(InstanceList applications, Version version) { - applications = applications.hasProductionDeployment(); + private void upgrade(ApplicationList applications, Version version) { + applications = applications.withProductionDeployment(); applications = applications.onLowerVersionThan(version); applications = applications.allowMajorVersion(version.getMajor(), targetMajorVersion().orElse(version.getMajor())); applications = applications.notDeploying(); // wait with applications deploying an application change or already upgrading @@ -103,15 +105,15 @@ public class Upgrader extends Maintainer { applications = applications.canUpgradeAt(controller().clock().instant()); // wait with applications that are currently blocking upgrades applications = applications.byIncreasingDeployedVersion(); // start with lowest versions applications = applications.first(numberOfApplicationsToUpgrade()); // throttle upgrades - for (Instance instance : applications.asList()) - controller().applications().deploymentTrigger().triggerChange(instance.id(), Change.of(version)); + for (Application application : applications.asList()) + controller().applications().deploymentTrigger().triggerChange(application.id(), Change.of(version)); } - private void cancelUpgradesOf(InstanceList applications, String reason) { + private void cancelUpgradesOf(ApplicationList applications, String reason) { if (applications.isEmpty()) return; log.info("Cancelling upgrading of " + applications.asList().size() + " applications: " + reason); - for (Instance instance : applications.asList()) - controller().applications().deploymentTrigger().cancelChange(instance.id(), PLATFORM); + for (Application application : applications.asList()) + controller().applications().deploymentTrigger().cancelChange(application.id(), PLATFORM); } /** Returns the number of applications to upgrade in this run */ 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 6d321231d00..f6ed4d29bed 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 @@ -6,6 +6,7 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; @@ -13,6 +14,7 @@ import com.yahoo.slime.Inspector; import com.yahoo.slime.ObjectTraverser; import com.yahoo.slime.Slime; import com.yahoo.vespa.hosted.controller.Application; +import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; @@ -29,6 +31,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.EndpointId; import com.yahoo.vespa.hosted.controller.application.JobStatus; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.rotation.RotationId; import com.yahoo.vespa.hosted.controller.rotation.RotationState; @@ -67,23 +70,30 @@ public class ApplicationSerializer { private static final String createdAtField = "createdAt"; private static final String deploymentSpecField = "deploymentSpecField"; private static final String validationOverridesField = "validationOverrides"; - private static final String deploymentsField = "deployments"; - private static final String deploymentJobsField = "deploymentJobs"; + private static final String instancesField = "instances"; private static final String deployingField = "deployingField"; + private static final String projectIdField = "projectId"; + private static final String builtInternallyField = "builtInternally"; private static final String pinnedField = "pinned"; private static final String outstandingChangeField = "outstandingChangeField"; + private static final String deploymentIssueField = "deploymentIssueId"; private static final String ownershipIssueIdField = "ownershipIssueId"; private static final String ownerField = "confirmedOwner"; private static final String majorVersionField = "majorVersion"; private static final String writeQualityField = "writeQuality"; private static final String queryQualityField = "queryQuality"; private static final String pemDeployKeyField = "pemDeployKey"; - private static final String assignedRotationsField = "assignedRotations"; - private static final String assignedRotationEndpointField = "endpointId"; private static final String assignedRotationClusterField = "clusterId"; private static final String assignedRotationRotationField = "rotationId"; private static final String applicationCertificateField = "applicationCertificate"; + // Instance fields + private static final String instanceNameField = "instanceName"; + private static final String deploymentsField = "deployments"; + private static final String deploymentJobsField = "deploymentJobs"; + private static final String assignedRotationsField = "assignedRotations"; + private static final String assignedRotationEndpointField = "endpointId"; + // Deployment fields private static final String zoneField = "zone"; private static final String environmentField = "environment"; @@ -104,10 +114,7 @@ public class ApplicationSerializer { private static final String lastWritesPerSecondField = "lastWritesPerSecond"; // DeploymentJobs fields - private static final String projectIdField = "projectId"; private static final String jobStatusField = "jobStatus"; - private static final String issueIdField = "jiraIssueId"; - private static final String builtInternallyField = "builtInternally"; // JobStatus field private static final String jobTypeField = "jobType"; @@ -162,28 +169,39 @@ public class ApplicationSerializer { // ------------------ Serialization - public Slime toSlime(Application instance) { + public Slime toSlime(Application application) { Slime slime = new Slime(); Cursor root = slime.setObject(); - root.setString(idField, instance.id().serializedForm()); - root.setLong(createdAtField, instance.createdAt().toEpochMilli()); - root.setString(deploymentSpecField, instance.deploymentSpec().xmlForm()); - root.setString(validationOverridesField, instance.validationOverrides().xmlForm()); - deploymentsToSlime(instance.deployments().values(), root.setArray(deploymentsField)); - toSlime(instance.deploymentJobs(), root.setObject(deploymentJobsField)); - toSlime(instance.change(), root, deployingField); - toSlime(instance.outstandingChange(), root, outstandingChangeField); - instance.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value())); - instance.owner().ifPresent(owner -> root.setString(ownerField, owner.username())); - instance.majorVersion().ifPresent(majorVersion -> root.setLong(majorVersionField, majorVersion)); - root.setDouble(queryQualityField, instance.metrics().queryServiceQuality()); - root.setDouble(writeQualityField, instance.metrics().writeServiceQuality()); - instance.pemDeployKey().ifPresent(pemDeployKey -> root.setString(pemDeployKeyField, pemDeployKey)); - assignedRotationsToSlime(instance.rotations(), root, assignedRotationsField); - toSlime(instance.rotationStatus(), root.setArray(rotationStatusField)); + root.setString(idField, application.id().serialized()); + root.setLong(createdAtField, application.createdAt().toEpochMilli()); + root.setString(deploymentSpecField, application.deploymentSpec().xmlForm()); + root.setString(validationOverridesField, application.validationOverrides().xmlForm()); + application.projectId().ifPresent(projectId -> root.setLong(projectIdField, projectId)); + application.deploymentIssueId().ifPresent(jiraIssueId -> root.setString(deploymentIssueField, jiraIssueId.value())); + application.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value())); + root.setBool(builtInternallyField, application.internal()); + toSlime(application.change(), root, deployingField); + toSlime(application.outstandingChange(), root, outstandingChangeField); + application.owner().ifPresent(owner -> root.setString(ownerField, owner.username())); + application.majorVersion().ifPresent(majorVersion -> root.setLong(majorVersionField, majorVersion)); + root.setDouble(queryQualityField, application.metrics().queryServiceQuality()); + root.setDouble(writeQualityField, application.metrics().writeServiceQuality()); + application.pemDeployKey().ifPresent(pemDeployKey -> root.setString(pemDeployKeyField, pemDeployKey)); + instancesToSlime(application, root.setArray(instancesField)); return slime; } + private void instancesToSlime(Application application, Cursor array) { + for (Instance instance : application.instances().values()) { + Cursor instanceObject = array.addObject(); + instanceObject.setString(instanceNameField, instance.name().value()); + deploymentsToSlime(instance.deployments().values(), instanceObject.setArray(deploymentsField)); + toSlime(instance.deploymentJobs(), instanceObject.setObject(deploymentJobsField)); + assignedRotationsToSlime(instance.rotations(), instanceObject, assignedRotationsField); + toSlime(instance.rotationStatus(), instanceObject.setArray(rotationStatusField)); + } + } + private void deploymentsToSlime(Collection<Deployment> deployments, Cursor array) { for (Deployment deployment : deployments) deploymentToSlime(deployment, array.addObject()); @@ -273,10 +291,7 @@ public class ApplicationSerializer { } private void toSlime(DeploymentJobs deploymentJobs, Cursor cursor) { - deploymentJobs.projectId().ifPresent(projectId -> cursor.setLong(projectIdField, projectId)); jobStatusToSlime(deploymentJobs.jobStatus().values(), cursor.setArray(jobStatusField)); - deploymentJobs.issueId().ifPresent(jiraIssueId -> cursor.setString(issueIdField, jiraIssueId.value())); - cursor.setBool(builtInternallyField, deploymentJobs.deployedInternally()); } private void jobStatusToSlime(Collection<JobStatus> jobStatuses, Cursor jobStatusArray) { @@ -347,26 +362,43 @@ public class ApplicationSerializer { public Application fromSlime(Slime slime) { Inspector root = slime.get(); - ApplicationId id = ApplicationId.fromSerializedForm(root.field(idField).asString()); + TenantAndApplicationId id = TenantAndApplicationId.fromSerialized(root.field(idField).asString()); Instant createdAt = Instant.ofEpochMilli(root.field(createdAtField).asLong()); DeploymentSpec deploymentSpec = DeploymentSpec.fromXml(root.field(deploymentSpecField).asString(), false); ValidationOverrides validationOverrides = ValidationOverrides.fromXml(root.field(validationOverridesField).asString()); - List<Deployment> deployments = deploymentsFromSlime(root.field(deploymentsField)); - DeploymentJobs deploymentJobs = deploymentJobsFromSlime(root.field(deploymentJobsField)); Change deploying = changeFromSlime(root.field(deployingField)); Change outstandingChange = changeFromSlime(root.field(outstandingChangeField)); + Optional<IssueId> deploymentIssueId = Serializers.optionalString(root.field(deploymentIssueField)).map(IssueId::from); Optional<IssueId> ownershipIssueId = Serializers.optionalString(root.field(ownershipIssueIdField)).map(IssueId::from); Optional<User> owner = Serializers.optionalString(root.field(ownerField)).map(User::from); OptionalInt majorVersion = Serializers.optionalInteger(root.field(majorVersionField)); ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(), root.field(writeQualityField).asDouble()); Optional<String> pemDeployKey = Serializers.optionalString(root.field(pemDeployKeyField)); - List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(deploymentSpec, root); - RotationStatus rotationStatus = rotationStatusFromSlime(root); - - return new Application(id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, - deploying, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, - pemDeployKey, assignedRotations, rotationStatus); + List<Instance> instances = instancesFromSlime(id, deploymentSpec, root.field(instancesField)); + OptionalLong projectId = Serializers.optionalLong(root.field(projectIdField)); + boolean builtInternally = root.field(builtInternallyField).asBool(); + + return new Application(id, createdAt, deploymentSpec, validationOverrides, deploying, outstandingChange, + deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, + pemDeployKey, projectId, builtInternally, instances); + } + + private List<Instance> instancesFromSlime(TenantAndApplicationId id, DeploymentSpec deploymentSpec, Inspector field) { + List<Instance> instances = new ArrayList<>(); + field.traverse((ArrayTraverser) (name, object) -> { + InstanceName instanceName = InstanceName.from(object.field(instanceNameField).asString()); + List<Deployment> deployments = deploymentsFromSlime(object.field(deploymentsField)); + DeploymentJobs deploymentJobs = deploymentJobsFromSlime(object.field(deploymentJobsField)); + List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(deploymentSpec, object); + RotationStatus rotationStatus = rotationStatusFromSlime(object); + instances.add(new Instance(id.instance(instanceName), + deployments, + deploymentJobs, + assignedRotations, + rotationStatus)); + }); + return instances; } private List<Deployment> deploymentsFromSlime(Inspector array) { @@ -497,12 +529,9 @@ public class ApplicationSerializer { } private DeploymentJobs deploymentJobsFromSlime(Inspector object) { - OptionalLong projectId = Serializers.optionalLong(object.field(projectIdField)); List<JobStatus> jobStatusList = jobStatusListFromSlime(object.field(jobStatusField)); - Optional<IssueId> issueId = Serializers.optionalString(object.field(issueIdField)).map(IssueId::from); - boolean builtInternally = object.field(builtInternallyField).asBool(); - return new DeploymentJobs(projectId, jobStatusList, issueId, builtInternally); + return new DeploymentJobs(OptionalLong.empty(), jobStatusList, Optional.empty(), false); // WARNING: Unused variables. } private Change changeFromSlime(Inspector object) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java index e80372113e8..2680160b1cb 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java @@ -6,6 +6,7 @@ import com.google.inject.Inject; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostName; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.path.Path; @@ -341,13 +342,16 @@ public class CuratorDb { // -------------- Applications --------------------------------------------- public void writeApplication(Application application) { - curator.set(applicationPath(TenantAndApplicationId.from(application.id())), asJson(applicationSerializer.toSlime(application))); - curator.set(oldApplicationPath(application.id()), asJson(applicationSerializer.toSlime(application))); - curator.set(instancePath(application.id()), asJson(applicationSerializer.toSlime(application))); + curator.set(applicationPath(application.id()), asJson(applicationSerializer.toSlime(application))); + for (InstanceName name : application.instances().keySet()) { + curator.set(oldApplicationPath(application.id().instance(name)), + asJson(instanceSerializer.toSlime(application.legacy(name)))); + } } - public Optional<Application> readApplication(ApplicationId application) { - return readSlime(instancePath(application)).map(applicationSerializer::fromSlime); + public Optional<Application> readApplication(TenantAndApplicationId application) { + List<Instance> instances = readInstances(id -> TenantAndApplicationId.from(id).equals(application)); + return Application.aggregate(instances); } public List<Application> readApplications() { @@ -358,92 +362,72 @@ public class CuratorDb { return readApplications(application -> application.tenant().equals(name)); } - private Stream<ApplicationId> readApplicationIds() { - // TODO jonmv: Filter on number of parts in id when reading from applicationRoot again. - return curator.getChildren(instanceRoot).stream().map(ApplicationId::fromSerializedForm); - } - - private List<Application> readApplications(Predicate<ApplicationId> applicationFilter) { - return readApplicationIds().filter(applicationFilter) - .sorted() - .map(this::readApplication) - .flatMap(Optional::stream) - .collect(Collectors.toUnmodifiableList()); - } - - public void removeApplication(ApplicationId id) { - // WARNING: This is part of a multi-step data move operation, so don't touch!!! - curator.delete(applicationPath(TenantAndApplicationId.from(id))); - curator.delete(oldApplicationPath(id)); - curator.delete(instancePath(id)); + private Stream<TenantAndApplicationId> readTenantAndApplicationIds() { + return readInstanceIds().map(TenantAndApplicationId::from).distinct(); } - // -------------- Instances --------------------------------------------- - - public void writeInstance(Instance instance) { - curator.set(applicationPath(TenantAndApplicationId.from(instance.id())), asJson(instanceSerializer.toSlime(instance))); - curator.set(oldApplicationPath(instance.id()), asJson(instanceSerializer.toSlime(instance))); - curator.set(instancePath(instance.id()), asJson(instanceSerializer.toSlime(instance))); + private List<Application> readApplications(Predicate<TenantAndApplicationId> applicationFilter) { + return readTenantAndApplicationIds().filter(applicationFilter) + .sorted() + .map(this::readApplication) + .flatMap(Optional::stream) + .collect(Collectors.toUnmodifiableList()); } - public Optional<Instance> readInstance(ApplicationId application) { - return readSlime(instancePath(application)).map(instanceSerializer::fromSlime); - } - - public List<Instance> readInstances() { - return readInstances(ignored -> true); - } - - public List<Instance> readInstances(TenantName name) { - return readInstances(instance -> instance.tenant().equals(name)); - } - - public List<Instance> readInstances(TenantAndApplicationId id) { - return readInstances(instance -> instance.tenant().equals(id.tenant()) - && instance.application().equals(id.application())); + private Optional<Instance> readInstance(ApplicationId application) { + return readSlime(oldApplicationPath(application)).map(instanceSerializer::fromSlime); } private Stream<ApplicationId> readInstanceIds() { - return curator.getChildren(instanceRoot).stream().map(ApplicationId::fromSerializedForm); + return curator.getChildren(applicationRoot).stream() + .filter(id -> id.split(":").length == 3) + .map(ApplicationId::fromSerializedForm); } - private List<Instance> readInstances(Predicate<ApplicationId> instanceFilter) { - return readInstanceIds().filter(instanceFilter) + private List<Instance> readInstances(Predicate<ApplicationId> applicationFilter) { + return readInstanceIds().filter(applicationFilter) .sorted() .map(this::readInstance) .flatMap(Optional::stream) .collect(Collectors.toUnmodifiableList()); } - public void removeInstance(ApplicationId id) { + public void removeApplication(ApplicationId id) { // WARNING: This is part of a multi-step data move operation, so don't touch!!! - curator.delete(applicationPath(TenantAndApplicationId.from(id))); curator.delete(oldApplicationPath(id)); - curator.delete(instancePath(id)); + if (readApplication(TenantAndApplicationId.from(id)).isEmpty()) + curator.delete(applicationPath(TenantAndApplicationId.from(id))); + } + + public void clearInstanceRoot() { + curator.delete(instanceRoot); } /** * Migration plan: * * Add filter for reading only Instance from old application path RELEASED - * Write instance to Instance and old application path RELEASED + * Write Instance to instance and old application path RELEASED + * + * Lock on application level for instance mutations MERGED * - * Write Instance to instance and application and old application paths DONE TO REMOVE - * Read Instance from instance path DONE TO REMOVE + * Write Instance to instance and application and old application paths DONE TO CHANGE DONE + * Read Instance from instance path DONE TO REMOVE DONE * Duplicate Application from Instance, with helper classes DONE - * Write Application to instance and application and old application paths DONE TO CHANGE - * Read Application from instance path DONE TO REMOVE - * Use Application where applicable DO - * Lock instances and application on same level: tenant + application DONE TO CHANGE - * When reading an application, read all instances, and aggregate them - * Write application with instances to application path - * Lock application on both tenant + application, and instance levels DONE + * Write Application to instance and application and old application paths DONE TO CHANGE DONE + * Read Application from instance path DONE TO REMOVE DONE + * Use Application where applicable DONE !!! + * Lock instances and application on same level: tenant + application DONE TO CHANGE DONE + * When reading an application, read all instances, and aggregate them DONE + * Write application with instances to application path DONE + * Write all instances of an application to old application path DONE + * Remove everything under instance root DONE * * Read Application with instances from application path (with filter) * Stop locking applications on instance level * * Stop writing Instance to old application path - * Remove unused parts of Instance and Application + * Remove unused parts of Instance (Used only for legacy serialization) */ // -------------- Job Runs ------------------------------------------------ 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 bfbd1beaa24..4b7414a42a6 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 @@ -9,6 +9,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.container.jdisc.HttpRequest; @@ -350,10 +351,10 @@ public class ApplicationApiHandler extends LoggingRequestHandler { TenantName tenant = TenantName.from(tenantName); Slime slime = new Slime(); Cursor array = slime.setArray(); - for (Instance instance : controller.applications().asList(tenant)) { - if (applicationName.isPresent() && ! instance.id().application().toString().equals(applicationName.get())) - continue; - toSlime(instance, array.addObject(), request); + for (Application application : controller.applications().asList(tenant)) { + if (applicationName.map(application.id().application().value()::equals).orElse(true)) + for (InstanceName instance : application.instances().keySet()) + toSlime(application.id().instance(instance), array.addObject(), request); } return new SlimeJsonResponse(slime); } @@ -395,7 +396,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private Instance getInstance(String tenantName, String applicationName, String instanceName) { ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, instanceName); - return controller.applications().get(applicationId) + return controller.applications().getInstance(applicationId) .orElseThrow(() -> new NotExistsException(applicationId + " not found")); } @@ -498,8 +499,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { .map(run -> run.application().source()) .ifPresent(source -> sourceRevisionToSlime(source, object.setObject("source"))); - instance.deploymentJobs().projectId() - .ifPresent(id -> object.setLong("projectId", id)); + application.projectId().ifPresent(id -> object.setLong("projectId", id)); // Currently deploying change if ( ! application.change().isEmpty()) { @@ -516,7 +516,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { .steps(application.deploymentSpec()) .sortedJobs(instance.deploymentJobs().jobStatus().values()); - object.setBool("deployedInternally", application.deploymentJobs().deployedInternally()); + object.setBool("deployedInternally", application.internal()); Cursor deploymentsArray = object.setArray("deploymentJobs"); for (JobStatus job : jobStatus) { Cursor jobObject = deploymentsArray.addObject(); @@ -608,7 +608,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } } - instance.pemDeployKey().ifPresent(key -> object.setString("pemDeployKey", key)); + application.pemDeployKey().ifPresent(key -> object.setString("pemDeployKey", key)); // Metrics Cursor metricsObject = object.setObject("metrics"); @@ -617,19 +617,19 @@ public class ApplicationApiHandler extends LoggingRequestHandler { // Activity Cursor activity = object.setObject("activity"); - instance.activity().lastQueried().ifPresent(instant -> activity.setLong("lastQueried", instant.toEpochMilli())); - instance.activity().lastWritten().ifPresent(instant -> activity.setLong("lastWritten", instant.toEpochMilli())); - instance.activity().lastQueriesPerSecond().ifPresent(value -> activity.setDouble("lastQueriesPerSecond", value)); - instance.activity().lastWritesPerSecond().ifPresent(value -> activity.setDouble("lastWritesPerSecond", value)); + application.activity().lastQueried().ifPresent(instant -> activity.setLong("lastQueried", instant.toEpochMilli())); + application.activity().lastWritten().ifPresent(instant -> activity.setLong("lastWritten", instant.toEpochMilli())); + application.activity().lastQueriesPerSecond().ifPresent(value -> activity.setDouble("lastQueriesPerSecond", value)); + application.activity().lastWritesPerSecond().ifPresent(value -> activity.setDouble("lastWritesPerSecond", value)); application.ownershipIssueId().ifPresent(issueId -> object.setString("ownershipIssueId", issueId.value())); application.owner().ifPresent(owner -> object.setString("owner", owner.username())); - application.deploymentJobs().issueId().ifPresent(issueId -> object.setString("deploymentIssueId", issueId.value())); + application.deploymentIssueId().ifPresent(issueId -> object.setString("deploymentIssueId", issueId.value())); } private HttpResponse deployment(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) { ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName); - Instance instance = controller.applications().get(id) + Instance instance = controller.applications().getInstance(id) .orElseThrow(() -> new NotExistsException(id + " not found")); DeploymentId deploymentId = new DeploymentId(instance.id(), @@ -683,7 +683,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { controller.zoneRegistry().getDeploymentTimeToLive(deploymentId.zoneId()) .ifPresent(deploymentTimeToLive -> response.setLong("expiryTimeEpochMs", deployment.at().plus(deploymentTimeToLive).toEpochMilli())); - controller.applications().require(deploymentId.applicationId()).deploymentJobs().projectId() + controller.applications().requireApplication(TenantAndApplicationId.from(deploymentId.applicationId())).projectId() .ifPresent(i -> response.setString("screwdriverId", String.valueOf(i))); sourceRevisionToSlime(deployment.applicationVersion().source(), response); @@ -749,7 +749,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { * If no known version is applicable, the newest version at least as old as the oldest platform is selected, * among all versions released for this system. If no such versions exists, throws an IllegalStateException. */ - private Version compileVersion(ApplicationId id) { + private Version compileVersion(TenantAndApplicationId id) { Version oldestPlatform = controller.applications().oldestInstalledPlatform(id); return controller.versionStatus().versions().stream() .filter(version -> version.confidence().equalOrHigherThan(VespaVersion.Confidence.low)) @@ -768,7 +768,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private HttpResponse setGlobalRotationOverride(String tenantName, String applicationName, String instanceName, String environment, String region, boolean inService, HttpRequest request) { - Instance instance = controller.applications().require(ApplicationId.from(tenantName, applicationName, instanceName)); + Instance instance = controller.applications().requireInstance(ApplicationId.from(tenantName, applicationName, instanceName)); ZoneId zone = ZoneId.from(environment, region); Deployment deployment = instance.deployments().get(zone); if (deployment == null) { @@ -813,7 +813,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private HttpResponse rotationStatus(String tenantName, String applicationName, String instanceName, String environment, String region, Optional<String> endpointId) { ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, instanceName); - Instance instance = controller.applications().require(applicationId); + Instance instance = controller.applications().requireInstance(applicationId); ZoneId zone = ZoneId.from(environment, region); RotationId rotation = findRotationId(instance, endpointId); Deployment deployment = instance.deployments().get(zone); @@ -891,7 +891,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private HttpResponse deploying(String tenant, String application, String instance, HttpRequest request) { - Instance app = controller.applications().require(ApplicationId.from(tenant, application, instance)); + Application app = controller.applications().requireApplication(TenantAndApplicationId.from(tenant, application)); Slime slime = new Slime(); Cursor root = slime.setObject(); if ( ! app.change().isEmpty()) { @@ -979,7 +979,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Application application = controller.applications().createApplication(id, credentials); Slime slime = new Slime(); - toSlime(application, slime.setObject(), request); + toSlime(id, slime.setObject(), request); return new SlimeJsonResponse(slime); } catch (ZmsClientException e) { // TODO: Push conversion down @@ -994,9 +994,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private HttpResponse deployPlatform(String tenantName, String applicationName, String instanceName, boolean pin, HttpRequest request) { request = controller.auditLogger().log(request); String versionString = readToString(request.getData()); - ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName); + TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName); StringBuilder response = new StringBuilder(); - controller.applications().lockOrThrow(id, application -> { + controller.applications().lockApplicationOrThrow(id, application -> { Version version = Version.fromString(versionString); if (version.equals(Version.emptyVersion)) version = controller.systemVersion(); @@ -1021,10 +1021,10 @@ public class ApplicationApiHandler extends LoggingRequestHandler { /** Trigger deployment to the last known application package for the given application. */ private HttpResponse deployApplication(String tenantName, String applicationName, String instanceName, HttpRequest request) { controller.auditLogger().log(request); - ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName); + TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName); StringBuilder response = new StringBuilder(); - controller.applications().lockOrThrow(id, application -> { - Change change = Change.of(application.get().deploymentJobs().statusOf(JobType.component).get().lastSuccess().get().application()); + controller.applications().lockApplicationOrThrow(id, application -> { + Change change = Change.of(application.get().require(InstanceName.from(instanceName)).deploymentJobs().statusOf(JobType.component).get().lastSuccess().get().application()); controller.applications().deploymentTrigger().forceChange(id, change); response.append("Triggered " + change + " for " + id); }); @@ -1033,9 +1033,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler { /** Cancel ongoing change for given application, e.g., everything with {"cancel":"all"} */ private HttpResponse cancelDeploy(String tenantName, String applicationName, String instanceName, String choice) { - ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName); + TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName); StringBuilder response = new StringBuilder(); - controller.applications().lockOrThrow(id, application -> { + controller.applications().lockApplicationOrThrow(id, application -> { Change change = application.get().change(); if (change.isEmpty()) { response.append("No deployment in progress for " + application + " at this time"); @@ -1045,7 +1045,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { ChangesToCancel cancel = ChangesToCancel.valueOf(choice.toUpperCase()); controller.applications().deploymentTrigger().cancelChange(id, cancel); response.append("Changed deployment from '" + change + "' to '" + - controller.applications().require(id).change() + "' for " + application); + controller.applications().requireApplication(id).change() + "' for " + application); }); return new MessageResponse(response.toString()); @@ -1130,6 +1130,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Optional<ApplicationPackage> applicationPackage = Optional.ofNullable(dataParts.get("applicationZip")) .map(ApplicationPackage::new); + Optional<Application> application = controller.applications().getApplication(TenantAndApplicationId.from(applicationId)); Inspector sourceRevision = deployOptions.field("sourceRevision"); Inspector buildNumber = deployOptions.field("buildNumber"); @@ -1143,7 +1144,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler { applicationVersion = Optional.of(ApplicationVersion.from(toSourceRevision(sourceRevision), buildNumber.asLong())); - applicationPackage = Optional.of(controller.applications().getApplicationPackage(controller.applications().require(applicationId), applicationVersion.get())); + applicationPackage = Optional.of(controller.applications().getApplicationPackage(applicationId, + application.get().internal(), + applicationVersion.get())); } boolean deployDirectly = deployOptions.field("deployDirectly").asBool(); @@ -1152,7 +1155,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (deployDirectly && applicationPackage.isEmpty() && applicationVersion.isEmpty() && vespaVersion.isEmpty()) { // Redeploy the existing deployment with the same versions. - Optional<Deployment> deployment = controller.applications().get(applicationId) + Optional<Deployment> deployment = controller.applications().getInstance(applicationId) .map(Instance::deployments) .flatMap(deployments -> Optional.ofNullable(deployments.get(zone))); @@ -1165,7 +1168,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler { applicationVersion = Optional.of(version); vespaVersion = Optional.of(deployment.get().version()); - applicationPackage = Optional.of(controller.applications().getApplicationPackage(controller.applications().require(applicationId), applicationVersion.get())); + applicationPackage = Optional.of(controller.applications().getApplicationPackage(applicationId, + application.get().internal(), + applicationVersion.get())); } // TODO: get rid of the json object @@ -1221,7 +1226,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private HttpResponse deactivate(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) { - Instance instance = controller.applications().require(ApplicationId.from(tenantName, applicationName, instanceName)); + Instance instance = controller.applications().requireInstance(ApplicationId.from(tenantName, applicationName, instanceName)); // Attempt to deactivate application even if the deployment is not known by the controller DeploymentId deploymentId = new DeploymentId(instance.id(), ZoneId.from(environment, region)); @@ -1234,7 +1239,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { try { DeploymentJobs.JobReport report = toJobReport(tenant, application, toSlime(request.getData()).get()); if ( report.jobType() == JobType.component - && controller.applications().require(report.applicationId()).deploymentJobs().deployedInternally()) + && controller.applications().requireApplication(TenantAndApplicationId.from(report.applicationId())).internal()) throw new IllegalArgumentException(report.applicationId() + " is set up to be deployed from internally, and no " + "longer accepts submissions from Screwdriver v3 jobs. If you need to revert " + "to the old pipeline, please file a ticket at yo/vespa-support and request this."); @@ -1313,19 +1318,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler { default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'."); } Cursor applicationArray = object.setArray("applications"); - for (Instance instance : controller.applications().asList(tenant.name())) { - if ( ! instance.id().instance().isTester()) { + for (Application application : controller.applications().asList(tenant.name())) + for (Instance instance : application.instances().values()) if (recurseOverApplications(request)) - toSlime(applicationArray.addObject(), - instance, - getApplication(instance.id().tenant().value(), - instance.id().application().value(), - instance.id().instance().value()), - request); + toSlime(applicationArray.addObject(), instance, application, request); else - toSlime(instance, applicationArray.addObject(), request); - } - } + toSlime(instance.id(), applicationArray.addObject(), request); } // A tenant has different content when in a list ... antipattern, but not solvable before application/v5 @@ -1404,25 +1402,14 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return Joiner.on("/").join(elements); } - private void toSlime(Application application, Cursor object, HttpRequest request) { - object.setString("tenant", application.id().tenant().value()); - object.setString("application", application.id().application().value()); - object.setString("instance", application.id().instance().value()); - object.setString("url", withPath("/application/v4" + - "/tenant/" + application.id().tenant().value() + - "/application/" + application.id().application().value() + - "/instance/" + application.id().instance().value(), - request.getUri()).toString()); - } - - private void toSlime(Instance instance, Cursor object, HttpRequest request) { - object.setString("tenant", instance.id().tenant().value()); - object.setString("application", instance.id().application().value()); - object.setString("instance", instance.id().instance().value()); + private void toSlime(ApplicationId id, Cursor object, HttpRequest request) { + object.setString("tenant", id.tenant().value()); + object.setString("application", id.application().value()); + object.setString("instance", id.instance().value()); object.setString("url", withPath("/application/v4" + - "/tenant/" + instance.id().tenant().value() + - "/application/" + instance.id().application().value() + - "/instance/" + instance.id().instance().value(), + "/tenant/" + id.tenant().value() + + "/application/" + id.application().value() + + "/instance/" + id.instance().value(), request.getUri()).toString()); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java index 697e95e75b3..90a4ecdef9a 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java @@ -10,6 +10,7 @@ import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.NotExistsException; @@ -22,6 +23,7 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.JobStatus; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.deployment.DeploymentSteps; import com.yahoo.vespa.hosted.controller.deployment.JobController; import com.yahoo.vespa.hosted.controller.deployment.Run; @@ -69,17 +71,18 @@ class JobControllerApiHandlerHelper { * @return Response with all job types that have recorded runs for the application _and_ the status for the last run of that type */ static HttpResponse jobTypeResponse(Controller controller, ApplicationId id, URI baseUriForJobs) { - Instance instance = controller.applications().require(id); - Change change = instance.change(); - DeploymentSteps steps = new DeploymentSteps(instance.deploymentSpec(), controller::system); + Application application = controller.applications().requireApplication(TenantAndApplicationId.from(id)); + Instance instance = application.require(id.instance()); + Change change = application.change(); + DeploymentSteps steps = new DeploymentSteps(application.deploymentSpec(), controller::system); // The logic for pending runs imitates DeploymentTrigger logic; not good, but the trigger wiring must be re-written to reuse :S Map<JobType, Versions> pendingProduction = steps.productionJobs().stream() - .filter(type -> ! controller.applications().deploymentTrigger().isComplete(change, instance, type)) + .filter(type -> ! controller.applications().deploymentTrigger().isComplete(change, change, instance, type)) .collect(Collectors.toMap(type -> type, type -> Versions.from(change, - instance, + application, Optional.ofNullable(instance.deployments().get(type.zone(controller.system()))), controller.systemVersion()), (v1, v2) -> { throw new IllegalStateException("Entries '" + v1 + "' and '" + v2 + "' have the same key!"); }, @@ -97,8 +100,8 @@ class JobControllerApiHandlerHelper { Cursor lastVersionsObject = responseObject.setObject("lastVersions"); if (instance.deploymentJobs().statusOf(component).flatMap(JobStatus::lastSuccess).isPresent()) { - lastPlatformToSlime(lastVersionsObject.setObject("platform"), controller, instance, change, steps); - lastApplicationToSlime(lastVersionsObject.setObject("application"), instance, change, steps, controller); + lastPlatformToSlime(lastVersionsObject.setObject("platform"), controller, application, instance, change, steps); + lastApplicationToSlime(lastVersionsObject.setObject("application"), application, instance, change, steps, controller); } Cursor deployingObject = responseObject.setObject("deploying"); @@ -128,6 +131,7 @@ class JobControllerApiHandlerHelper { steps.jobs().forEach(type -> { jobTypeToSlime(jobsObject.setObject(shortNameOf(type, controller.system())), controller, + application, instance, type, steps, @@ -152,11 +156,11 @@ class JobControllerApiHandlerHelper { return new SlimeJsonResponse(slime); } - private static void lastPlatformToSlime(Cursor lastPlatformObject, Controller controller, Instance instance, Change change, DeploymentSteps steps) { + private static void lastPlatformToSlime(Cursor lastPlatformObject, Controller controller, Application application, Instance instance, Change change, DeploymentSteps steps) { VespaVersion lastVespa = controller.versionStatus().version(controller.systemVersion()); VespaVersion.Confidence targetConfidence = Map.of(defaultPolicy, normal, conservative, high) - .getOrDefault(instance.deploymentSpec().upgradePolicy(), broken); + .getOrDefault(application.deploymentSpec().upgradePolicy(), broken); for (VespaVersion version : controller.versionStatus().versions()) if ( ! version.versionNumber().isAfter(controller.systemVersion()) && version.confidence().equalOrHigherThan(targetConfidence)) @@ -165,39 +169,39 @@ class JobControllerApiHandlerHelper { Version lastPlatform = lastVespa.versionNumber(); lastPlatformObject.setString("platform", lastPlatform.toString()); lastPlatformObject.setLong("at", lastVespa.committedAt().toEpochMilli()); - long completed = steps.productionJobs().stream().filter(type -> controller.applications().deploymentTrigger().isComplete(Change.of(lastPlatform), instance, type)).count(); + long completed = steps.productionJobs().stream().filter(type -> controller.applications().deploymentTrigger().isComplete(Change.of(lastPlatform), change, instance, type)).count(); if (Optional.of(lastPlatform).equals(change.platform())) lastPlatformObject.setString("deploying", completed + " of " + steps.productionJobs().size() + " complete"); else if (completed == steps.productionJobs().size()) lastPlatformObject.setString("completed", completed + " of " + steps.productionJobs().size() + " complete"); - else if ( ! instance.deploymentSpec().canUpgradeAt(controller.clock().instant())) { - lastPlatformObject.setString("blocked", instance.deploymentSpec().changeBlocker().stream() - .filter(blocker -> blocker.blocksVersions()) - .filter(blocker -> blocker.window().includes(controller.clock().instant())) - .findAny().map(blocker -> blocker.window().toString()).get()); + else if ( ! application.deploymentSpec().canUpgradeAt(controller.clock().instant())) { + lastPlatformObject.setString("blocked", application.deploymentSpec().changeBlocker().stream() + .filter(blocker -> blocker.blocksVersions()) + .filter(blocker -> blocker.window().includes(controller.clock().instant())) + .findAny().map(blocker -> blocker.window().toString()).get()); } else lastPlatformObject.setString("pending", - instance.change().isEmpty() + application.change().isEmpty() ? "Waiting for upgrade slot" : "Waiting for " + instance.change() + " to complete"); } - private static void lastApplicationToSlime(Cursor lastApplicationObject, Instance instance, Change change, DeploymentSteps steps, Controller controller) { + private static void lastApplicationToSlime(Cursor lastApplicationObject, Application application, Instance instance, Change change, DeploymentSteps steps, Controller controller) { long completed; ApplicationVersion lastApplication = instance.deploymentJobs().statusOf(component).flatMap(JobStatus::lastSuccess).get().application(); applicationVersionToSlime(lastApplicationObject.setObject("application"), lastApplication); lastApplicationObject.setLong("at", instance.deploymentJobs().statusOf(component).flatMap(JobStatus::lastSuccess).get().at().toEpochMilli()); - completed = steps.productionJobs().stream().filter(type -> controller.applications().deploymentTrigger().isComplete(Change.of(lastApplication), instance, type)).count(); + completed = steps.productionJobs().stream().filter(type -> controller.applications().deploymentTrigger().isComplete(Change.of(lastApplication), change, instance, type)).count(); if (Optional.of(lastApplication).equals(change.application())) lastApplicationObject.setString("deploying", completed + " of " + steps.productionJobs().size() + " complete"); else if (completed == steps.productionJobs().size()) lastApplicationObject.setString("completed", completed + " of " + steps.productionJobs().size() + " complete"); - else if ( ! instance.deploymentSpec().canChangeRevisionAt(controller.clock().instant())) { - lastApplicationObject.setString("blocked", instance.deploymentSpec().changeBlocker().stream() - .filter(blocker -> blocker.blocksRevisions()) - .filter(blocker -> blocker.window().includes(controller.clock().instant())) - .findAny().map(blocker -> blocker.window().toString()).get()); + else if ( ! application.deploymentSpec().canChangeRevisionAt(controller.clock().instant())) { + lastApplicationObject.setString("blocked", application.deploymentSpec().changeBlocker().stream() + .filter(blocker -> blocker.blocksRevisions()) + .filter(blocker -> blocker.window().includes(controller.clock().instant())) + .findAny().map(blocker -> blocker.window().toString()).get()); } else lastApplicationObject.setString("pending", "Waiting for current deployment to complete"); @@ -220,7 +224,7 @@ class JobControllerApiHandlerHelper { deploymentObject.setString("status", pendingProduction.containsKey(type) ? "pending" : "completed"); } - private static void jobTypeToSlime(Cursor jobObject, Controller controller, Instance instance, JobType type, DeploymentSteps steps, + private static void jobTypeToSlime(Cursor jobObject, Controller controller, Application application, Instance instance, JobType type, DeploymentSteps steps, Map<JobType, Versions> pendingProduction, Map<JobType, Run> running, URI baseUriForJob) { instance.deploymentJobs().statusOf(type).ifPresent(status -> status.pausedUntil().ifPresent(until -> jobObject.setLong("pausedUntil", until))); @@ -244,7 +248,7 @@ class JobControllerApiHandlerHelper { Cursor runObject = runArray.addObject(); runObject.setString("status", "pending"); versionsToSlime(runObject, versions); - if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, versions, instance)) + if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, versions, instance, application.deploymentSpec())) runObject.setObject("tasks").setString("cooldown", "failed"); else runObject.setObject("tasks").setString("capacity", "running"); @@ -262,7 +266,7 @@ class JobControllerApiHandlerHelper { Cursor pendingObject = runObject.setObject("tasks"); if (instance.deploymentJobs().statusOf(type).map(status -> status.pausedUntil().isPresent()).orElse(false)) pendingObject.setString("paused", "pending"); - else if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, versions, instance)) + else if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, versions, instance, application.deploymentSpec())) pendingObject.setString("cooldown", "failed"); else { int pending = 0; @@ -281,8 +285,8 @@ class JobControllerApiHandlerHelper { break; for (JobType stepType : steps.toJobs(step)) { if (pendingProduction.containsKey(stepType)) { - Versions jobVersions = Versions.from(instance.change(), - instance, + Versions jobVersions = Versions.from(application.change(), + application, Optional.ofNullable(instance.deployments().get(stepType.zone(controller.system()))), controller.systemVersion()); pendingObject.setString(shortNameOf(stepType, controller.system()), statusOf(controller, instance.id(), stepType, jobVersions)); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java index b2bc6d72044..86310ca2f6b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java @@ -18,6 +18,7 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.restapi.ErrorResponse; import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.restapi.Uri; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.restapi.application.EmptyResponse; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; import com.yahoo.yolean.Exceptions; @@ -97,7 +98,7 @@ public class DeploymentApiHandler extends LoggingRequestHandler { Cursor failingArray = versionObject.setArray("failingApplications"); for (ApplicationId id : version.statistics().failing()) { - controller.applications().get(id).ifPresent(application -> { + controller.applications().getInstance(id).ifPresent(application -> { firstFailingOn(version.versionNumber(), application).ifPresent(firstFailing -> { Cursor applicationObject = failingArray.addObject(); toSlime(applicationObject, application, request); @@ -108,7 +109,7 @@ public class DeploymentApiHandler extends LoggingRequestHandler { Cursor productionArray = versionObject.setArray("productionApplications"); for (ApplicationId id : version.statistics().production()) { - controller.applications().get(id).ifPresent(application -> { + controller.applications().getInstance(id).ifPresent(application -> { int successes = productionSuccessesFor(version.versionNumber(), application); if (successes == 0) return; // Just upgraded to a newer version. Cursor applicationObject = productionArray.addObject(); @@ -120,7 +121,7 @@ public class DeploymentApiHandler extends LoggingRequestHandler { Cursor runningArray = versionObject.setArray("deployingApplications"); for (ApplicationId id : version.statistics().deploying()) { - controller.applications().get(id).ifPresent(application -> { + controller.applications().getInstance(id).ifPresent(application -> { lastDeployingTo(version.versionNumber(), application).ifPresent(lastDeploying -> { Cursor applicationObject = runningArray.addObject(); toSlime(applicationObject, application, request); @@ -140,7 +141,8 @@ public class DeploymentApiHandler extends LoggingRequestHandler { instance.id().tenant().value() + "/application/" + instance.id().application().value()).toString()); - object.setString("upgradePolicy", toString(instance.deploymentSpec().upgradePolicy())); + object.setString("upgradePolicy", toString(controller.applications().requireApplication(TenantAndApplicationId.from(instance.id())) + .deploymentSpec().upgradePolicy())); } private static String toString(DeploymentSpec.UpgradePolicy upgradePolicy) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java index 9d7fa68807e..e86a5d16452 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java @@ -10,10 +10,12 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.jdisc.http.filter.DiscFilterRequest; import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase; import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.role.Role; import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.yolean.Exceptions; import java.security.Principal; @@ -45,8 +47,8 @@ public class SignatureFilter extends JsonSecurityRequestFilterBase { && request.getHeader("X-Authorization") != null) try { ApplicationId id = ApplicationId.fromSerializedForm(request.getHeader("X-Key-Id")); - boolean verified = controller.applications().get(id) - .flatMap(Instance::pemDeployKey) + boolean verified = controller.applications().getApplication(TenantAndApplicationId.from(id)) + .flatMap(Application::pemDeployKey) .map(key -> new RequestVerifier(key, controller.clock())) .map(verifier -> verifier.verify(Method.valueOf(request.getMethod()), request.getUri(), diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java index 988df9c6a66..a16ca5cb201 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java @@ -1,10 +1,14 @@ // 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.application.api.Endpoint; +import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.application.AssignedRotation; @@ -73,23 +77,24 @@ public class RotationRepository { * 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 instance The application requesting a rotation + * @param deploymentSpec The deployment spec for the application + * @param instance The instance requesting a rotation * @param lock Lock which must be acquired by the caller */ - public Rotation getOrAssignRotation(Instance instance, RotationLock lock) { - if (! instance.rotations().isEmpty()) { + public Rotation getOrAssignRotation(DeploymentSpec deploymentSpec, Instance instance, RotationLock lock) { + if ( ! instance.rotations().isEmpty()) { return allRotations.get(instance.rotations().get(0).rotationId()); } - if (instance.deploymentSpec().globalServiceId().isEmpty()) { + if (deploymentSpec.globalServiceId().isEmpty()) { throw new IllegalArgumentException("global-service-id is not set in deployment spec"); } - long productionZones = instance.deploymentSpec().zones().stream() - .filter(zone -> zone.deploysTo(Environment.prod)) - .count(); + long productionZones = deploymentSpec.zones().stream() + .filter(zone -> zone.deploysTo(Environment.prod)) + .count(); if (productionZones < 2) { throw new IllegalArgumentException("global-service-id is set but less than 2 prod zones are defined"); } - return findAvailableRotation(instance, lock); + return findAvailableRotation(instance.id(), lock); } /** @@ -99,27 +104,28 @@ public class RotationRepository { * If rotations are not assigned, a new assignment will be created taking new rotations from the repository. * This method supports both global-service-id as well as the new endpoints tag. * - * @param instance The application requesting rotations. + * @param deploymentSpec The deployment spec of the application + * @param instance The application requesting rotations * @param lock Lock which by acquired by the caller - * @return List of rotation assignments - either new or existing. + * @return List of rotation assignments - either new or existing */ - public List<AssignedRotation> getOrAssignRotations(Instance instance, RotationLock lock) { - if (instance.deploymentSpec().globalServiceId().isPresent() && ! instance.deploymentSpec().endpoints().isEmpty()) { + public List<AssignedRotation> getOrAssignRotations(DeploymentSpec deploymentSpec, Instance instance, RotationLock lock) { + if (deploymentSpec.globalServiceId().isPresent() && ! deploymentSpec.endpoints().isEmpty()) { throw new IllegalArgumentException("Cannot provision rotations with both global-service-id and 'endpoints'"); } // Support the older case of setting global-service-id - if (instance.deploymentSpec().globalServiceId().isPresent()) { - final var regions = instance.deploymentSpec().zones().stream() - .filter(zone -> zone.environment().isProduction()) - .flatMap(zone -> zone.region().stream()) - .collect(Collectors.toSet()); + if (deploymentSpec.globalServiceId().isPresent()) { + final var regions = deploymentSpec.zones().stream() + .filter(zone -> zone.environment().isProduction()) + .flatMap(zone -> zone.region().stream()) + .collect(Collectors.toSet()); - final var rotation = getOrAssignRotation(instance, lock); + final var rotation = getOrAssignRotation(deploymentSpec, instance, lock); return List.of( new AssignedRotation( - new ClusterSpec.Id(instance.deploymentSpec().globalServiceId().get()), + new ClusterSpec.Id(deploymentSpec.globalServiceId().get()), EndpointId.default_(), rotation.id(), regions @@ -127,20 +133,20 @@ public class RotationRepository { ); } - final Map<EndpointId, AssignedRotation> existingAssignments = existingEndpointAssignments(instance); - final Map<EndpointId, AssignedRotation> updatedAssignments = assignRotationsToEndpoints(instance, existingAssignments, lock); + final Map<EndpointId, AssignedRotation> existingAssignments = existingEndpointAssignments(deploymentSpec, instance); + final Map<EndpointId, AssignedRotation> updatedAssignments = assignRotationsToEndpoints(deploymentSpec, existingAssignments, lock); existingAssignments.putAll(updatedAssignments); return List.copyOf(existingAssignments.values()); } - private Map<EndpointId, AssignedRotation> assignRotationsToEndpoints(Instance instance, Map<EndpointId, AssignedRotation> existingAssignments, RotationLock lock) { + private Map<EndpointId, AssignedRotation> assignRotationsToEndpoints(DeploymentSpec deploymentSpec, Map<EndpointId, AssignedRotation> existingAssignments, RotationLock lock) { final var availableRotations = new ArrayList<>(availableRotations(lock).values()); - final var neededRotations = instance.deploymentSpec().endpoints().stream() - .filter(Predicate.not(endpoint -> existingAssignments.containsKey(EndpointId.of(endpoint.endpointId())))) - .collect(Collectors.toSet()); + final var neededRotations = deploymentSpec.endpoints().stream() + .filter(Predicate.not(endpoint -> existingAssignments.containsKey(EndpointId.of(endpoint.endpointId())))) + .collect(Collectors.toSet()); if (neededRotations.size() > availableRotations.size()) { throw new IllegalStateException("Hosted Vespa ran out of rotations, unable to assign rotation: need " + neededRotations.size() + ", have " + availableRotations.size()); @@ -165,17 +171,17 @@ public class RotationRepository { ); } - private Map<EndpointId, AssignedRotation> existingEndpointAssignments(Instance instance) { + private Map<EndpointId, AssignedRotation> existingEndpointAssignments(DeploymentSpec deploymentSpec, Instance instance) { // // Get the regions that has been configured for an endpoint. Empty set if the endpoint // is no longer mentioned in the configuration file. // final Function<EndpointId, Set<RegionName>> configuredRegionsForEndpoint = endpointId -> { - return instance.deploymentSpec().endpoints().stream() - .filter(endpoint -> endpointId.id().equals(endpoint.endpointId())) - .map(Endpoint::regions) - .findFirst() - .orElse(Set.of()); + return deploymentSpec.endpoints().stream() + .filter(endpoint -> endpointId.id().equals(endpoint.endpointId())) + .map(Endpoint::regions) + .findFirst() + .orElse(Set.of()); }; // @@ -210,8 +216,8 @@ public class RotationRepository { */ public Map<RotationId, Rotation> availableRotations(@SuppressWarnings("unused") RotationLock lock) { List<RotationId> assignedRotations = applications.asList().stream() - .filter(application -> ! application.rotations().isEmpty()) - .flatMap(application -> application.rotations().stream()) + .flatMap(application -> application.instances().values().stream()) + .flatMap(instance -> instance.rotations().stream()) .map(AssignedRotation::rotationId) .collect(Collectors.toList()); Map<RotationId, Rotation> unassignedRotations = new LinkedHashMap<>(this.allRotations); @@ -219,15 +225,15 @@ public class RotationRepository { return Collections.unmodifiableMap(unassignedRotations); } - private Rotation findAvailableRotation(Instance instance, RotationLock lock) { + private Rotation findAvailableRotation(ApplicationId id, RotationLock lock) { Map<RotationId, Rotation> availableRotations = availableRotations(lock); if (availableRotations.isEmpty()) { - throw new IllegalStateException("Unable to assign global rotation to " + instance.id() + throw new IllegalStateException("Unable to assign global rotation to " + id + " - no rotations available"); } // Return first available rotation RotationId rotation = availableRotations.keySet().iterator().next(); - log.info(String.format("Offering %s to application %s", rotation, instance.id())); + log.info(String.format("Offering %s to application %s", rotation, id)); return allRotations.get(rotation); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java index 6ed32feae28..77ccce873fe 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java @@ -2,7 +2,7 @@ package com.yahoo.vespa.hosted.controller.security; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.TenantName; -import com.yahoo.vespa.hosted.controller.Instance; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import java.util.List; @@ -33,10 +33,10 @@ public interface AccessControl { * @param tenantSpec specification for the tenant to update * @param credentials the credentials for the entity requesting the update * @param existing list of existing tenants, to check for conflicts - * @param instances list of applications this tenant already owns + * @param applications list of applications this tenant already owns * @return the updated tenant, for keeping */ - Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Instance> instances); + Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Application> applications); /** * Deletes access control for the given tenant. diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java index b53388d7aa6..33529c342a3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.security; import com.google.inject.Inject; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo; import com.yahoo.vespa.hosted.controller.api.integration.user.Roles; @@ -44,7 +45,7 @@ public class CloudAccessControl implements AccessControl { } @Override - public Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Instance> instances) { + public Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Application> applications) { throw new UnsupportedOperationException("Update is not supported here, as it would entail changing the tenant name."); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java index c6c66eab1b4..9dc6b86e4be 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java @@ -8,10 +8,11 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node; -import com.yahoo.vespa.hosted.controller.application.InstanceList; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.JobList; import com.yahoo.vespa.hosted.controller.application.SystemApplication; @@ -189,51 +190,50 @@ public class VersionStatus { } private static Collection<DeploymentStatistics> computeDeploymentStatistics(Set<Version> infrastructureVersions, - List<Instance> instances) { + List<Application> instances) { Map<Version, DeploymentStatistics> versionMap = new HashMap<>(); for (Version infrastructureVersion : infrastructureVersions) { versionMap.put(infrastructureVersion, DeploymentStatistics.empty(infrastructureVersion)); } - InstanceList instanceList = InstanceList.from(instances) - .hasProductionDeployment(); - for (Instance instance : instanceList.asList()) { - // Note that each version deployed on this application in production exists - // (ignore non-production versions) - for (Deployment deployment : instance.productionDeployments().values()) { - versionMap.computeIfAbsent(deployment.version(), DeploymentStatistics::empty); - } + for (Application application : ApplicationList.from(instances).withProductionDeployment().asList()) + for (Instance instance : application.instances().values()) { + // Note that each version deployed on this application in production exists + // (ignore non-production versions) + for (Deployment deployment : instance.productionDeployments().values()) { + versionMap.computeIfAbsent(deployment.version(), DeploymentStatistics::empty); + } - // List versions which have failing jobs, versions which are in production, and versions for which there are running deployment jobs - - // Failing versions - JobList.from(instance) - .failing() - .not().failingApplicationChange() - .not().failingBecause(outOfCapacity) - .mapToList(job -> job.lastCompleted().get().platform()) - .forEach(version -> versionMap - .put(version, versionMap.getOrDefault(version, DeploymentStatistics.empty(version)) - .withFailing(instance.id()))); - - // Succeeding versions - JobList.from(instance) - .lastSuccess().present() - .production() - .mapToList(job -> job.lastSuccess().get().platform()) - .forEach(version -> versionMap - .put(version, versionMap.getOrDefault(version, DeploymentStatistics.empty(version)) - .withProduction(instance.id()))); - - // Deploying versions - JobList.from(instance) - .upgrading() - .mapToList(job -> job.lastTriggered().get().platform()) - .forEach(version -> versionMap - .put(version, versionMap.getOrDefault(version, DeploymentStatistics.empty(version)) - .withDeploying(instance.id()))); - } + // List versions which have failing jobs, versions which are in production, and versions for which there are running deployment jobs + + // Failing versions + JobList.from(instance) + .failing() + .not().failingApplicationChange() + .not().failingBecause(outOfCapacity) + .mapToList(job -> job.lastCompleted().get().platform()) + .forEach(version -> versionMap + .put(version, versionMap.getOrDefault(version, DeploymentStatistics.empty(version)) + .withFailing(instance.id()))); + + // Succeeding versions + JobList.from(instance) + .lastSuccess().present() + .production() + .mapToList(job -> job.lastSuccess().get().platform()) + .forEach(version -> versionMap + .put(version, versionMap.getOrDefault(version, DeploymentStatistics.empty(version)) + .withProduction(instance.id()))); + + // Deploying versions + JobList.from(instance) + .upgrading() + .mapToList(job -> job.lastTriggered().get().platform()) + .forEach(version -> versionMap + .put(version, versionMap.getOrDefault(version, DeploymentStatistics.empty(version)) + .withDeploying(instance.id()))); + } return versionMap.values(); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java index aaf216f805b..e6a16a67af9 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java @@ -4,7 +4,9 @@ package com.yahoo.vespa.hosted.controller.versions; import com.google.common.collect.ImmutableSet; import com.yahoo.component.Version; import com.yahoo.config.provision.HostName; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.InstanceList; import java.time.Instant; @@ -49,12 +51,12 @@ public class VespaVersion implements Comparable<VespaVersion> { public static Confidence confidenceFrom(DeploymentStatistics statistics, Controller controller) { // 'production on this': All deployment jobs upgrading to this version have completed without failure - InstanceList productionOnThis = InstanceList.from(statistics.production(), controller.applications()) - .notUpgradingTo(statistics.version()) - .notFailing(); - InstanceList failingOnThis = InstanceList.from(statistics.failing(), controller.applications()); - InstanceList all = InstanceList.from(controller.applications().asList()) - .hasDeployment(); + ApplicationList productionOnThis = ApplicationList.from(statistics.production(), controller.applications()) + .notUpgradingTo(statistics.version()) + .notFailing(); + ApplicationList failingOnThis = ApplicationList.from(statistics.failing(), controller.applications()); + ApplicationList all = ApplicationList.from(controller.applications().asList()) + .withProductionDeployment(); // 'broken' if any Canary fails if ( ! failingOnThis.with(UpgradePolicy.canary).isEmpty()) @@ -162,10 +164,10 @@ public class VespaVersion implements Comparable<VespaVersion> { } private static boolean nonCanaryApplicationsBroken(Version version, - InstanceList failingOnThis, - InstanceList productionOnThis) { - InstanceList failingNonCanaries = failingOnThis.without(UpgradePolicy.canary).startedFailingOn(version); - InstanceList productionNonCanaries = productionOnThis.without(UpgradePolicy.canary); + ApplicationList failingOnThis, + ApplicationList productionOnThis) { + ApplicationList failingNonCanaries = failingOnThis.without(UpgradePolicy.canary).startedFailingOn(version); + ApplicationList productionNonCanaries = productionOnThis.without(UpgradePolicy.canary); if (productionNonCanaries.size() + failingNonCanaries.size() == 0) return false; 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 c5baa040ffc..5d82225c9d5 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java @@ -7,7 +7,8 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.AthenzDomain; +import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; @@ -15,6 +16,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.ZoneId; +import com.yahoo.slime.JsonFormat; import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.flags.InMemoryFlagSource; import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions; @@ -22,33 +24,44 @@ import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.certificates.ApplicationCertificate; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; +import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; +import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingEndpoint; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.AssignedRotation; +import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.Deployment; +import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; 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.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.deployment.BuildJob; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; -import com.yahoo.vespa.hosted.controller.persistence.ApplicationSerializer; +import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.persistence.InstanceSerializer; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; import com.yahoo.vespa.hosted.controller.persistence.OldMockCuratorDb; import com.yahoo.vespa.hosted.controller.rotation.RotationId; import com.yahoo.vespa.hosted.controller.rotation.RotationLock; +import com.yahoo.vespa.hosted.controller.rotation.RotationState; +import com.yahoo.vespa.hosted.controller.rotation.RotationStatus; import org.junit.Test; +import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; @@ -59,6 +72,7 @@ import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobTy import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.productionUsWest1; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.stagingTest; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.systemTest; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.time.temporal.ChronoUnit.MILLIS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -88,41 +102,41 @@ public class ControllerTest { // staging job - succeeding Version version1 = tester.defaultPlatformVersion(); Application app1 = tester.createApplication("app1", "tenant1", 1, 11L); - Instance instance = tester.instance(app1.id()); + Instance instance = tester.defaultInstance(app1.id()); tester.jobCompletion(component).application(app1).uploadArtifact(applicationPackage).submit(); assertEquals("Application version is known from completion of initial job", ApplicationVersion.from(BuildJob.defaultSourceRevision, BuildJob.defaultBuildNumber), - tester.controller().applications().require(app1.id()).change().application().get()); + tester.application(app1.id()).change().application().get()); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, systemTest); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, stagingTest); - assertEquals(4, applications.require(app1.id()).deploymentJobs().jobStatus().size()); + assertEquals(4, tester.defaultInstance(app1.id()).deploymentJobs().jobStatus().size()); - ApplicationVersion applicationVersion = tester.controller().applications().require(app1.id()).change().application().get(); + ApplicationVersion applicationVersion = tester.application(app1.id()).change().application().get(); assertFalse("Application version has been set during deployment", applicationVersion.isUnknown()); assertStatus(JobStatus.initial(stagingTest) .withTriggering(version1, applicationVersion, Optional.empty(),"", tester.clock().instant().truncatedTo(MILLIS)) - .withCompletion(42, Optional.empty(), tester.clock().instant().truncatedTo(MILLIS)), app1.id(), tester.controller()); + .withCompletion(42, Optional.empty(), tester.clock().instant().truncatedTo(MILLIS)), app1.id().defaultInstance(), tester.controller()); // Causes first deployment job to be triggered assertStatus(JobStatus.initial(productionUsWest1) - .withTriggering(version1, applicationVersion, Optional.empty(), "", tester.clock().instant().truncatedTo(MILLIS)), app1.id(), tester.controller()); + .withTriggering(version1, applicationVersion, Optional.empty(), "", tester.clock().instant().truncatedTo(MILLIS)), app1.id().defaultInstance(), tester.controller()); tester.clock().advance(Duration.ofSeconds(1)); // production job (failing) after deployment tester.deploy(productionUsWest1, instance.id(), applicationPackage); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), false, productionUsWest1); - assertEquals(4, applications.require(app1.id()).deploymentJobs().jobStatus().size()); + assertEquals(4, tester.defaultInstance(app1.id()).deploymentJobs().jobStatus().size()); JobStatus expectedJobStatus = JobStatus.initial(productionUsWest1) .withTriggering(version1, applicationVersion, Optional.empty(), "", tester.clock().instant().truncatedTo(MILLIS)) // Triggered first without application version info .withCompletion(42, Optional.of(JobError.unknown), tester.clock().instant().truncatedTo(MILLIS)) .withTriggering(version1, applicationVersion, - Optional.of(tester.instance(app1.id()).deployments().get(productionUsWest1.zone(main))), + Optional.of(tester.defaultInstance(app1.id()).deployments().get(productionUsWest1.zone(main))), "", tester.clock().instant().truncatedTo(MILLIS)); // Re-triggering (due to failure) has application version info - assertStatus(expectedJobStatus, app1.id(), tester.controller()); + assertStatus(expectedJobStatus, app1.id().defaultInstance(), tester.controller()); // Simulate restart tester.restartController(); @@ -130,40 +144,38 @@ public class ControllerTest { applications = tester.controller().applications(); assertNotNull(tester.controller().tenants().get(TenantName.from("tenant1"))); - assertNotNull(applications.get(ApplicationId.from(TenantName.from("tenant1"), - ApplicationName.from("application1"), - InstanceName.from("default")))); - assertEquals(4, applications.require(app1.id()).deploymentJobs().jobStatus().size()); + assertNotNull(tester.defaultInstance(app1.id())); + assertEquals(4, tester.defaultInstance(app1.id()).deploymentJobs().jobStatus().size()); tester.clock().advance(Duration.ofHours(1)); // system and staging test job - succeeding tester.jobCompletion(component).application(app1).nextBuildNumber().uploadArtifact(applicationPackage).submit(); - applicationVersion = tester.instance("app1").change().application().get(); + applicationVersion = tester.defaultInstance("app1").change().application().get(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, systemTest); assertStatus(JobStatus.initial(systemTest) - .withTriggering(version1, applicationVersion, Optional.of(tester.instance(app1.id()).deployments().get(productionUsWest1.zone(main))), "", tester.clock().instant().truncatedTo(MILLIS)) + .withTriggering(version1, applicationVersion, Optional.of(tester.defaultInstance(app1.id()).deployments().get(productionUsWest1.zone(main))), "", tester.clock().instant().truncatedTo(MILLIS)) .withCompletion(42, Optional.empty(), tester.clock().instant().truncatedTo(MILLIS)), - app1.id(), tester.controller()); + app1.id().defaultInstance(), tester.controller()); tester.clock().advance(Duration.ofHours(1)); // Stop retrying tester.jobCompletion(productionUsWest1).application(app1).unsuccessful().submit(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, stagingTest); // production job succeeding now expectedJobStatus = expectedJobStatus - .withTriggering(version1, applicationVersion, Optional.of(tester.instance(app1.id()).deployments().get(productionUsWest1.zone(main))), "", tester.clock().instant().truncatedTo(MILLIS)) + .withTriggering(version1, applicationVersion, Optional.of(tester.defaultInstance(app1.id()).deployments().get(productionUsWest1.zone(main))), "", tester.clock().instant().truncatedTo(MILLIS)) .withCompletion(42, Optional.empty(), tester.clock().instant().truncatedTo(MILLIS)); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsWest1); - assertStatus(expectedJobStatus, app1.id(), tester.controller()); + assertStatus(expectedJobStatus, app1.id().defaultInstance(), tester.controller()); // causes triggering of next production job assertStatus(JobStatus.initial(productionUsEast3) .withTriggering(version1, applicationVersion, Optional.empty(), "", tester.clock().instant().truncatedTo(MILLIS)), - app1.id(), tester.controller()); + app1.id().defaultInstance(), tester.controller()); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsEast3); - assertEquals(5, applications.get(app1.id()).get().deploymentJobs().jobStatus().size()); + assertEquals(5, tester.defaultInstance(app1.id()).deploymentJobs().jobStatus().size()); // Production zone for which there is no JobType is not allowed. applicationPackage = new ApplicationPackageBuilder() @@ -171,7 +183,7 @@ public class ControllerTest { .region("deep-space-9") .build(); try { - tester.controller().jobController().submit(app1.id(), BuildJob.defaultSourceRevision, "a@b", + tester.controller().jobController().submit(app1.id().defaultInstance(), BuildJob.defaultSourceRevision, "a@b", 2, applicationPackage, new byte[0]); fail("Expected exception due to illegal deployment spec."); } @@ -196,8 +208,8 @@ public class ControllerTest { e.getMessage()); } assertNotNull("Zone was not removed", - applications.require(app1.id()).deployments().get(productionUsWest1.zone(main))); - JobStatus jobStatus = applications.require(app1.id()).deploymentJobs().jobStatus().get(productionUsWest1); + tester.defaultInstance(app1.id()).deployments().get(productionUsWest1.zone(main))); + JobStatus jobStatus = tester.defaultInstance(app1.id()).deploymentJobs().jobStatus().get(productionUsWest1); assertNotNull("Deployment job was not removed", jobStatus); assertEquals(42, jobStatus.lastCompleted().get().id()); assertEquals("New change available", jobStatus.lastCompleted().get().reason()); @@ -212,8 +224,8 @@ public class ControllerTest { tester.jobCompletion(component).application(app1).nextBuildNumber(2).uploadArtifact(applicationPackage).submit(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, systemTest); assertNull("Zone was removed", - applications.require(app1.id()).deployments().get(productionUsWest1.zone(main))); - assertNull("Deployment job was removed", applications.require(app1.id()).deploymentJobs().jobStatus().get(productionUsWest1)); + tester.defaultInstance(app1.id()).deployments().get(productionUsWest1.zone(main))); + assertNull("Deployment job was removed", tester.defaultInstance(app1.id()).deploymentJobs().jobStatus().get(productionUsWest1)); } @Test @@ -229,11 +241,11 @@ public class ControllerTest { ApplicationVersion applicationVersion = ApplicationVersion.from(source, 101); runDeployment(tester, app.id(), applicationVersion, applicationPackage, source,101); assertEquals("Artifact is downloaded twice in staging and once for other zones", 5, - tester.controllerTester().serviceRegistry().artifactRepositoryMock().hits(app.id(), applicationVersion.id())); + tester.controllerTester().serviceRegistry().artifactRepositoryMock().hits(app.id().defaultInstance(), applicationVersion.id())); // Application is upgraded. This makes deployment orchestration pick the last successful application version in // zones which do not have permanent deployments, e.g. test and staging - runUpgrade(tester, app.id(), applicationVersion); + runUpgrade(tester, app.id().defaultInstance(), applicationVersion); } @Test @@ -296,13 +308,13 @@ public class ControllerTest { .build(); tester.deployCompletely(application, applicationPackage); - Collection<Deployment> deployments = tester.instance(application.id()).deployments().values(); + Collection<Deployment> deployments = tester.defaultInstance(application.id()).deployments().values(); assertFalse(deployments.isEmpty()); for (Deployment deployment : deployments) { assertEquals("Rotation names are passed to config server in " + deployment.zone(), Set.of("rotation-id-01", "app1--tenant1.global.vespa.oath.cloud"), - tester.configServer().rotationNames().get(new DeploymentId(application.id(), deployment.zone()))); + tester.configServer().rotationNames().get(new DeploymentId(application.id().defaultInstance(), deployment.zone()))); } tester.flushDnsRequests(); @@ -326,7 +338,7 @@ public class ControllerTest { .build(); tester.deployCompletely(application, applicationPackage); - Collection<Deployment> deployments = tester.instance(application.id()).deployments().values(); + Collection<Deployment> deployments = tester.defaultInstance(application.id()).deployments().values(); assertFalse(deployments.isEmpty()); for (Deployment deployment : deployments) { assertEquals("Rotation names are passed to config server in " + deployment.zone(), @@ -334,7 +346,7 @@ public class ControllerTest { "app1--tenant1.global.vespa.oath.cloud", "app1.tenant1.global.vespa.yahooapis.com", "app1--tenant1.global.vespa.yahooapis.com"), - tester.configServer().rotationNames().get(new DeploymentId(application.id(), deployment.zone()))); + tester.configServer().rotationNames().get(new DeploymentId(application.id().defaultInstance(), deployment.zone()))); } tester.flushDnsRequests(); assertEquals(3, tester.controllerTester().nameService().records().size()); @@ -370,7 +382,7 @@ public class ControllerTest { .build(); tester.deployCompletely(application, applicationPackage); - Collection<Deployment> deployments = tester.instance(application.id()).deployments().values(); + Collection<Deployment> deployments = tester.defaultInstance(application.id()).deployments().values(); assertFalse(deployments.isEmpty()); var notWest = Set.of( @@ -383,7 +395,7 @@ public class ControllerTest { for (Deployment deployment : deployments) { assertEquals("Rotation names are passed to config server in " + deployment.zone(), ZoneId.from("prod.us-west-1").equals(deployment.zone()) ? west : notWest, - tester.configServer().rotationNames().get(new DeploymentId(application.id(), deployment.zone()))); + tester.configServer().rotationNames().get(new DeploymentId(application.id().defaultInstance(), deployment.zone()))); } tester.flushDnsRequests(); @@ -425,12 +437,12 @@ public class ControllerTest { assertEquals( Set.of("rotation-id-01", "app1--tenant1.global.vespa.oath.cloud"), - tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-west-1"))) + tester.configServer().rotationNames().get(new DeploymentId(application.id().defaultInstance(), ZoneId.from("prod", "us-west-1"))) ); assertEquals( Set.of("rotation-id-01", "app1--tenant1.global.vespa.oath.cloud"), - tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-central-1"))) + tester.configServer().rotationNames().get(new DeploymentId(application.id().defaultInstance(), ZoneId.from("prod", "us-central-1"))) ); @@ -445,15 +457,15 @@ public class ControllerTest { assertEquals( Set.of("rotation-id-01", "app1--tenant1.global.vespa.oath.cloud"), - tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-west-1"))) + tester.configServer().rotationNames().get(new DeploymentId(application.id().defaultInstance(), ZoneId.from("prod", "us-west-1"))) ); assertEquals( Set.of(), - tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-central-1"))) + tester.configServer().rotationNames().get(new DeploymentId(application.id().defaultInstance(), ZoneId.from("prod", "us-central-1"))) ); - assertEquals(Set.of(RegionName.from("us-west-1")), tester.instance(application.id()).rotations().get(0).regions()); + assertEquals(Set.of(RegionName.from("us-west-1")), tester.defaultInstance(application.id()).rotations().get(0).regions()); } @Test @@ -480,12 +492,12 @@ public class ControllerTest { assertEquals( List.of(AssignedRotation.fromStrings("qrs", "default", "rotation-id-01", Set.of())), - tester.instance(application.id()).rotations() + tester.defaultInstance(application.id()).rotations() ); assertEquals( Set.of(), - tester.configServer().rotationNames().get(new DeploymentId(application.id(), ZoneId.from("prod", "us-west-1"))) + tester.configServer().rotationNames().get(new DeploymentId(application.id().defaultInstance(), ZoneId.from("prod", "us-west-1"))) ); } @@ -515,9 +527,9 @@ public class ControllerTest { .allow(ValidationId.deploymentRemoval) .build(); tester.jobCompletion(component).application(app1).nextBuildNumber().uploadArtifact(applicationPackage).submit(); - tester.deployAndNotify(tester.instance(app1.id()).id(), Optional.of(applicationPackage), true, systemTest); - tester.applications().deactivate(app1.id(), ZoneId.from(Environment.test, RegionName.from("us-east-1"))); - tester.applications().deactivate(app1.id(), ZoneId.from(Environment.staging, RegionName.from("us-east-3"))); + tester.deployAndNotify(tester.defaultInstance(app1.id()).id(), Optional.of(applicationPackage), true, systemTest); + tester.applications().deactivate(app1.id().defaultInstance(), ZoneId.from(Environment.test, RegionName.from("us-east-1"))); + tester.applications().deactivate(app1.id().defaultInstance(), ZoneId.from(Environment.staging, RegionName.from("us-east-3"))); tester.applications().deleteApplication(app1.id().tenant(), app1.id().application(), tester.controllerTester().credentialsFor(app1.id())); try (RotationLock lock = tester.applications().rotationRepository().lock()) { assertTrue("Rotation is unassigned", @@ -566,7 +578,7 @@ public class ControllerTest { .region("us-central-1") .build(); tester.deployCompletely(app1, applicationPackage); - assertEquals("rotation-id-02", tester.instance(app1.id()).rotations().get(0).rotationId().asString()); + assertEquals("rotation-id-02", tester.defaultInstance(app1.id()).rotations().get(0).rotationId().asString()); // DNS records are created for the newly assigned rotation assertEquals(2, tester.controllerTester().nameService().records().size()); @@ -602,18 +614,18 @@ public class ControllerTest { // Same options as used in our integration tests DeployOptions options = new DeployOptions(true, Optional.empty(), false, false); - tester.controller().applications().deploy(app.id(), zone, Optional.of(applicationPackage), options); + tester.controller().applications().deploy(app.id().defaultInstance(), zone, Optional.of(applicationPackage), options); assertTrue("Application deployed and activated", - tester.controllerTester().configServer().application(app.id(), zone).get().activated()); + tester.controllerTester().configServer().application(app.id().defaultInstance(), zone).get().activated()); assertTrue("No job status added", - tester.applications().require(app.id()).deploymentJobs().jobStatus().isEmpty()); + tester.applications().requireInstance(app.id().defaultInstance()).deploymentJobs().jobStatus().isEmpty()); Version seven = Version.fromString("7.2"); tester.upgradeSystem(seven); - tester.controller().applications().deploy(app.id(), zone, Optional.of(applicationPackage), options); - assertEquals(six, tester.instance(app.id()).deployments().get(zone).version()); + tester.controller().applications().deploy(app.id().defaultInstance(), zone, Optional.of(applicationPackage), options); + assertEquals(six, tester.defaultInstance(app.id()).deployments().get(zone).version()); } @Test @@ -629,12 +641,12 @@ public class ControllerTest { ZoneId zone = ZoneId.from("dev", "us-east-1"); // Deploy - tester.controller().applications().deploy(app.id(), zone, Optional.of(applicationPackage), DeployOptions.none()); + tester.controller().applications().deploy(app.id().defaultInstance(), zone, Optional.of(applicationPackage), DeployOptions.none()); assertTrue("Application deployed and activated", - tester.controllerTester().configServer().application(app.id(), zone).get().activated()); + tester.controllerTester().configServer().application(app.id().defaultInstance(), zone).get().activated()); assertTrue("No job status added", - tester.applications().require(app.id()).deploymentJobs().jobStatus().isEmpty()); - assertEquals("DeploymentSpec is not persisted", DeploymentSpec.empty, tester.applications().require(app.id()).deploymentSpec()); + tester.defaultInstance(app.id()).deploymentJobs().jobStatus().isEmpty()); + assertEquals("DeploymentSpec is not persisted", DeploymentSpec.empty, tester.application(app.id()).deploymentSpec()); } @Test @@ -650,8 +662,8 @@ public class ControllerTest { ApplicationVersion applicationVersion = ApplicationVersion.from(source, 101); runDeployment(tester, app.id(), applicationVersion, applicationPackage, source,101); - DeploymentId deployment1 = new DeploymentId(app.id(), ZoneId.from(Environment.prod, RegionName.from("us-west-1"))); - DeploymentId deployment2 = new DeploymentId(app.id(), ZoneId.from(Environment.prod, RegionName.from("us-east-3"))); + DeploymentId deployment1 = new DeploymentId(app.id().defaultInstance(), ZoneId.from(Environment.prod, RegionName.from("us-west-1"))); + DeploymentId deployment2 = new DeploymentId(app.id().defaultInstance(), ZoneId.from(Environment.prod, RegionName.from("us-east-3"))); assertFalse(tester.configServer().isSuspended(deployment1)); assertFalse(tester.configServer().isSuspended(deployment2)); tester.configServer().setSuspended(deployment1, true); @@ -671,33 +683,33 @@ public class ControllerTest { .build(); ZoneId zone = ZoneId.from("prod", "us-west-1"); - tester.controller().applications().deploy(app.id(), zone, Optional.of(applicationPackage), DeployOptions.none()); - tester.controller().applications().deactivate(app.id(), ZoneId.from(Environment.prod, RegionName.from("us-west-1"))); - tester.controller().applications().deactivate(app.id(), ZoneId.from(Environment.prod, RegionName.from("us-west-1"))); + tester.controller().applications().deploy(app.id().defaultInstance(), zone, Optional.of(applicationPackage), DeployOptions.none()); + tester.controller().applications().deactivate(app.id().defaultInstance(), ZoneId.from(Environment.prod, RegionName.from("us-west-1"))); + tester.controller().applications().deactivate(app.id().defaultInstance(), ZoneId.from(Environment.prod, RegionName.from("us-west-1"))); } @Test public void testDeployApplicationPackageWithApplicationDir() { - Application instance = tester.createApplication("app1", "tenant1", 1, 1L); + Application application = tester.createApplication("app1", "tenant1", 1, 1L); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-west-1") .build(true); - tester.deployCompletely(instance, applicationPackage); + tester.deployCompletely(application, applicationPackage); } @Test public void testDeployApplicationWithWarnings() { - Application instance = tester.createApplication("app1", "tenant1", 1, 1L); + Application application = tester.createApplication("app1", "tenant1", 1, 1L); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-west-1") .build(); ZoneId zone = ZoneId.from("prod", "us-west-1"); int warnings = 3; - tester.configServer().generateWarnings(new DeploymentId(instance.id(), zone), warnings); - tester.deployCompletely(instance, applicationPackage); - assertEquals(warnings, tester.applications().require(instance.id()).deployments().get(zone) + tester.configServer().generateWarnings(new DeploymentId(application.id().defaultInstance(), zone), warnings); + tester.deployCompletely(application, applicationPackage); + assertEquals(warnings, tester.applications().requireInstance(application.id().defaultInstance()).deployments().get(zone) .metrics().warnings().get(DeploymentMetrics.Warning.all).intValue()); } @@ -713,7 +725,7 @@ public class ControllerTest { .build(); // Deploy app1 in production tester.deployCompletely(app1, applicationPackage); - Instance instance1 = tester.instance(app1.id()); + Instance instance1 = tester.defaultInstance(app1.id()); var cert = certificate.apply(instance1); assertTrue("Provisions certificate in " + Environment.prod, cert.isPresent()); assertEquals(List.of( @@ -728,7 +740,7 @@ public class ControllerTest { "*.app1.tenant1.us-central-1.vespa.oath.cloud", "app1.tenant1.eu-west-1.vespa.oath.cloud", "*.app1.tenant1.eu-west-1.vespa.oath.cloud" - ), tester.controllerTester().serviceRegistry().applicationCertificateMock().dnsNamesOf(app1.id())); + ), tester.controllerTester().serviceRegistry().applicationCertificateMock().dnsNamesOf(app1.id().defaultInstance())); // Next deployment reuses certificate tester.deployCompletely(app1, applicationPackage, BuildJob.defaultBuildNumber + 1); @@ -736,13 +748,13 @@ public class ControllerTest { // Create app2 var app2 = tester.createApplication("app2", "tenant2", 3, 4L); - Instance instance2 = tester.instance(app2.id()); + Instance instance2 = tester.defaultInstance(app2.id()); ZoneId zone = ZoneId.from("dev", "us-east-1"); // Deploy app2 in dev - tester.controller().applications().deploy(app2.id(), zone, Optional.of(applicationPackage), DeployOptions.none()); + tester.controller().applications().deploy(app2.id().defaultInstance(), zone, Optional.of(applicationPackage), DeployOptions.none()); assertTrue("Application deployed and activated", - tester.controllerTester().configServer().application(app2.id(), zone).get().activated()); + tester.controllerTester().configServer().application(app2.id().defaultInstance(), zone).get().activated()); assertFalse("Does not provision certificate in " + Environment.dev, certificate.apply(instance2).isPresent()); } @@ -782,54 +794,107 @@ public class ControllerTest { @Test - public void testInstanceDataMigration() { + public void testInstanceDataMigration() throws IOException { + /* + Set up initial state, using old DB: + Create two instances of an application; the default will be the base. + + Read, modify and write the application using the new DB. + Verify results using both old and new DBs. + */ + + ApplicationPackage applicationPackage = new ApplicationPackageBuilder().allow(ValidationId.contentClusterRemoval) + .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service")) + .endpoint("endpoint", "container", "us-east-1") + .region("us-east-1") + .build(); + ApplicationId defaultId = ApplicationId.from("t1", "a1", "default"); + Instance old1 = new Instance(defaultId, + Instant.ofEpochMilli(123), + applicationPackage.deploymentSpec(), + applicationPackage.validationOverrides(), + List.of(new Deployment(ZoneId.from("prod", "us-east-1"), + ApplicationVersion.from(new SourceRevision("repo", "branch", "commit"), 3), + Version.fromString("7.8.9"), + Instant.ofEpochMilli(321))), + new DeploymentJobs(OptionalLong.of(72), + List.of(new JobStatus(JobType.productionAwsUsEast1a, + Optional.empty(), + Optional.of(new JobStatus.JobRun(32, + Version.fromString("7.8.9"), + ApplicationVersion.unknown, + Optional.empty(), + Optional.empty(), + "make the job great again", + Instant.ofEpochMilli(200))), + Optional.empty(), + Optional.empty(), + Optional.empty(), + OptionalLong.empty())), + Optional.of(IssueId.from("issue")), + true), + Change.of(Version.fromString("9")), + Change.empty(), + Optional.of(IssueId.from("tissue")), + Optional.of(User.from("user")), + OptionalInt.of(3), + new ApplicationMetrics(2, 3), + Optional.of("key"), + List.of(AssignedRotation.fromStrings("container", "endpoint", "rot13", List.of("us-east-1"))), + RotationStatus.from(Map.of(new RotationId("rot13"), Map.of(ZoneId.from("prod", "us-east-1"), RotationState.in)))); + + Instance old2 = new Instance(ApplicationId.from("t1", "a1", "i1"), + Instant.ofEpochMilli(400)); + + + InstanceSerializer instanceSerializer = new InstanceSerializer(); + String old1Serialized = new String(JsonFormat.toJsonBytes(instanceSerializer.toSlime(old1)), UTF_8); + MockCuratorDb newDb = new MockCuratorDb(); OldMockCuratorDb oldDb = new OldMockCuratorDb(newDb.curator()); - Instance instance1 = new Instance(ApplicationId.from("tenant1", "application1", "instance1"), Instant.ofEpochMilli(1)); - Instance instance2 = new Instance(ApplicationId.from("tenant2", "application2", "instance2"), Instant.ofEpochMilli(2)); - - oldDb.writeInstance(instance1); - newDb.writeInstance(instance2); - - assertEquals(instance1, oldDb.readInstance(instance1.id()).orElseThrow()); - assertEquals(instance1, newDb.readInstance(instance1.id()).orElseThrow()); - - assertEquals(instance2, oldDb.readInstance(instance2.id()).orElseThrow()); - assertEquals(instance2, newDb.readInstance(instance2.id()).orElseThrow()); - - assertEquals(List.of(instance1, instance2), oldDb.readInstances()); - assertEquals(List.of(instance1, instance2), newDb.readInstances()); - - instance1 = new Instance(instance1.id(), Instant.ofEpochMilli(3)); - oldDb.writeInstance(instance1); - assertEquals(instance1, oldDb.readInstance(instance1.id()).orElseThrow()); - assertEquals(instance1, newDb.readInstance(instance1.id()).orElseThrow()); - - instance2 = new Instance(instance2.id(), Instant.ofEpochMilli(4)); - newDb.writeInstance(instance2); - assertEquals(instance2, oldDb.readInstance(instance2.id()).orElseThrow()); - assertEquals(instance2, newDb.readInstance(instance2.id()).orElseThrow()); - - Application application = newDb.readApplication(instance1.id()).orElseThrow(); - assertEquals(new ApplicationSerializer().toSlime(application).toString(), - new InstanceSerializer().toSlime(instance1).toString()); - - oldDb.removeInstance(instance1.id()); - newDb.removeInstance(instance2.id()); - assertEquals(List.of(), oldDb.readInstances()); - assertEquals(List.of(), newDb.readInstances()); + oldDb.writeInstance(old1); + oldDb.writeInstance(old2); + + Application application = newDb.readApplication(TenantAndApplicationId.from("t1", "a1")).orElseThrow(); + Instance new1 = application.legacy(InstanceName.defaultName()); + String new1Serialized = new String(JsonFormat.toJsonBytes(instanceSerializer.toSlime(new1)), UTF_8); + assertEquals(old1Serialized, new1Serialized); + + LockedApplication locked = new LockedApplication(application, newDb.lock(application.id())); + newDb.writeApplication(locked.with(new ApplicationMetrics(8, 9)).get()); + Instance mod1 = oldDb.readInstance(old1.id()).orElseThrow(); + Instance mod2 = oldDb.readInstance(old2.id()).orElseThrow(); + + old1 = old1.with(new ApplicationMetrics(8, 9)); + old1Serialized = new String(JsonFormat.toJsonBytes(instanceSerializer.toSlime(old1)), UTF_8); + String mod1Serialized = new String(JsonFormat.toJsonBytes(instanceSerializer.toSlime(mod1)), UTF_8); + assertEquals(old1Serialized, mod1Serialized); + + assertEquals(old1.createdAt(), mod2.createdAt()); + assertEquals(old1.change(), mod2.change()); + assertEquals(old1.outstandingChange(), mod2.outstandingChange()); + assertEquals(old1.deploymentSpec(), mod2.deploymentSpec()); + assertEquals(old2.deployments(), mod2.deployments()); + assertEquals(old2.deploymentJobs().jobStatus(), mod2.deploymentJobs().jobStatus()); + + newDb.removeApplication(old1.id()); + assertEquals(1, newDb.readApplication(application.id()).orElseThrow().instances().size()); + newDb.removeApplication(old2.id()); + assertTrue(newDb.readApplication(application.id()).isEmpty()); + assertTrue(oldDb.readInstance(old1.id()).isEmpty()); + assertTrue(oldDb.readInstance(old2.id()).isEmpty()); } private void runUpgrade(DeploymentTester tester, ApplicationId application, ApplicationVersion version) { Version next = Version.fromString("6.2"); tester.upgradeSystem(next); - runDeployment(tester, tester.applications().require(application), version, Optional.of(next), Optional.empty()); + runDeployment(tester, tester.applications().requireInstance(application), version, Optional.of(next), Optional.empty()); } - private void runDeployment(DeploymentTester tester, ApplicationId id, ApplicationVersion version, + private void runDeployment(DeploymentTester tester, TenantAndApplicationId id, ApplicationVersion version, ApplicationPackage applicationPackage, SourceRevision sourceRevision, long buildNumber) { - Instance instance = tester.applications().require(id); + Instance instance = tester.defaultInstance(id); tester.jobCompletion(component) .application(tester.application(id)) .buildNumber(buildNumber) @@ -839,13 +904,13 @@ public class ControllerTest { ApplicationVersion change = ApplicationVersion.from(sourceRevision, buildNumber); assertEquals(change.id(), tester.controller().applications() - .require(id) + .requireApplication(id) .change().application().get().id()); runDeployment(tester, instance, version, Optional.empty(), Optional.of(applicationPackage)); } private void assertStatus(JobStatus expectedStatus, ApplicationId id, Controller controller) { - Instance app = controller.applications().get(id).get(); + Instance app = controller.applications().getInstance(id).get(); JobStatus existingStatus = app.deploymentJobs().jobStatus().get(expectedStatus.type()); assertNotNull("Status of type " + expectedStatus.type() + " is present", existingStatus); assertEquals(expectedStatus, existingStatus); @@ -880,7 +945,7 @@ public class ControllerTest { assertStatus(expected, app.id(), tester.controller()); // Verify deployed version - app = tester.controller().applications().require(app.id()); + app = tester.controller().applications().requireInstance(app.id()); for (Deployment deployment : app.productionDeployments().values()) { assertEquals(version, deployment.applicationVersion()); upgrade.ifPresent(v -> assertEquals(v, deployment.version())); 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 10757902f75..82d9690f7d7 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 @@ -14,7 +14,6 @@ import com.yahoo.vespa.athenz.api.AthenzPrincipal; import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.athenz.api.OktaAccessToken; import com.yahoo.vespa.curator.Lock; -import com.yahoo.vespa.curator.mock.MockCurator; import com.yahoo.vespa.flags.InMemoryFlagSource; import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; @@ -46,6 +45,7 @@ import com.yahoo.vespa.hosted.controller.versions.VersionStatus; import com.yahoo.vespa.hosted.rotation.config.RotationsConfig; import java.util.Arrays; +import java.util.List; import java.util.Optional; import java.util.OptionalLong; import java.util.function.Consumer; @@ -54,6 +54,7 @@ import java.util.logging.Handler; import java.util.logging.Logger; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** * Convenience methods for controller tests. @@ -182,13 +183,12 @@ public final class ControllerTester { } /** Create application from slime */ - public Instance createApplication(Slime slime) { - InstanceSerializer serializer = new InstanceSerializer(); - Instance instance = serializer.fromSlime(slime); - try (Lock lock = controller().applications().lock(TenantAndApplicationId.from(instance.id()))) { - controller().applications().store(new LockedInstance(instance, lock)); + public void createApplication(Slime slime) { + Instance instance = new InstanceSerializer().fromSlime(slime); + Application application = Application.aggregate(List.of(instance)).get(); + try (Lock lock = controller().applications().lock(application.id())) { + controller().applications().store(new LockedApplication(application, lock)); } - return instance; } public ZoneId toZone(Environment environment) { @@ -208,7 +208,7 @@ public final class ControllerTester { return domain; } - public Optional<AthenzDomain> domainOf(ApplicationId id) { + public Optional<AthenzDomain> domainOf(TenantAndApplicationId id) { Tenant tenant = controller().tenants().require(id.tenant()); return tenant.type() == Tenant.Type.athenz ? Optional.of(((AthenzTenant) tenant).domain()) : Optional.empty(); } @@ -236,7 +236,7 @@ public final class ControllerTester { return createTenant(tenantName, domainName, propertyId, Optional.empty()); } - public Optional<Credentials> credentialsFor(ApplicationId id) { + public Optional<Credentials> credentialsFor(TenantAndApplicationId id) { return domainOf(id).map(domain -> new AthenzCredentials(new AthenzPrincipal(new AthenzUser("user")), domain, new OktaAccessToken("okta-token"))); @@ -244,10 +244,12 @@ public final class ControllerTester { public Application createApplication(TenantName tenant, String applicationName, String instanceName, long projectId) { ApplicationId applicationId = ApplicationId.from(tenant.value(), applicationName, instanceName); - controller().applications().createApplication(applicationId, credentialsFor(applicationId)); - controller().applications().lockApplicationOrThrow(applicationId, application -> + controller().applications().createApplication(applicationId, credentialsFor(TenantAndApplicationId.from(applicationId))); + controller().applications().lockApplicationOrThrow(TenantAndApplicationId.from(applicationId), application -> controller().applications().store(application.withProjectId(OptionalLong.of(projectId)))); - return controller().applications().requireApplication(applicationId); + Application application = controller().applications().requireApplication(TenantAndApplicationId.from(applicationId)); + assertTrue(application.projectId().isPresent()); + return application; } public void deploy(ApplicationId id, ZoneId zone) { @@ -274,12 +276,7 @@ public final class ControllerTester { } public Supplier<Instance> application(ApplicationId application) { - return () -> controller().applications().require(application); - } - - /** Used by ApplicationSerializerTest to avoid breaking encapsulation. Should not be used by anything else */ - public static LockedInstance writable(Instance instance) { - return new LockedInstance(instance, new Lock("/test", new MockCurator())); + return () -> controller().applications().requireInstance(application); } private static Controller createController(CuratorDb curator, RotationsConfig rotationsConfig, diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BuildJob.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BuildJob.java index afe4a1dba1f..ce733d60a77 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BuildJob.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BuildJob.java @@ -52,11 +52,10 @@ public class BuildJob { } public BuildJob application(Application application) { - this.applicationId = application.id(); - if (application.deploymentJobs().projectId().isPresent()) { - this.projectId = application.deploymentJobs().projectId().getAsLong(); - } - return this; + if (application.projectId().isPresent()) + this.projectId = application.projectId().getAsLong(); + + return application(application.id().defaultInstance()); } public BuildJob application(ApplicationId applicationId) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java index c1d8c32f516..5dc6fb183a2 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.deployment; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.test.ManualClock; @@ -19,6 +20,7 @@ import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.SystemApplication; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock; import com.yahoo.vespa.hosted.controller.maintenance.JobControl; import com.yahoo.vespa.hosted.controller.maintenance.NameServiceDispatcher; @@ -113,16 +115,20 @@ public class DeploymentTester { public ConfigServerMock configServer() { return tester.serviceRegistry().configServerMock(); } - public Application application(ApplicationId id) { + public Application application(TenantAndApplicationId id) { return controller().applications().requireApplication(id); } - public Instance instance(String name) { + public Instance defaultInstance(String name) { return instance(ApplicationId.from("tenant1", name, "default")); } + public Instance defaultInstance(TenantAndApplicationId application) { + return controller().applications().requireApplication(application).require(InstanceName.defaultName()); + } + public Instance instance(ApplicationId application) { - return controller().applications().require(application); + return controller().applications().requireInstance(application); } /** Re-compute and write version status */ @@ -236,22 +242,22 @@ public class DeploymentTester { } private void completeDeployment(Application application, ApplicationPackage applicationPackage, Optional<JobType> failOnJob) { - assertTrue(application.id() + " has pending changes to deploy", applications().require(application.id()).change().hasTargets()); + assertTrue(application.id() + " has pending changes to deploy", applications().requireApplication(application.id()).change().hasTargets()); DeploymentSteps steps = controller().applications().deploymentTrigger().steps(applicationPackage.deploymentSpec()); List<JobType> jobs = steps.jobs(); // TODO jonmv: Change to list instances here. for (JobType job : jobs) { boolean failJob = failOnJob.map(j -> j.equals(job)).orElse(false); - deployAndNotify(application.id(), applicationPackage, ! failJob, job); + deployAndNotify(application.id().defaultInstance(), applicationPackage, ! failJob, job); if (failJob) { break; } } if (failOnJob.isPresent()) { - assertTrue(applications().require(application.id()).change().hasTargets()); - assertTrue(applications().require(application.id()).deploymentJobs().hasFailures()); + assertTrue(applications().requireApplication(application.id()).change().hasTargets()); + assertTrue(defaultInstance(application.id()).deploymentJobs().hasFailures()); } else { - assertFalse(applications().require(application.id()).change().hasTargets()); + assertFalse(applications().requireApplication(application.id()).change().hasTargets()); } if (updateDnsAutomatically) { flushDnsRequests(); @@ -263,8 +269,8 @@ public class DeploymentTester { } public void completeUpgrade(Application application, Version version, ApplicationPackage applicationPackage) { - assertTrue(application + " has a change", applications().require(application.id()).change().hasTargets()); - assertEquals(Change.of(version), applications().require(application.id()).change()); + assertTrue(application + " has a change", applications().requireApplication(application.id()).change().hasTargets()); + assertEquals(Change.of(version), applications().requireApplication(application.id()).change()); completeDeployment(application, applicationPackage, Optional.empty()); } @@ -277,8 +283,8 @@ public class DeploymentTester { } private void completeUpgradeWithError(Application application, Version version, ApplicationPackage applicationPackage, Optional<JobType> failOnJob) { - assertTrue(applications().require(application.id()).change().hasTargets()); - assertEquals(Change.of(version), applications().require(application.id()).change()); + assertTrue(applications().requireApplication(application.id()).change().hasTargets()); + assertEquals(Change.of(version), applications().requireApplication(application.id()).change()); completeDeployment(application, applicationPackage, failOnJob); } @@ -327,7 +333,7 @@ public class DeploymentTester { } public Optional<JobStatus.JobRun> firstFailing(Instance instance, JobType job) { - return tester.controller().applications().require(instance.id()) + return tester.controller().applications().requireInstance(instance.id()) .deploymentJobs().jobStatus().get(job).firstFailing(); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java index 6951271fbaf..5441ed6aec0 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.deployment; import com.yahoo.component.Version; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.slime.Slime; @@ -19,9 +20,11 @@ import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockBuildService; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.maintenance.JobControl; import com.yahoo.vespa.hosted.controller.maintenance.ReadyJobsTrigger; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; +import org.junit.Ignore; import org.junit.Test; import java.nio.file.Files; @@ -69,7 +72,7 @@ public class DeploymentTriggerTest { @Test public void testTriggerFailing() { Application app = tester.createApplication("app1", "tenant1", 1, 1L); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .upgradePolicy("default") .environment(Environment.prod) @@ -90,14 +93,14 @@ public class DeploymentTriggerTest { tester.upgradeSystem(version); // staging-test times out and is retried - tester.buildService().remove(buildJob(app.id(), stagingTest)); + tester.buildService().remove(buildJob(app.id().defaultInstance(), stagingTest)); tester.readyJobTrigger().maintain(); assertEquals("Retried dead job", 2, tester.buildService().jobs().size()); - tester.assertRunning(stagingTest, app.id()); + tester.assertRunning(stagingTest, app.id().defaultInstance()); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, stagingTest); // system-test is now the only running job -- production jobs haven't started yet, since it is unfinished. - tester.assertRunning(systemTest, app.id()); + tester.assertRunning(systemTest, app.id().defaultInstance()); assertEquals(1, tester.buildService().jobs().size()); // system-test fails and is retried @@ -105,15 +108,17 @@ public class DeploymentTriggerTest { assertEquals("Job is retried on failure", 1, tester.buildService().jobs().size()); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.systemTest); - tester.assertRunning(productionUsWest1, app.id()); + tester.assertRunning(productionUsWest1, app.id().defaultInstance()); } @Test + @Ignore + // TODO jonmv: Re-enable, but changed, when instances are orchestrated. public void testIndependentInstances() { Application app1 = tester.createApplication("instance1", "app", "tenant", 1, 1L); Application app2 = tester.createApplication("instance2", "app", "tenant", 2, 1L); - Instance instance1 = tester.instance(app1.id()); - Instance instance2 = tester.instance(app2.id()); + Instance instance1 = tester.instance(app1.id().instance(InstanceName.from("instance1"))); + Instance instance2 = tester.instance(app2.id().instance(InstanceName.from("instance2"))); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .upgradePolicy("default") .environment(Environment.prod) @@ -124,12 +129,12 @@ public class DeploymentTriggerTest { tester.upgradeSystem(version); // Deploy completely once - tester.jobCompletion(component).application(app1).uploadArtifact(applicationPackage).submit(); + tester.jobCompletion(component).application(app1).application(instance1.id()).uploadArtifact(applicationPackage).submit(); tester.deployAndNotify(instance1.id(), Optional.of(applicationPackage), true, JobType.systemTest); tester.deployAndNotify(instance1.id(), Optional.of(applicationPackage), true, JobType.stagingTest); tester.deployAndNotify(instance1.id(), Optional.of(applicationPackage), true, JobType.productionUsWest1); - tester.jobCompletion(component).application(app2).uploadArtifact(applicationPackage).submit(); + tester.jobCompletion(component).application(app2).application(instance2.id()).uploadArtifact(applicationPackage).submit(); tester.deployAndNotify(instance2.id(), Optional.of(applicationPackage), true, JobType.systemTest); tester.deployAndNotify(instance2.id(), Optional.of(applicationPackage), true, JobType.stagingTest); tester.deployAndNotify(instance2.id(), Optional.of(applicationPackage), true, JobType.productionUsWest1); @@ -143,8 +148,8 @@ public class DeploymentTriggerTest { tester.deployAndNotify(instance1.id(), Optional.of(applicationPackage), true, JobType.stagingTest); tester.deployAndNotify(instance1.id(), Optional.of(applicationPackage), true, JobType.productionUsWest1); - Version instance1Version = tester.instance(app1.id()).deployments().get(JobType.productionUsWest1.zone(main)).version(); - Version instance2Version = tester.instance(app2.id()).deployments().get(JobType.productionUsWest1.zone(main)).version(); + Version instance1Version = tester.defaultInstance(app1.id()).deployments().get(JobType.productionUsWest1.zone(main)).version(); + Version instance2Version = tester.defaultInstance(app2.id()).deployments().get(JobType.productionUsWest1.zone(main)).version(); assertEquals(newVersion, instance1Version); assertEquals(version, instance2Version); @@ -156,7 +161,7 @@ public class DeploymentTriggerTest { DeploymentTester tester = iTester.tester(); Instance instance = iTester.instance(); - Application application = tester.application(instance.id()); + Application application = tester.application(TenantAndApplicationId.from(instance.id())); ApplicationPackage applicationPackage = InternalDeploymentTester.applicationPackage; tester.jobCompletion(component).application(application).uploadArtifact(applicationPackage).submit(); @@ -168,7 +173,7 @@ public class DeploymentTriggerTest { tester.jobCompletion(component).application(application).nextBuildNumber().uploadArtifact(applicationPackage).submit(); tester.assertRunning(productionUsCentral1, instance.id()); - tester.applications().deploymentTrigger().cancelChange(instance.id(), ALL); + tester.applications().deploymentTrigger().cancelChange(application.id(), ALL); tester.deployAndNotify(instance.id(), Optional.empty(), false, systemTest); tester.deployAndNotify(instance.id(), Optional.empty(), false, stagingTest); tester.deployAndNotify(instance.id(), Optional.empty(), false, productionUsCentral1); @@ -218,7 +223,7 @@ public class DeploymentTriggerTest { TenantName tenant = tester.controllerTester().createTenant("tenant1", "domain1", 1L); MockBuildService mockBuildService = tester.buildService(); Application application = tester.controllerTester().createApplication(tenant, "app1", "default", 1L); - Instance instance = tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-east-3") @@ -242,7 +247,7 @@ public class DeploymentTriggerTest { public void deploymentsSpecWithDelays() { MockBuildService mockBuildService = tester.buildService(); Application application = tester.createApplication("app1", "tenant1", 1, 1L); - Instance instance = tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) @@ -269,13 +274,13 @@ public class DeploymentTriggerTest { // 30 seconds later, the first jobs may trigger. assertEquals(1, mockBuildService.jobs().size()); - tester.assertRunning(productionUsWest1, application.id()); + tester.assertRunning(productionUsWest1, instance.id()); // 3 minutes pass, delayed trigger does nothing as us-west-1 is still in progress tester.clock().advance(Duration.ofMinutes(3)); tester.deploymentTrigger().triggerReadyJobs(); assertEquals(1, mockBuildService.jobs().size()); - tester.assertRunning(productionUsWest1, application.id()); + tester.assertRunning(productionUsWest1, instance.id()); // us-west-1 completes tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsWest1); @@ -304,7 +309,7 @@ public class DeploymentTriggerTest { @Test public void deploymentSpecWithParallelDeployments() { Application application = tester.createApplication("app1", "tenant1", 1, 1L); - Instance instance = tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) @@ -326,8 +331,8 @@ public class DeploymentTriggerTest { // Deploys in two regions in parallel assertEquals(2, tester.buildService().jobs().size()); - tester.assertRunning(productionUsEast3, application.id()); - tester.assertRunning(productionUsWest1, application.id()); + tester.assertRunning(productionUsEast3, instance.id()); + tester.assertRunning(productionUsWest1, instance.id()); tester.deploy(JobType.productionUsWest1, instance.id(), applicationPackage, false); tester.jobCompletion(JobType.productionUsWest1).application(application).submit(); @@ -346,7 +351,7 @@ public class DeploymentTriggerTest { public void testNoOtherChangesDuringSuspension() { // Application is deployed in 3 regions: Application application = tester.createApplication("app1", "tenant1", 1, 1L); - Instance instance = tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-central-1") @@ -363,7 +368,7 @@ public class DeploymentTriggerTest { tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.productionUsEast3); // The first production zone is suspended: - tester.configServer().setSuspended(new DeploymentId(application.id(), JobType.productionUsCentral1.zone(tester.controller().system())), true); + tester.configServer().setSuspended(new DeploymentId(instance.id(), JobType.productionUsCentral1.zone(tester.controller().system())), true); // A new change needs to be pushed out, but should not go beyond the suspended zone: tester.jobCompletion(component) @@ -376,11 +381,11 @@ public class DeploymentTriggerTest { tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.stagingTest); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.productionUsCentral1); tester.triggerUntilQuiescence(); - tester.assertNotRunning(JobType.productionUsEast3, application.id()); - tester.assertNotRunning(JobType.productionUsWest1, application.id()); + tester.assertNotRunning(JobType.productionUsEast3, instance.id()); + tester.assertNotRunning(JobType.productionUsWest1, instance.id()); // The zone is unsuspended so jobs start: - tester.configServer().setSuspended(new DeploymentId(application.id(), JobType.productionUsCentral1.zone(tester.controller().system())), false); + tester.configServer().setSuspended(new DeploymentId(instance.id(), JobType.productionUsCentral1.zone(tester.controller().system())), false); tester.triggerUntilQuiescence(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.productionUsWest1); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.productionUsEast3); @@ -394,7 +399,7 @@ public class DeploymentTriggerTest { .build(); Application app = tester.createApplication("app1", "tenant1", 1, 11L); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); tester.jobCompletion(component).application(app).uploadArtifact(applicationPackage).submit(); // Test environments pass @@ -405,13 +410,13 @@ public class DeploymentTriggerTest { tester.deploy(JobType.productionUsWest1, instance.id(), applicationPackage); tester.jobCompletion(JobType.productionUsWest1).application(app).submit(); assertTrue("Change is present as not all jobs are complete", - tester.applications().require(app.id()).change().hasTargets()); + tester.applications().requireApplication(app.id()).change().hasTargets()); // All jobs complete tester.deploy(JobType.productionUsEast3, instance.id(), applicationPackage); tester.jobCompletion(JobType.productionUsEast3).application(app).submit(); assertFalse("Change has been deployed", - tester.applications().require(app.id()).change().hasTargets()); + tester.applications().requireApplication(app.id()).change().hasTargets()); } @Test @@ -419,7 +424,7 @@ public class DeploymentTriggerTest { TenantName tenant = tester.controllerTester().createTenant("tenant1", "domain1", 1L); MockBuildService mockBuildService = tester.buildService(); Application application = tester.controllerTester().createApplication(tenant, "app1", "default", 1L); - Instance instance = tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); ApplicationPackage previousApplicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-east-3") @@ -468,7 +473,7 @@ public class DeploymentTriggerTest { .region("us-east-3"); Application app = tester.createAndDeploy("app1", 1, applicationPackageBuilder.build()); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); tester.clock().advance(Duration.ofHours(1)); // --------------- Enter block window: 18:30 @@ -489,12 +494,12 @@ public class DeploymentTriggerTest { .sourceRevision(new SourceRevision("repository1", "master", "cafed00d")) .uploadArtifact(changedApplication) .submit(); - assertTrue(tester.applications().require(app.id()).outstandingChange().hasTargets()); + assertTrue(tester.applications().requireApplication(app.id()).outstandingChange().hasTargets()); tester.deployAndNotify(instance.id(), Optional.of(changedApplication), true, systemTest); tester.deployAndNotify(instance.id(), Optional.of(changedApplication), true, stagingTest); tester.outstandingChangeDeployer().run(); - assertTrue(tester.applications().require(app.id()).outstandingChange().hasTargets()); + assertTrue(tester.applications().requireApplication(app.id()).outstandingChange().hasTargets()); readyJobsTrigger.run(); assertEquals(emptyList(), tester.buildService().jobs()); @@ -502,10 +507,10 @@ public class DeploymentTriggerTest { tester.clock().advance(Duration.ofHours(2)); // ---------------- Exit block window: 20:30 tester.outstandingChangeDeployer().run(); - assertFalse(tester.applications().require(app.id()).outstandingChange().hasTargets()); + assertFalse(tester.applications().requireApplication(app.id()).outstandingChange().hasTargets()); tester.deploymentTrigger().triggerReadyJobs(); // Schedules staging test for the blocked production job(s) - assertEquals(singletonList(buildJob(app.id(), productionUsWest1)), tester.buildService().jobs()); + assertEquals(singletonList(buildJob(instance.id(), productionUsWest1)), tester.buildService().jobs()); } @Test @@ -518,7 +523,7 @@ public class DeploymentTriggerTest { .region("us-east-3") .build(); Application application = tester.createAndDeploy("app1", 1, applicationPackage); - Instance instance = tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); // Application on (6.1, 1.0.42) Version v1 = Version.fromString("6.1"); @@ -535,9 +540,9 @@ public class DeploymentTriggerTest { tester.clock().advance(Duration.ofHours(1)); tester.outstandingChangeDeployer().run(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsWest1); - assertEquals(BuildJob.defaultBuildNumber, tester.instance(application.id()).deploymentJobs().jobStatus() + assertEquals(BuildJob.defaultBuildNumber, tester.defaultInstance(application.id()).deploymentJobs().jobStatus() .get(productionUsWest1).lastSuccess().get().application().buildNumber().getAsLong()); - assertEquals((BuildJob.defaultBuildNumber + 1), tester.instance(application.id()).outstandingChange().application().get().buildNumber().getAsLong()); + assertEquals((BuildJob.defaultBuildNumber + 1), tester.defaultInstance(application.id()).outstandingChange().application().get().buildNumber().getAsLong()); tester.readyJobTrigger().maintain(); // Platform upgrade keeps rolling, since it has already deployed in a production zone, and tests for the new revision have also started. @@ -546,55 +551,55 @@ public class DeploymentTriggerTest { assertEquals(2, tester.buildService().jobs().size()); // Upgrade is done, and oustanding change rolls out when block window ends. - assertEquals(Change.empty(), tester.instance(application.id()).change()); - assertFalse(tester.instance(application.id()).change().hasTargets()); - assertTrue(tester.instance(application.id()).outstandingChange().hasTargets()); + assertEquals(Change.empty(), tester.defaultInstance(application.id()).change()); + assertFalse(tester.defaultInstance(application.id()).change().hasTargets()); + assertTrue(tester.defaultInstance(application.id()).outstandingChange().hasTargets()); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, stagingTest); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, systemTest); tester.clock().advance(Duration.ofHours(1)); tester.outstandingChangeDeployer().run(); - assertTrue(tester.instance(application.id()).change().hasTargets()); - assertFalse(tester.instance(application.id()).outstandingChange().hasTargets()); + assertTrue(tester.defaultInstance(application.id()).change().hasTargets()); + assertFalse(tester.defaultInstance(application.id()).outstandingChange().hasTargets()); tester.readyJobTrigger().run(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsWest1); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsEast3); - assertFalse(tester.instance(application.id()).change().hasTargets()); - assertFalse(tester.instance(application.id()).outstandingChange().hasTargets()); + assertFalse(tester.defaultInstance(application.id()).change().hasTargets()); + assertFalse(tester.defaultInstance(application.id()).outstandingChange().hasTargets()); } @Test public void testJobPause() { Application app = tester.createAndDeploy("app", 3, "default"); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); tester.upgradeSystem(new Version("9.8.7")); - tester.applications().deploymentTrigger().pauseJob(app.id(), productionUsWest1, tester.clock().instant().plus(Duration.ofSeconds(1))); - tester.applications().deploymentTrigger().pauseJob(app.id(), productionUsEast3, tester.clock().instant().plus(Duration.ofSeconds(3))); + tester.applications().deploymentTrigger().pauseJob(instance.id(), productionUsWest1, tester.clock().instant().plus(Duration.ofSeconds(1))); + tester.applications().deploymentTrigger().pauseJob(instance.id(), productionUsEast3, tester.clock().instant().plus(Duration.ofSeconds(3))); // us-west-1 does not trigger when paused. tester.deployAndNotify(instance.id(), Optional.empty(), true, systemTest); tester.deployAndNotify(instance.id(), Optional.empty(), true, stagingTest); - tester.assertNotRunning(productionUsWest1, app.id()); + tester.assertNotRunning(productionUsWest1, instance.id()); // us-west-1 triggers when no longer paused, but does not retry when paused again. tester.clock().advance(Duration.ofMillis(1500)); tester.readyJobTrigger().run(); - tester.assertRunning(productionUsWest1, app.id()); - tester.applications().deploymentTrigger().pauseJob(app.id(), productionUsWest1, tester.clock().instant().plus(Duration.ofSeconds(1))); + tester.assertRunning(productionUsWest1, instance.id()); + tester.applications().deploymentTrigger().pauseJob(instance.id(), productionUsWest1, tester.clock().instant().plus(Duration.ofSeconds(1))); tester.deployAndNotify(instance.id(), Optional.empty(), false, productionUsWest1); - tester.assertNotRunning(productionUsWest1, app.id()); + tester.assertNotRunning(productionUsWest1, instance.id()); tester.clock().advance(Duration.ofMillis(1000)); tester.readyJobTrigger().run(); tester.deployAndNotify(instance.id(), Optional.empty(), true, productionUsWest1); // us-east-3 does not automatically trigger when paused, but does when forced. - tester.assertNotRunning(productionUsEast3, app.id()); - tester.deploymentTrigger().forceTrigger(app.id(), productionUsEast3, "mrTrigger"); - tester.assertRunning(productionUsEast3, app.id()); - assertFalse(tester.instance(app.id()).deploymentJobs().jobStatus().get(productionUsEast3).pausedUntil().isPresent()); + tester.assertNotRunning(productionUsEast3, instance.id()); + tester.deploymentTrigger().forceTrigger(instance.id(), productionUsEast3, "mrTrigger"); + tester.assertRunning(productionUsEast3, instance.id()); + assertFalse(tester.instance(instance.id()).deploymentJobs().jobStatus().get(productionUsEast3).pausedUntil().isPresent()); } @Test @@ -609,15 +614,15 @@ public class DeploymentTriggerTest { }); assertEquals(0, tester.buildService().jobs().size()); readyJobsTrigger.run(); - tester.assertRunning(systemTest, app.id()); - tester.assertRunning(stagingTest, app.id()); + tester.assertRunning(systemTest, app.id().defaultInstance()); + tester.assertRunning(stagingTest, app.id().defaultInstance()); } @Test public void applicationVersionIsNotDowngraded() { Application application = tester.createApplication("app1", "tenant1", 1, 1L); - Instance instance = tester.instance(application.id()); - Supplier<Instance> app = () -> tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); + Supplier<Instance> app = () -> tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-central-1") @@ -667,9 +672,9 @@ public class DeploymentTriggerTest { public void stepIsCompletePreciselyWhenItShouldBe() { Application application1 = tester.createApplication("app1", "tenant1", 1, 1L); Application application2 = tester.createApplication("app2", "tenant2", 2, 2L); - Instance instance1 = tester.instance(application1.id()); - Instance instance2 = tester.instance(application2.id()); - Supplier<Instance> app1 = () -> tester.instance(application1.id()); + Instance instance1 = tester.defaultInstance(application1.id()); + Instance instance2 = tester.defaultInstance(application2.id()); + Supplier<Instance> app1 = () -> tester.defaultInstance(application1.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-central-1") @@ -723,7 +728,7 @@ public class DeploymentTriggerTest { tester.deployAndNotify(instance1.id(), Optional.of(applicationPackage), true, systemTest); tester.deployAndNotify(instance1.id(), Optional.of(applicationPackage), true, stagingTest); - tester.assertRunning(productionUsCentral1, application1.id()); + tester.assertRunning(productionUsCentral1, instance1.id()); assertEquals(version2, app1.get().deployments().get(productionUsCentral1.zone(main)).version()); assertEquals(42, app1.get().deployments().get(productionUsCentral1.zone(main)).applicationVersion().buildNumber().getAsLong()); assertNotEquals(triggered, app1.get().deploymentJobs().jobStatus().get(productionUsCentral1).lastTriggered().get().at()); @@ -738,7 +743,7 @@ public class DeploymentTriggerTest { tester.clock().advance(Duration.ofHours(2).plus(Duration.ofSeconds(1))); // Enough time for retry tester.readyJobTrigger().maintain(); // Failing job is not retried as change has been deployed - tester.assertNotRunning(productionUsCentral1, application1.id()); + tester.assertNotRunning(productionUsCentral1, instance1.id()); // Last job has a different deployment target, so tests need to run again. tester.deployAndNotify(instance1.id(), Optional.empty(), true, systemTest); @@ -751,8 +756,8 @@ public class DeploymentTriggerTest { @Test public void eachDeployTargetIsTested() { Application application = tester.createApplication("app1", "tenant1", 1, 1L); - Instance instance = tester.instance(application.id()); - Supplier<Instance> app = () -> tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); + Supplier<Instance> app = () -> tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .parallel("eu-west-1", "us-east-3") @@ -808,8 +813,8 @@ public class DeploymentTriggerTest { @Test public void eachDifferentUpgradeCombinationIsTested() { Application application = tester.createApplication("app1", "tenant1", 1, 1L); - Instance instance = tester.instance(application.id()); - Supplier<Instance> app = () -> tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); + Supplier<Instance> app = () -> tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-central-1") @@ -849,7 +854,7 @@ public class DeploymentTriggerTest { @Test public void retriesFailingJobs() { Application application = tester.createApplication("app1", "tenant1", 1, 1L); - Instance instance = tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-central-1") @@ -867,34 +872,34 @@ public class DeploymentTriggerTest { tester.clock().advance(Duration.ofSeconds(59)); tester.jobCompletion(systemTest).application(application).unsuccessful().submit(); tester.readyJobTrigger().maintain(); - tester.assertRunning(systemTest, application.id()); + tester.assertRunning(systemTest, instance.id()); // Stops immediate retry after failing for 1 minute tester.clock().advance(Duration.ofSeconds(1)); tester.jobCompletion(systemTest).application(application).unsuccessful().submit(); tester.readyJobTrigger().maintain(); - tester.assertNotRunning(systemTest, application.id()); + tester.assertNotRunning(systemTest, instance.id()); // Retries after 10 minutes since previous completion as we failed within the last hour tester.clock().advance(Duration.ofMinutes(10).plus(Duration.ofSeconds(1))); tester.readyJobTrigger().maintain(); - tester.assertRunning(systemTest, application.id()); + tester.assertRunning(systemTest, instance.id()); // Retries less frequently after 1 hour of failure tester.clock().advance(Duration.ofMinutes(50)); tester.jobCompletion(systemTest).application(application).unsuccessful().submit(); tester.readyJobTrigger().maintain(); - tester.assertNotRunning(systemTest, application.id()); + tester.assertNotRunning(systemTest, instance.id()); // Retries after two hours pass since last completion tester.clock().advance(Duration.ofHours(2).plus(Duration.ofSeconds(1))); tester.readyJobTrigger().maintain(); - tester.assertRunning(systemTest, application.id()); + tester.assertRunning(systemTest, instance.id()); // Still fails and is not retried tester.jobCompletion(systemTest).application(application).unsuccessful().submit(); tester.readyJobTrigger().maintain(); - tester.assertNotRunning(systemTest, application.id()); + tester.assertNotRunning(systemTest, instance.id()); // Another application change is deployed and fixes system-test. Change is triggered immediately as target changes tester.jobCompletion(component).application(application).nextBuildNumber(2).uploadArtifact(applicationPackage).submit(); @@ -915,7 +920,7 @@ public class DeploymentTriggerTest { tester.upgradeSystem(version); Application app = tester.createApplication("app1", "tenant1", 1, 11L); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); tester.jobCompletion(component).application(app).uploadArtifact(applicationPackage).submit(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.systemTest); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.stagingTest); @@ -936,7 +941,7 @@ public class DeploymentTriggerTest { tester.clock().advance(Duration.ofSeconds(1)); // Advance time so that we can detect jobs in progress tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), false, JobType.productionUsEast3); assertEquals("Production job is retried", 1, tester.buildService().jobs().size()); - assertEquals("Application has pending upgrade to " + version, version, tester.instance(app.id()).change().platform().get()); + assertEquals("Application has pending upgrade to " + version, version, tester.defaultInstance(app.id()).change().platform().get()); // Another version is released, which cancels any pending upgrades to lower versions version = Version.fromString("6.4"); @@ -944,7 +949,7 @@ public class DeploymentTriggerTest { tester.upgrader().maintain(); tester.jobCompletion(JobType.productionUsEast3).application(app).unsuccessful().submit(); assertEquals("Application starts upgrading to new version", 2, tester.buildService().jobs().size()); - assertEquals("Application has pending upgrade to " + version, version, tester.instance(app.id()).change().platform().get()); + assertEquals("Application has pending upgrade to " + version, version, tester.defaultInstance(app.id()).change().platform().get()); // Failure re-deployer did not retry failing job for prod.us-east-3, since it no longer had an available change assertFalse("Job is not retried", tester.buildService().jobs().stream() @@ -956,12 +961,12 @@ public class DeploymentTriggerTest { // Production job fails again, and is retried tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), false, JobType.productionUsEast3); - assertEquals("Job is retried", Collections.singletonList(buildJob(app.id(), productionUsEast3)), tester.buildService().jobs()); + assertEquals("Job is retried", Collections.singletonList(buildJob(instance.id(), productionUsEast3)), tester.buildService().jobs()); // Production job finally succeeds tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.productionUsEast3); assertTrue("All jobs consumed", tester.buildService().jobs().isEmpty()); - assertFalse("No failures", tester.instance(app.id()).deploymentJobs().hasFailures()); + assertFalse("No failures", tester.defaultInstance(app.id()).deploymentJobs().hasFailures()); } @Test @@ -975,7 +980,7 @@ public class DeploymentTriggerTest { tester.upgradeSystem(version); Application app = tester.createApplication("app1", "tenant1", 1, 11L); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); tester.jobCompletion(component).application(app).uploadArtifact(applicationPackage).submit(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.systemTest); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.stagingTest); @@ -987,7 +992,7 @@ public class DeploymentTriggerTest { assertEquals(version, tester.controller().versionStatus().systemVersion().get().versionNumber()); tester.upgrader().maintain(); tester.readyJobTrigger().maintain(); - assertEquals("Application has pending upgrade to " + version, version, tester.instance(app.id()).change().platform().get()); + assertEquals("Application has pending upgrade to " + version, version, tester.defaultInstance(app.id()).change().platform().get()); // system-test fails and is left with a retry tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), false, JobType.systemTest); @@ -997,20 +1002,20 @@ public class DeploymentTriggerTest { tester.upgradeSystem(version); assertEquals(version, tester.controller().versionStatus().systemVersion().get().versionNumber()); - tester.buildService().remove(buildJob(app.id(), systemTest)); + tester.buildService().remove(buildJob(instance.id(), systemTest)); tester.upgrader().maintain(); tester.readyJobTrigger().maintain(); - assertEquals("Application has pending upgrade to " + version, version, tester.instance(app.id()).change().platform().get()); + assertEquals("Application has pending upgrade to " + version, version, tester.defaultInstance(app.id()).change().platform().get()); // Cancellation of outdated version and triggering on a new version is done by the upgrader. - assertEquals(version, tester.instance(app.id()).deploymentJobs().jobStatus().get(systemTest).lastTriggered().get().platform()); + assertEquals(version, tester.defaultInstance(app.id()).deploymentJobs().jobStatus().get(systemTest).lastTriggered().get().platform()); } @Test public void testUpdatesFailingJobStatus() { // Setup application Application app = tester.createApplication("app1", "foo", 1, 1L); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-west-1") @@ -1081,7 +1086,7 @@ public class DeploymentTriggerTest { Version version1 = tester.controller().versionStatus().systemVersion().get().versionNumber(); Application app1 = tester.createApplication("application1", "tenant1", 1, 1L); - Instance instance1 = tester.instance(app1.id()); + Instance instance1 = tester.defaultInstance(app1.id()); // First deployment: An application change tester.jobCompletion(component).application(app1).uploadArtifact(applicationPackage).submit(); @@ -1097,7 +1102,7 @@ public class DeploymentTriggerTest { // Unexpected deployment tester.deploy(productionUsWest1, instance1.id(), applicationPackage); // instances are immutable, so any change to one, including deployment changes, would give rise to a new instance. - assertEquals("Unexpected deployment is ignored", instance1, tester.instance(app1.id())); + assertEquals("Unexpected deployment is ignored", instance1, tester.defaultInstance(app1.id())); // Application change after a new system version, and a region added Version version2 = new Version(version1.getMajor(), version1.getMinor() + 1); @@ -1151,9 +1156,9 @@ public class DeploymentTriggerTest { Application app1 = tester.createApplication("app1", "tenant1", project1, 1L); Application app2 = tester.createApplication("app2", "tenant2", project2, 1L); Application app3 = tester.createApplication("app3", "tenant3", project3, 1L); - Instance instance1 = tester.instance(app1.id()); - Instance instance2 = tester.instance(app2.id()); - Instance instance3 = tester.instance(app3.id()); + Instance instance1 = tester.defaultInstance(app1.id()); + Instance instance2 = tester.defaultInstance(app2.id()); + Instance instance3 = tester.defaultInstance(app3.id()); MockBuildService mockBuildService = tester.buildService(); // all applications: system-test completes successfully with some time in between, to determine trigger order. @@ -1178,23 +1183,23 @@ public class DeploymentTriggerTest { assertJobsInOrder(jobs, tester.buildService().jobs()); tester.triggerUntilQuiescence(); - jobs.add(buildJob(app2.id(), stagingTest)); - jobs.add(buildJob(app1.id(), stagingTest)); - jobs.add(buildJob(app3.id(), stagingTest)); + jobs.add(buildJob(instance2.id(), stagingTest)); + jobs.add(buildJob(instance1.id(), stagingTest)); + jobs.add(buildJob(instance3.id(), stagingTest)); assertJobsInOrder(jobs, tester.buildService().jobs()); // Remove the jobs for app1 and app2, and then let app3 fail with outOfCapacity. // All three jobs are now eligible, but the one for app3 should trigger first as an outOfCapacity-retry. - tester.buildService().remove(buildJob(app1.id(), stagingTest)); - tester.buildService().remove(buildJob(app2.id(), stagingTest)); - jobs.remove(buildJob(app1.id(), stagingTest)); - jobs.remove(buildJob(app2.id(), stagingTest)); + tester.buildService().remove(buildJob(instance1.id(), stagingTest)); + tester.buildService().remove(buildJob(instance2.id(), stagingTest)); + jobs.remove(buildJob(instance1.id(), stagingTest)); + jobs.remove(buildJob(instance2.id(), stagingTest)); tester.jobCompletion(stagingTest).application(app3).error(DeploymentJobs.JobError.outOfCapacity).submit(); assertJobsInOrder(jobs, tester.buildService().jobs()); tester.triggerUntilQuiescence(); - jobs.add(buildJob(app2.id(), stagingTest)); - jobs.add(buildJob(app1.id(), stagingTest)); + jobs.add(buildJob(instance2.id(), stagingTest)); + jobs.add(buildJob(instance1.id(), stagingTest)); assertJobsInOrder(jobs, tester.buildService().jobs()); // Finish deployment for apps 2 and 3, then release a new version, leaving only app1 with an application upgrade. @@ -1209,29 +1214,29 @@ public class DeploymentTriggerTest { tester.buildService().clear(); tester.jobCompletion(component).application(app1).nextBuildNumber().uploadArtifact(applicationPackage).submit(); jobs.clear(); - jobs.add(buildJob(app1.id(), stagingTest)); - jobs.add(buildJob(app1.id(), systemTest)); + jobs.add(buildJob(instance1.id(), stagingTest)); + jobs.add(buildJob(instance1.id(), systemTest)); // Tests for app1 trigger before the others since it carries an application upgrade. assertJobsInOrder(jobs, tester.buildService().jobs()); // Let the test jobs start, remove everything expect system test for app3, which fails with outOfCapacity again. tester.triggerUntilQuiescence(); - tester.buildService().remove(buildJob(app1.id(), systemTest)); - tester.buildService().remove(buildJob(app2.id(), systemTest)); - tester.buildService().remove(buildJob(app1.id(), stagingTest)); - tester.buildService().remove(buildJob(app2.id(), stagingTest)); - tester.buildService().remove(buildJob(app3.id(), stagingTest)); + tester.buildService().remove(buildJob(instance1.id(), systemTest)); + tester.buildService().remove(buildJob(instance2.id(), systemTest)); + tester.buildService().remove(buildJob(instance1.id(), stagingTest)); + tester.buildService().remove(buildJob(instance2.id(), stagingTest)); + tester.buildService().remove(buildJob(instance3.id(), stagingTest)); tester.jobCompletion(systemTest).application(app3).error(DeploymentJobs.JobError.outOfCapacity).submit(); jobs.clear(); - jobs.add(buildJob(app1.id(), stagingTest)); - jobs.add(buildJob(app3.id(), systemTest)); + jobs.add(buildJob(instance1.id(), stagingTest)); + jobs.add(buildJob(instance3.id(), systemTest)); assertJobsInOrder(jobs, tester.buildService().jobs()); tester.triggerUntilQuiescence(); - jobs.add(buildJob(app2.id(), stagingTest)); - jobs.add(buildJob(app1.id(), systemTest)); - jobs.add(buildJob(app3.id(), stagingTest)); - jobs.add(buildJob(app2.id(), systemTest)); + jobs.add(buildJob(instance2.id(), stagingTest)); + jobs.add(buildJob(instance1.id(), systemTest)); + jobs.add(buildJob(instance3.id(), stagingTest)); + jobs.add(buildJob(instance2.id(), systemTest)); assertJobsInOrder(jobs, tester.buildService().jobs()); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java index c0618937bdf..bcc6fcfda44 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java @@ -5,6 +5,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.AthenzService; +import com.yahoo.config.provision.InstanceName; import com.yahoo.log.LogLevel; import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; @@ -25,6 +26,7 @@ import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock; import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGeneratorMock; import com.yahoo.vespa.hosted.controller.maintenance.JobControl; @@ -69,8 +71,9 @@ public class InternalDeploymentTester { .emailAddress("b@a") .trust(generateCertificate()) .build(); - public static final ApplicationId appId = ApplicationId.from("tenant", "application", "default"); - public static final TesterId testerId = TesterId.of(appId); + public static final TenantAndApplicationId appId = TenantAndApplicationId.from("tenant", "application"); + public static final ApplicationId instanceId = appId.defaultInstance(); + public static final TesterId testerId = TesterId.of(instanceId); public static final String athenzDomain = "domain"; private final DeploymentTester tester; @@ -88,12 +91,12 @@ public class InternalDeploymentTester { public ApplicationController applications() { return tester.applications(); } public ManualClock clock() { return tester.clock(); } public Application application() { return tester.application(appId); } - public Instance instance() { return tester.instance(appId); } + public Instance instance() { return tester.instance(instanceId); } public InternalDeploymentTester() { tester = new DeploymentTester(); - tester.controllerTester().createApplication(tester.controllerTester().createTenant(appId.tenant().value(), athenzDomain, 1L), - appId.application().value(), + tester.controllerTester().createApplication(tester.controllerTester().createTenant(instanceId.tenant().value(), athenzDomain, 1L), + instanceId.application().value(), "default", 1); jobs = tester.controller().jobController(); @@ -117,7 +120,7 @@ public class InternalDeploymentTester { * Submits a new application, and returns the version of the new submission. */ public ApplicationVersion newSubmission() { - return jobs.submit(appId, BuildJob.defaultSourceRevision, "a@b", 2, + return jobs.submit(instanceId, BuildJob.defaultSourceRevision, "a@b", 2, tester.controller().system().isPublic() ? publicCdApplicationPackage : applicationPackage, new byte[0]); } @@ -179,13 +182,13 @@ public class InternalDeploymentTester { assertTrue(instance().productionDeployments().values().stream() .allMatch(deployment -> deployment.version().equals(version))); assertTrue(tester.configServer().nodeRepository() - .list(JobType.productionAwsUsEast1a.zone(tester.controller().system()), appId).stream() + .list(JobType.productionAwsUsEast1a.zone(tester.controller().system()), instanceId).stream() .allMatch(node -> node.currentVersion().equals(version))); assertTrue(tester.configServer().nodeRepository() - .list(JobType.productionUsEast3.zone(tester.controller().system()), appId).stream() + .list(JobType.productionUsEast3.zone(tester.controller().system()), instanceId).stream() .allMatch(node -> node.currentVersion().equals(version))); assertTrue(tester.configServer().nodeRepository() - .list(JobType.productionUsEast3.zone(tester.controller().system()), appId).stream() + .list(JobType.productionUsEast3.zone(tester.controller().system()), instanceId).stream() .allMatch(node -> node.currentVersion().equals(version))); assertFalse(instance().change().hasTargets()); } @@ -203,15 +206,15 @@ public class InternalDeploymentTester { assertNotSame(aborted, run.status()); ZoneId zone = type.zone(tester.controller().system()); - DeploymentId deployment = new DeploymentId(appId, zone); + DeploymentId deployment = new DeploymentId(instanceId, zone); // First steps are always deployments. runner.run(); if (type == JobType.stagingTest) { // Do the initial deployment and installation of the real application. assertEquals(unfinished, jobs.active(run.id()).get().steps().get(Step.installInitialReal)); - tester.configServer().convergeServices(appId, zone); - setEndpoints(appId, zone); + tester.configServer().convergeServices(instanceId, zone); + setEndpoints(instanceId, zone); run.versions().sourcePlatform().ifPresent(version -> tester.configServer().nodeRepository().doUpgrade(deployment, Optional.empty(), version)); runner.run(); assertEquals(Step.Status.succeeded, jobs.active(run.id()).get().steps().get(Step.installInitialReal)); @@ -221,12 +224,12 @@ public class InternalDeploymentTester { tester.configServer().nodeRepository().doUpgrade(deployment, Optional.empty(), run.versions().targetPlatform()); runner.run(); assertEquals(unfinished, jobs.active(run.id()).get().steps().get(Step.installReal)); - tester.configServer().convergeServices(appId, zone); + tester.configServer().convergeServices(instanceId, zone); runner.run(); if ( ! (run.versions().sourceApplication().isPresent() && type.isProduction()) && type != JobType.stagingTest) { assertEquals(unfinished, jobs.active(run.id()).get().steps().get(Step.installReal)); - setEndpoints(appId, zone); + setEndpoints(instanceId, zone); } runner.run(); if (type.environment().isManuallyDeployed()) { @@ -266,9 +269,9 @@ public class InternalDeploymentTester { public RunId startSystemTestTests() { RunId id = newRun(JobType.systemTest); runner.run(); - tester.configServer().convergeServices(appId, JobType.systemTest.zone(tester.controller().system())); + tester.configServer().convergeServices(instanceId, JobType.systemTest.zone(tester.controller().system())); tester.configServer().convergeServices(testerId.id(), JobType.systemTest.zone(tester.controller().system())); - setEndpoints(appId, JobType.systemTest.zone(tester.controller().system())); + setEndpoints(instanceId, JobType.systemTest.zone(tester.controller().system())); setEndpoints(testerId.id(), JobType.systemTest.zone(tester.controller().system())); runner.run(); assertEquals(unfinished, jobs.run(id).get().steps().get(Step.endTests)); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java index 6813e7f1206..034fd8a64b5 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java @@ -50,7 +50,7 @@ import java.util.concurrent.Future; import static com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type.error; import static com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type.info; import static com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type.warning; -import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.appId; +import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.instanceId; import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.applicationPackage; import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.publicCdApplicationPackage; import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.testerId; @@ -91,9 +91,9 @@ public class InternalStepRunnerTest { public void canSwitchFromScrewdriverAndBackAgain() { // Deploys a default application package with default build number. tester.tester().deployCompletely(tester.application(), InternalDeploymentTester.applicationPackage); - tester.setEndpoints(appId, JobType.productionUsCentral1.zone(system())); - tester.setEndpoints(appId, JobType.productionUsWest1.zone(system())); - tester.setEndpoints(appId, JobType.productionUsEast3.zone(system())); + tester.setEndpoints(instanceId, JobType.productionUsCentral1.zone(system())); + tester.setEndpoints(instanceId, JobType.productionUsWest1.zone(system())); + tester.setEndpoints(instanceId, JobType.productionUsEast3.zone(system())); // Let application have an ongoing upgrade when it switches (but kill the jobs, as the tester assumes they aren't running). tester.tester().upgradeSystem(new Version("7.1")); @@ -104,7 +104,7 @@ public class InternalStepRunnerTest { tester.deployNewPlatform(new Version("7.2")); - tester.jobs().unregister(appId); + tester.jobs().unregister(instanceId); try { tester.tester().deployCompletely(tester.application(), InternalDeploymentTester.applicationPackage, BuildJob.defaultBuildNumber + 1); throw new IllegalStateException("Component job should get even again with build numbers to produce a change."); @@ -133,8 +133,8 @@ public class InternalStepRunnerTest { tester.runner().run(); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.installInitialReal)); - tester.setEndpoints(appId, JobType.stagingTest.zone(system())); - tester.configServer().convergeServices(appId, JobType.stagingTest.zone(system())); + tester.setEndpoints(instanceId, JobType.stagingTest.zone(system())); + tester.configServer().convergeServices(instanceId, JobType.stagingTest.zone(system())); tester.configServer().setConfigChangeActions(new ConfigChangeActions(Collections.emptyList(), singletonList(new RefeedAction("Refeed", false, @@ -151,7 +151,7 @@ public class InternalStepRunnerTest { public void restartsServicesAndWaitsForRestartAndReboot() { RunId id = tester.newRun(JobType.productionUsCentral1); ZoneId zone = id.type().zone(system()); - HostName host = tester.configServer().hostFor(appId, zone); + HostName host = tester.configServer().hostFor(instanceId, zone); tester.setEndpoints(testerId.id(), JobType.productionUsCentral1.zone(system())); tester.runner().run(); @@ -168,11 +168,11 @@ public class InternalStepRunnerTest { tester.runner().run(); assertEquals(succeeded, tester.jobs().run(id).get().steps().get(Step.deployReal)); - tester.configServer().convergeServices(appId, zone); + tester.configServer().convergeServices(instanceId, zone); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.installReal)); - tester.configServer().nodeRepository().doRestart(new DeploymentId(appId, zone), Optional.of(host)); - tester.configServer().nodeRepository().requestReboot(new DeploymentId(appId, zone), Optional.of(host)); + tester.configServer().nodeRepository().doRestart(new DeploymentId(instanceId, zone), Optional.of(host)); + tester.configServer().nodeRepository().requestReboot(new DeploymentId(instanceId, zone), Optional.of(host)); tester.runner().run(); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.installReal)); @@ -187,67 +187,67 @@ public class InternalStepRunnerTest { // Tester fails to show up for staging tests, and the real deployment for system tests. tester.setEndpoints(testerId.id(), JobType.systemTest.zone(system())); - tester.setEndpoints(appId, JobType.stagingTest.zone(system())); + tester.setEndpoints(instanceId, JobType.stagingTest.zone(system())); tester.runner().run(); - tester.configServer().convergeServices(appId, JobType.stagingTest.zone(system())); + tester.configServer().convergeServices(instanceId, JobType.stagingTest.zone(system())); tester.runner().run(); - tester.configServer().convergeServices(appId, JobType.systemTest.zone(system())); + tester.configServer().convergeServices(instanceId, JobType.systemTest.zone(system())); tester.configServer().convergeServices(testerId.id(), JobType.systemTest.zone(system())); - tester.configServer().convergeServices(appId, JobType.stagingTest.zone(system())); + tester.configServer().convergeServices(instanceId, JobType.stagingTest.zone(system())); tester.configServer().convergeServices(testerId.id(), JobType.stagingTest.zone(system())); tester.runner().run(); tester.clock().advance(InternalStepRunner.endpointTimeout.plus(Duration.ofSeconds(1))); tester.runner().run(); - assertEquals(failed, tester.jobs().last(appId, JobType.systemTest).get().steps().get(Step.installReal)); - assertEquals(failed, tester.jobs().last(appId, JobType.stagingTest).get().steps().get(Step.installTester)); + assertEquals(failed, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installReal)); + assertEquals(failed, tester.jobs().last(instanceId, JobType.stagingTest).get().steps().get(Step.installTester)); } @Test public void installationFailsIfDeploymentExpires() { tester.newRun(JobType.systemTest); tester.runner().run(); - tester.configServer().convergeServices(appId, JobType.systemTest.zone(system())); - tester.setEndpoints(appId, JobType.systemTest.zone(system())); + tester.configServer().convergeServices(instanceId, JobType.systemTest.zone(system())); + tester.setEndpoints(instanceId, JobType.systemTest.zone(system())); tester.runner().run(); - assertEquals(succeeded, tester.jobs().last(appId, JobType.systemTest).get().steps().get(Step.installReal)); + assertEquals(succeeded, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installReal)); - tester.applications().deactivate(appId, JobType.systemTest.zone(system())); + tester.applications().deactivate(instanceId, JobType.systemTest.zone(system())); tester.runner().run(); - assertEquals(failed, tester.jobs().last(appId, JobType.systemTest).get().steps().get(Step.installTester)); - assertTrue(tester.jobs().last(appId, JobType.systemTest).get().hasEnded()); - assertTrue(tester.jobs().last(appId, JobType.systemTest).get().hasFailed()); + assertEquals(failed, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installTester)); + assertTrue(tester.jobs().last(instanceId, JobType.systemTest).get().hasEnded()); + assertTrue(tester.jobs().last(instanceId, JobType.systemTest).get().hasFailed()); } @Test public void startTestsFailsIfDeploymentExpires() { tester.newRun(JobType.systemTest); tester.runner().run(); - tester.configServer().convergeServices(appId, JobType.systemTest.zone(system())); + tester.configServer().convergeServices(instanceId, JobType.systemTest.zone(system())); tester.configServer().convergeServices(testerId.id(), JobType.systemTest.zone(system())); tester.runner().run(); - tester.applications().deactivate(appId, JobType.systemTest.zone(system())); + tester.applications().deactivate(instanceId, JobType.systemTest.zone(system())); tester.runner().run(); - assertEquals(unfinished, tester.jobs().last(appId, JobType.systemTest).get().steps().get(Step.startTests)); + assertEquals(unfinished, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.startTests)); } @Test public void alternativeEndpointsAreDetected() { tester.newRun(JobType.systemTest); tester.runner().run();; - tester.configServer().convergeServices(appId, JobType.systemTest.zone(system())); + tester.configServer().convergeServices(instanceId, JobType.systemTest.zone(system())); tester.configServer().convergeServices(testerId.id(), JobType.systemTest.zone(system())); - assertEquals(unfinished, tester.jobs().last(appId, JobType.systemTest).get().steps().get(Step.installReal)); - assertEquals(unfinished, tester.jobs().last(appId, JobType.systemTest).get().steps().get(Step.installTester)); - - tester.tester().controller().curator().writeRoutingPolicies(appId, Set.of(new RoutingPolicy(appId, - ClusterSpec.Id.from("default"), - JobType.systemTest.zone(system()), - HostName.from("host"), - Optional.empty(), - emptySet()))); + assertEquals(unfinished, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installReal)); + assertEquals(unfinished, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installTester)); + + tester.tester().controller().curator().writeRoutingPolicies(instanceId, Set.of(new RoutingPolicy(instanceId, + ClusterSpec.Id.from("default"), + JobType.systemTest.zone(system()), + HostName.from("host"), + Optional.empty(), + emptySet()))); tester.tester().controller().curator().writeRoutingPolicies(testerId.id(), Set.of(new RoutingPolicy(testerId.id(), ClusterSpec.Id.from("default"), JobType.systemTest.zone(system()), @@ -255,8 +255,8 @@ public class InternalStepRunnerTest { Optional.empty(), emptySet()))); tester.runner().run();; - assertEquals(succeeded, tester.jobs().last(appId, JobType.systemTest).get().steps().get(Step.installReal)); - assertEquals(succeeded, tester.jobs().last(appId, JobType.systemTest).get().steps().get(Step.installTester)); + assertEquals(succeeded, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installReal)); + assertEquals(succeeded, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installTester)); } @Test @@ -303,12 +303,12 @@ public class InternalStepRunnerTest { assertEquals(URI.create(tester.routing().endpoints(new DeploymentId(testerId.id(), JobType.systemTest.zone(system()))).get(0).endpoint()), tester.cloud().testerUrl()); Inspector configObject = SlimeUtils.jsonToSlime(tester.cloud().config()).get(); - assertEquals(appId.serializedForm(), configObject.field("application").asString()); + assertEquals(instanceId.serializedForm(), configObject.field("application").asString()); assertEquals(JobType.systemTest.zone(system()).value(), configObject.field("zone").asString()); assertEquals(system().value(), configObject.field("system").asString()); assertEquals(1, configObject.field("endpoints").children()); assertEquals(1, configObject.field("endpoints").field(JobType.systemTest.zone(system()).value()).entries()); - configObject.field("endpoints").field(JobType.systemTest.zone(system()).value()).traverse((ArrayTraverser) (__, endpoint) -> assertEquals(tester.routing().endpoints(new DeploymentId(appId, JobType.systemTest.zone(system()))).get(0).endpoint(), endpoint.asString())); + configObject.field("endpoints").field(JobType.systemTest.zone(system()).value()).traverse((ArrayTraverser) (__, endpoint) -> assertEquals(tester.routing().endpoints(new DeploymentId(instanceId, JobType.systemTest.zone(system()))).get(0).endpoint(), endpoint.asString())); long lastId = tester.jobs().details(id).get().lastId().getAsLong(); tester.cloud().add(new LogEntry(0, Instant.ofEpochMilli(123), info, "Ready!")); @@ -336,36 +336,36 @@ public class InternalStepRunnerTest { @Test public void deployToDev() { ZoneId zone = JobType.devUsEast1.zone(system()); - tester.jobs().deploy(appId, JobType.devUsEast1, Optional.empty(), applicationPackage); + tester.jobs().deploy(instanceId, JobType.devUsEast1, Optional.empty(), applicationPackage); tester.runner().run(); - RunId id = tester.jobs().last(appId, JobType.devUsEast1).get().id(); + RunId id = tester.jobs().last(instanceId, JobType.devUsEast1).get().id(); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.installReal)); Version version = new Version("7.8.9"); Future<?> concurrentDeployment = Executors.newSingleThreadExecutor().submit(() -> { - tester.jobs().deploy(appId, JobType.devUsEast1, Optional.of(version), applicationPackage); + tester.jobs().deploy(instanceId, JobType.devUsEast1, Optional.of(version), applicationPackage); }); while ( ! concurrentDeployment.isDone()) tester.runner().run(); - assertEquals(id.number() + 1, tester.jobs().last(appId, JobType.devUsEast1).get().id().number()); + assertEquals(id.number() + 1, tester.jobs().last(instanceId, JobType.devUsEast1).get().id().number()); ApplicationPackage otherPackage = new ApplicationPackageBuilder().region("us-central-1").build(); - tester.jobs().deploy(appId, JobType.perfUsEast3, Optional.empty(), otherPackage); + tester.jobs().deploy(instanceId, JobType.perfUsEast3, Optional.empty(), otherPackage); tester.runner().run(); // Job run order determined by JobType enum order per application. - tester.configServer().convergeServices(appId, zone); - tester.setEndpoints(appId, zone); + tester.configServer().convergeServices(instanceId, zone); + tester.setEndpoints(instanceId, zone); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.installReal)); - assertEquals(applicationPackage.hash(), tester.configServer().application(appId, zone).get().applicationPackage().hash()); - assertEquals(otherPackage.hash(), tester.configServer().application(appId, JobType.perfUsEast3.zone(system())).get().applicationPackage().hash()); + assertEquals(applicationPackage.hash(), tester.configServer().application(instanceId, zone).get().applicationPackage().hash()); + assertEquals(otherPackage.hash(), tester.configServer().application(instanceId, JobType.perfUsEast3.zone(system())).get().applicationPackage().hash()); - tester.configServer().setVersion(appId, zone, version); + tester.configServer().setVersion(instanceId, zone, version); tester.runner().run(); assertEquals(1, tester.jobs().active().size()); - assertEquals(version, tester.tester().instance(appId).deployments().get(zone).version()); + assertEquals(version, tester.tester().instance(instanceId).deployments().get(zone).version()); try { - tester.jobs().deploy(appId, JobType.productionApNortheast1, Optional.empty(), applicationPackage); + tester.jobs().deploy(instanceId, JobType.productionApNortheast1, Optional.empty(), applicationPackage); fail("Deployments outside dev should not be allowed."); } catch (IllegalArgumentException expected) { } @@ -416,7 +416,7 @@ public class InternalStepRunnerTest { List<X509Certificate> trusted = new ArrayList<>(publicCdApplicationPackage.trustedCertificates()); trusted.add(tester.jobs().run(id).get().testerCertificate().get()); - assertEquals(trusted, tester.configServer().application(appId, id.type().zone(system())).get().applicationPackage().trustedCertificates()); + assertEquals(trusted, tester.configServer().application(instanceId, id.type().zone(system())).get().applicationPackage().trustedCertificates()); tester.clock().advance(InternalStepRunner.certificateTimeout.plus(Duration.ofSeconds(1))); tester.runner().run(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java index d4550ecc338..2ddef642065 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java @@ -14,7 +14,7 @@ import java.nio.file.Paths; import java.util.List; import java.util.Map; -import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.appId; +import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.instanceId; import static org.junit.Assert.assertEquals; /** @@ -25,7 +25,7 @@ public class TestConfigSerializerTest { @Test public void testConfig() throws IOException { ZoneId zone = JobType.systemTest.zone(SystemName.PublicCd); - byte[] json = new TestConfigSerializer(SystemName.PublicCd).configJson(appId, + byte[] json = new TestConfigSerializer(SystemName.PublicCd).configJson(instanceId, JobType.systemTest, Map.of(zone, Map.of(ClusterSpec.Id.from("ai"), URI.create("https://server/"))), diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java index 98e8eff508e..ec98fbf69cb 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java @@ -44,12 +44,12 @@ public class ApplicationOwnershipConfirmerTest { Optional<Contact> contact = Optional.of(tester.controllerTester().serviceRegistry().contactRetrieverMock().contact()); TenantName property = tester.controllerTester().createTenant("property", "domain", 1L, contact); tester.createAndDeploy(property, "application", 1, "default"); - Supplier<Instance> propertyApp = () -> tester.controller().applications().require(ApplicationId.from("property", "application", "default")); + Supplier<Instance> propertyApp = () -> tester.controller().applications().requireInstance(ApplicationId.from("property", "application", "default")); UserTenant user = UserTenant.create("by-user", contact); tester.controller().tenants().createUser(user); tester.createAndDeploy(user.name(), "application", 2, "default"); - Supplier<Instance> userApp = () -> tester.controller().applications().require(ApplicationId.from("by-user", "application", "default")); + Supplier<Instance> userApp = () -> tester.controller().applications().requireInstance(ApplicationId.from("by-user", "application", "default")); assertFalse("No issue is initially stored for a new application.", propertyApp.get().ownershipIssueId().isPresent()); assertFalse("No issue is initially stored for a new application.", userApp.get().ownershipIssueId().isPresent()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainerTest.java index 1f5cc100fac..9739a24af01 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainerTest.java @@ -4,16 +4,13 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; -import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node; import com.yahoo.vespa.hosted.controller.application.Deployment; -import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; import org.junit.Test; @@ -33,12 +30,12 @@ public class ClusterInfoMaintainerTest { @Test public void maintain() { tester.createTenant("tenant1", "domain123", 321L); - ApplicationId app = tester.createApplication(TenantName.from("tenant1"), "app1", "default", 123).id(); + ApplicationId app = tester.createApplication(TenantName.from("tenant1"), "app1", "default", 123).id().defaultInstance(); ZoneId zone = ZoneId.from("dev", "us-east-1"); tester.deploy(app, zone); // Precondition: no cluster info attached to the deployments - Deployment deployment = tester.controller().applications().get(app).get().deployments().values().stream() + Deployment deployment = tester.controller().applications().getInstance(app).get().deployments().values().stream() .findFirst() .get(); assertEquals(0, deployment.clusterInfo().size()); @@ -48,7 +45,7 @@ public class ClusterInfoMaintainerTest { new JobControl(new MockCuratorDb())); maintainer.maintain(); - deployment = tester.controller().applications().get(app).get().deployments().values().stream() + deployment = tester.controller().applications().getInstance(app).get().deployments().values().stream() .findFirst() .get(); assertEquals(2, deployment.clusterInfo().size()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java index f3c387eca1b..d43526ce5fa 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java @@ -37,8 +37,8 @@ public class DeploymentExpirerTest { Application devApp = tester.createApplication("app1", "tenant1", 123L, 1L); Application prodApp = tester.createApplication("app2", "tenant2", 456L, 2L); - Instance devInstance = tester.instance(devApp.id()); - Instance prodInstance = tester.instance(prodApp.id()); + Instance devInstance = tester.defaultInstance(devApp.id()); + Instance prodInstance = tester.defaultInstance(prodApp.id()); // Deploy dev tester.controllerTester().deploy(devInstance.id(), tester.controllerTester().toZone(Environment.dev)); @@ -65,7 +65,7 @@ public class DeploymentExpirerTest { } private List<Deployment> permanentDeployments(Instance instance) { - return tester.controller().applications().get(instance.id()).get().deployments().values().stream() + return tester.controller().applications().getInstance(instance.id()).get().deployments().values().stream() .filter(deployment -> deployment.zone().environment() != Environment.test && deployment.zone().environment() != Environment.staging) .collect(Collectors.toList()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java index 7a4d6821637..3c1fb2fffd0 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java @@ -10,6 +10,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; @@ -87,16 +88,16 @@ public class DeploymentIssueReporterTest { // apps 1 and 3 have one failure each. tester.jobCompletion(component).application(app1).uploadArtifact(applicationPackage).submit(); - tester.deployAndNotify(app1.id(), applicationPackage, true, systemTest); - tester.deployAndNotify(app1.id(), applicationPackage, false, stagingTest); + tester.deployAndNotify(app1.id().defaultInstance(), applicationPackage, true, systemTest); + tester.deployAndNotify(app1.id().defaultInstance(), applicationPackage, false, stagingTest); // app2 is successful, but will fail later. tester.deployCompletely(app2, canaryPackage); tester.jobCompletion(component).application(app3).uploadArtifact(applicationPackage).submit(); - tester.deployAndNotify(app3.id(), applicationPackage, true, systemTest); - tester.deployAndNotify(app3.id(), applicationPackage, true, stagingTest); - tester.deployAndNotify(app3.id(), applicationPackage, false, productionUsWest1); + tester.deployAndNotify(app3.id().defaultInstance(), applicationPackage, true, systemTest); + tester.deployAndNotify(app3.id().defaultInstance(), applicationPackage, true, stagingTest); + tester.deployAndNotify(app3.id().defaultInstance(), applicationPackage, false, productionUsWest1); reporter.maintain(); reporter.maintain(); @@ -131,7 +132,7 @@ public class DeploymentIssueReporterTest { // app3 fixes their problems, but the ticket for app3 is left open; see the resolved ticket is not escalated when another escalation period has passed. - tester.deployAndNotify(app3.id(), applicationPackage, true, productionUsWest1); + tester.deployAndNotify(app3.id().defaultInstance(), applicationPackage, true, productionUsWest1); tester.clock().advance(maxInactivity.plus(Duration.ofDays(1))); reporter.maintain(); @@ -143,7 +144,7 @@ public class DeploymentIssueReporterTest { // app3 now has a new failure past max failure age; see that a new issue is filed. tester.jobCompletion(component).application(app3).nextBuildNumber().uploadArtifact(applicationPackage).submit(); - tester.deployAndNotify(app3.id(), applicationPackage, false, systemTest); + tester.deployAndNotify(app3.id().defaultInstance(), applicationPackage, false, systemTest); tester.clock().advance(maxInactivity.plus(maxFailureAge)); reporter.maintain(); @@ -176,7 +177,7 @@ public class DeploymentIssueReporterTest { class MockDeploymentIssues extends LoggingDeploymentIssues { - private final Map<ApplicationId, IssueId> applicationIssues = new HashMap<>(); + private final Map<TenantAndApplicationId, IssueId> applicationIssues = new HashMap<>(); private final Map<IssueId, Integer> issueLevels = new HashMap<>(); MockDeploymentIssues() { @@ -192,24 +193,24 @@ public class DeploymentIssueReporterTest { @Override protected IssueId fileIssue(ApplicationId applicationId) { IssueId issueId = super.fileIssue(applicationId); - applicationIssues.put(applicationId, issueId); + applicationIssues.put(TenantAndApplicationId.from(applicationId), issueId); return issueId; } - void closeFor(ApplicationId applicationId) { - issueUpdates.remove(applicationIssues.remove(applicationId)); + void closeFor(TenantAndApplicationId id) { + issueUpdates.remove(applicationIssues.remove(id)); } - void touchFor(ApplicationId applicationId) { - issueUpdates.put(applicationIssues.get(applicationId), tester.clock().instant()); + void touchFor(TenantAndApplicationId id) { + issueUpdates.put(applicationIssues.get(id), tester.clock().instant()); } - boolean isOpenFor(ApplicationId applicationId) { - return applicationIssues.containsKey(applicationId); + boolean isOpenFor(TenantAndApplicationId id) { + return applicationIssues.containsKey(id); } - int escalationLevelFor(ApplicationId applicationId) { - return issueLevels.getOrDefault(applicationIssues.get(applicationId), 0); + int escalationLevelFor(TenantAndApplicationId id) { + return issueLevels.getOrDefault(applicationIssues.get(id), 0); } int size() { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java index 7f537c2e4bb..cf33b354139 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java @@ -37,10 +37,10 @@ public class DeploymentMetricsMaintainerTest { @Test public void updates_metrics() { var application = tester.createApplication("app1", "tenant1", 123L, 1L); - deploy(application.id(), Version.fromString("7.1")); + deploy(application.id().defaultInstance(), Version.fromString("7.1")); DeploymentMetricsMaintainer maintainer = maintainer(tester.controller()); - Supplier<Instance> app = () -> tester.instance(application.id()); + Supplier<Instance> app = () -> tester.defaultInstance(application.id()); Supplier<Deployment> deployment = () -> app.get().deployments().values().stream().findFirst().get(); // No metrics gathered yet @@ -51,7 +51,7 @@ public class DeploymentMetricsMaintainerTest { assertFalse("Never received any writes", deployment.get().activity().lastWritten().isPresent()); // Only get application metrics for old version - deploy(application.id(), Version.fromString("6.3.3")); + deploy(application.id().defaultInstance(), Version.fromString("6.3.3")); maintainer.maintain(); assertEquals(0, app.get().metrics().queryServiceQuality(), 0); assertEquals(0, deployment.get().metrics().documentCount(), 0); @@ -60,13 +60,13 @@ public class DeploymentMetricsMaintainerTest { assertFalse("Never received any writes", deployment.get().activity().lastWritten().isPresent()); // Metrics are gathered and saved to application - deploy(application.id(), Version.fromString("7.5.5")); + deploy(application.id().defaultInstance(), Version.fromString("7.5.5")); var metrics0 = Map.of(ClusterMetrics.QUERIES_PER_SECOND, 1D, ClusterMetrics.FEED_PER_SECOND, 2D, ClusterMetrics.DOCUMENT_COUNT, 3D, ClusterMetrics.QUERY_LATENCY, 4D, ClusterMetrics.FEED_LATENCY, 5D); - setMetrics(application.id(), metrics0); + setMetrics(application.id().defaultInstance(), metrics0); maintainer.maintain(); Instant t1 = tester.clock().instant().truncatedTo(MILLIS); assertEquals(0.0, app.get().metrics().queryServiceQuality(), Double.MIN_VALUE); @@ -96,7 +96,7 @@ public class DeploymentMetricsMaintainerTest { var metrics1 = new HashMap<>(metrics0); metrics1.put(ClusterMetrics.QUERIES_PER_SECOND, 0D); metrics1.put(ClusterMetrics.FEED_PER_SECOND, 5D); - setMetrics(application.id(), metrics1); + setMetrics(application.id().defaultInstance(), metrics1); maintainer.maintain(); assertEquals(t2, deployment.get().activity().lastQueried().get()); assertEquals(t3, deployment.get().activity().lastWritten().get()); @@ -107,7 +107,7 @@ public class DeploymentMetricsMaintainerTest { tester.clock().advance(Duration.ofHours(1)); var metrics2 = new HashMap<>(metrics1); metrics2.put(ClusterMetrics.FEED_PER_SECOND, 0D); - setMetrics(application.id(), metrics2); + setMetrics(application.id().defaultInstance(), metrics2); maintainer.maintain(); assertEquals(t2, deployment.get().activity().lastQueried().get()); assertEquals(t3, deployment.get().activity().lastWritten().get()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java index d0896b5e60b..ff245e2e488 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java @@ -8,6 +8,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationV import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.deployment.JobController; import com.yahoo.vespa.hosted.controller.deployment.Run; @@ -83,7 +84,7 @@ public class JobRunnerTest { JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator()), phasedExecutor(phaser), stepRunner); - ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id(); + ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id().defaultInstance(); jobs.submit(id, versions.targetApplication().source().get(), "a@b", 2, applicationPackage, new byte[0]); jobs.start(id, systemTest, versions); @@ -114,7 +115,7 @@ public class JobRunnerTest { JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator()), inThreadExecutor(), mappedRunner(outcomes)); - ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id(); + ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id().defaultInstance(); jobs.submit(id, versions.targetApplication().source().get(), "a@b", 2, applicationPackage, new byte[0]); Supplier<Run> run = () -> jobs.last(id, systemTest).get(); @@ -202,7 +203,7 @@ public class JobRunnerTest { JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator()), Executors.newFixedThreadPool(32), waitingRunner(barrier)); - ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id(); + ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id().defaultInstance(); jobs.submit(id, versions.targetApplication().source().get(), "a@b", 2, applicationPackage, new byte[0]); RunId runId = new RunId(id, systemTest, 1); @@ -217,7 +218,7 @@ public class JobRunnerTest { // Thread is still trying to deploy tester -- delete application, and see all data is garbage collected. assertEquals(Collections.singletonList(runId), jobs.active().stream().map(run -> run.id()).collect(Collectors.toList())); - tester.controllerTester().controller().applications().deleteApplication(id.tenant(), id.application(), tester.controllerTester().credentialsFor(id)); + tester.controllerTester().controller().applications().deleteApplication(id.tenant(), id.application(), tester.controllerTester().credentialsFor(TenantAndApplicationId.from(id))); assertEquals(Collections.emptyList(), jobs.active()); assertEquals(runId, jobs.last(id, systemTest).get().id()); @@ -238,7 +239,7 @@ public class JobRunnerTest { JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator()), inThreadExecutor(), (id, step) -> Optional.of(running)); - ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id(); + ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id().defaultInstance(); jobs.submit(id, versions.targetApplication().source().get(), "a@b", 2, applicationPackage, new byte[0]); for (int i = 0; i < jobs.historyLength(); i++) { @@ -266,7 +267,7 @@ public class JobRunnerTest { JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator()), inThreadExecutor(), mappedRunner(outcomes)); - ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id(); + ApplicationId id = tester.createApplication("real", "tenant", 1, 1L).id().defaultInstance(); jobs.submit(id, versions.targetApplication().source().get(), "a@b", 2, applicationPackage, new byte[0]); jobs.start(id, systemTest, versions); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java index 11749afb6ed..4fc952b0b15 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java @@ -6,7 +6,6 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.Application; -import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; @@ -63,7 +62,7 @@ public class MetricsReporterTest { // 1 app fails system-test tester.jobCompletion(component).application(app4).nextBuildNumber().uploadArtifact(applicationPackage).submit(); - tester.deployAndNotify(app4.id(), Optional.of(applicationPackage), false, systemTest); + tester.deployAndNotify(app4.id().defaultInstance(), Optional.of(applicationPackage), false, systemTest); metricsReporter.maintain(); assertEquals(25.0, metrics.getMetric(MetricsReporter.DEPLOYMENT_FAIL_METRIC)); @@ -82,22 +81,22 @@ public class MetricsReporterTest { Application app = tester.createApplication("app1", "tenant1", 1, 11L); tester.deployCompletely(app, applicationPackage); reporter.maintain(); - assertEquals(Duration.ZERO, getAverageDeploymentDuration(app.id())); // An exceptionally fast deployment :-) + assertEquals(Duration.ZERO, getAverageDeploymentDuration(app.id().defaultInstance())); // An exceptionally fast deployment :-) // App spends 3 hours deploying tester.jobCompletion(component).application(app).nextBuildNumber().uploadArtifact(applicationPackage).submit(); tester.clock().advance(Duration.ofHours(1)); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), true, systemTest); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), true, systemTest); tester.clock().advance(Duration.ofMinutes(30)); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), true, stagingTest); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), true, stagingTest); tester.clock().advance(Duration.ofMinutes(90)); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), true, productionUsWest1); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), true, productionUsWest1); reporter.maintain(); // Average time is 1 hour (system-test) + 90 minutes (staging-test runs in parallel with system-test) + 90 minutes (production) / 3 jobs - assertEquals(Duration.ofMinutes(80), getAverageDeploymentDuration(app.id())); + assertEquals(Duration.ofMinutes(80), getAverageDeploymentDuration(app.id().defaultInstance())); // Another deployment starts and stalls for 12 hours tester.jobCompletion(component).application(app).nextBuildNumber(2).uploadArtifact(applicationPackage).submit(); @@ -108,7 +107,7 @@ public class MetricsReporterTest { .plus(Duration.ofHours(12)) // hanging staging-test .plus(Duration.ofMinutes(90)) // previous production job .dividedBy(3), // Total number of orchestrated jobs - getAverageDeploymentDuration(app.id())); + getAverageDeploymentDuration(app.id().defaultInstance())); } @Test @@ -125,41 +124,41 @@ public class MetricsReporterTest { // Initial deployment without failures tester.deployCompletely(app, applicationPackage); reporter.maintain(); - assertEquals(0, getDeploymentsFailingUpgrade(app.id())); + assertEquals(0, getDeploymentsFailingUpgrade(app.id().defaultInstance())); // Failing application change is not counted tester.jobCompletion(component).application(app).nextBuildNumber().uploadArtifact(applicationPackage).submit(); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), false, systemTest); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), false, systemTest); reporter.maintain(); - assertEquals(0, getDeploymentsFailingUpgrade(app.id())); + assertEquals(0, getDeploymentsFailingUpgrade(app.id().defaultInstance())); // Application change completes - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), true, systemTest); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), true, stagingTest); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), true, productionUsWest1); - assertFalse("Change deployed", tester.controller().applications().require(app.id()).change().hasTargets()); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), true, systemTest); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), true, stagingTest); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), true, productionUsWest1); + assertFalse("Change deployed", tester.controller().applications().requireApplication(app.id()).change().hasTargets()); // New versions is released and upgrade fails in test environments Version version = Version.fromString("7.1"); tester.upgradeSystem(version); tester.upgrader().maintain(); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), false, systemTest); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), false, stagingTest); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), false, systemTest); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), false, stagingTest); reporter.maintain(); - assertEquals(2, getDeploymentsFailingUpgrade(app.id())); + assertEquals(2, getDeploymentsFailingUpgrade(app.id().defaultInstance())); // Test and staging pass and upgrade fails in production - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), true, systemTest); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), true, stagingTest); - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), false, productionUsWest1); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), true, systemTest); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), true, stagingTest); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), false, productionUsWest1); reporter.maintain(); - assertEquals(1, getDeploymentsFailingUpgrade(app.id())); + assertEquals(1, getDeploymentsFailingUpgrade(app.id().defaultInstance())); // Upgrade eventually succeeds - tester.deployAndNotify(app.id(), Optional.of(applicationPackage), true, productionUsWest1); - assertFalse("Upgrade deployed", tester.controller().applications().require(app.id()).change().hasTargets()); + tester.deployAndNotify(app.id().defaultInstance(), Optional.of(applicationPackage), true, productionUsWest1); + assertFalse("Upgrade deployed", tester.controller().applications().requireApplication(app.id()).change().hasTargets()); reporter.maintain(); - assertEquals(0, getDeploymentsFailingUpgrade(app.id())); + assertEquals(0, getDeploymentsFailingUpgrade(app.id().defaultInstance())); } @Test @@ -172,11 +171,11 @@ public class MetricsReporterTest { .build(); MetricsReporter reporter = createReporter(tester.controller()); Application application = tester.createApplication("app1", "tenant1", 1, 11L); - tester.configServer().generateWarnings(new DeploymentId(application.id(), ZoneId.from("prod", "us-west-1")), 3); - tester.configServer().generateWarnings(new DeploymentId(application.id(), ZoneId.from("prod", "us-east-3")), 4); + tester.configServer().generateWarnings(new DeploymentId(application.id().defaultInstance(), ZoneId.from("prod", "us-west-1")), 3); + tester.configServer().generateWarnings(new DeploymentId(application.id().defaultInstance(), ZoneId.from("prod", "us-east-3")), 4); tester.deployCompletely(application, applicationPackage); reporter.maintain(); - assertEquals(4, getDeploymentWarnings(application.id())); + assertEquals(4, getDeploymentWarnings(application.id().defaultInstance())); } @Test diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java index e438aab007f..02282894544 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java @@ -42,11 +42,11 @@ public class OutstandingChangeDeployerTest { Application app2 = tester.createAndDeploy("app2", 22, applicationPackage); Version version = new Version(6, 2); - tester.deploymentTrigger().triggerChange(tester.instance("app1").id(), Change.of(version)); + tester.deploymentTrigger().triggerChange(app1.id(), Change.of(version)); tester.deploymentTrigger().triggerReadyJobs(); - assertEquals(Change.of(version), tester.instance("app1").change()); - assertFalse(tester.instance("app1").outstandingChange().hasTargets()); + assertEquals(Change.of(version), tester.defaultInstance("app1").change()); + assertFalse(tester.defaultInstance("app1").outstandingChange().hasTargets()); tester.jobCompletion(JobType.component) .application(app1) @@ -55,7 +55,7 @@ public class OutstandingChangeDeployerTest { .uploadArtifact(applicationPackage) .submit(); - Instance instance = tester.instance("app1"); + Instance instance = tester.defaultInstance("app1"); assertTrue(instance.outstandingChange().hasTargets()); assertEquals("1.0.43-cafed00d", instance.outstandingChange().application().get().id()); assertEquals(2, tester.buildService().jobs().size()); @@ -74,13 +74,13 @@ public class OutstandingChangeDeployerTest { deployer.maintain(); tester.deploymentTrigger().triggerReadyJobs(); - instance = tester.instance("app1"); + instance = tester.defaultInstance("app1"); assertEquals("1.0.43-cafed00d", instance.change().application().get().id()); List<BuildService.BuildJob> jobs = tester.buildService().jobs(); assertEquals(1, jobs.size()); assertEquals(JobType.productionUsWest1.jobName(), jobs.get(0).jobName()); - assertEquals(11, jobs.get(0).projectId()); - assertFalse(tester.instance("app1").outstandingChange().hasTargets()); + assertEquals(app1.id().defaultInstance(), jobs.get(0).applicationId()); + assertFalse(tester.defaultInstance("app1").outstandingChange().hasTargets()); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdaterTest.java index 0eaa3e19927..b0201a21de5 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdaterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdaterTest.java @@ -42,7 +42,7 @@ public class RotationStatusUpdaterTest { .build(); tester.deployCompletely(application, applicationPackage); - Supplier<Instance> app = () -> tester.instance(application.id()); + Supplier<Instance> app = () -> tester.defaultInstance(application.id()); Supplier<Deployment> deployment1 = () -> app.get().deployments().get(zone1); Supplier<Deployment> deployment2 = () -> app.get().deployments().get(zone2); Supplier<Deployment> deployment3 = () -> app.get().deployments().get(zone3); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPoliciesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPoliciesTest.java index c1e1f0d0607..78a9be9f1d3 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPoliciesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPoliciesTest.java @@ -41,8 +41,8 @@ public class RoutingPoliciesTest { private final Application app1 = tester.createApplication("app1", "tenant1", 1, 1L); private final Application app2 = tester.createApplication("app2", "tenant1", 1, 1L); - private final Instance instance1 = tester.instance(app1.id()); - private final Instance instance2 = tester.instance(app2.id()); + private final Instance instance1 = tester.defaultInstance(app1.id()); + private final Instance instance2 = tester.defaultInstance(app2.id()); private final ZoneId zone1 = ZoneId.from("prod", "us-west-1"); private final ZoneId zone2 = ZoneId.from("prod", "us-central-1"); @@ -65,7 +65,7 @@ public class RoutingPoliciesTest { .endpoint("r1", "c0", "us-west-1") .endpoint("r2", "c1") .build(); - provisionLoadBalancers(clustersPerZone, app1.id(), zone1, zone2); + provisionLoadBalancers(clustersPerZone, instance1.id(), zone1, zone2); // Creates alias records tester.deployCompletely(app1, applicationPackage, ++buildNumber); @@ -86,7 +86,7 @@ public class RoutingPoliciesTest { aliasDataOf(endpoint3)); assertEquals("Routing policy count is equal to cluster count", numberOfDeployments * clustersPerZone, - tester.controller().applications().routingPolicies().get(app1.id()).size()); + tester.controller().applications().routingPolicies().get(instance1.id()).size()); // Applications gains a new deployment ApplicationPackage applicationPackage2 = new ApplicationPackageBuilder() @@ -98,7 +98,7 @@ public class RoutingPoliciesTest { .endpoint("r2", "c1") .build(); numberOfDeployments++; - provisionLoadBalancers(clustersPerZone, app1.id(), zone3); + provisionLoadBalancers(clustersPerZone, instance1.id(), zone3); tester.deployCompletely(app1, applicationPackage2, ++buildNumber); // Endpoint is updated to contain cluster in new deployment @@ -110,7 +110,7 @@ public class RoutingPoliciesTest { // Another application is deployed with a single cluster and global endpoint var endpoint4 = "r0.app2.tenant1.global.vespa.oath.cloud"; - provisionLoadBalancers(1, app2.id(), zone1, zone2); + provisionLoadBalancers(1, instance2.id(), zone1, zone2); var applicationPackage3 = new ApplicationPackageBuilder() .region(zone1.region()) .region(zone2.region()) @@ -132,7 +132,7 @@ public class RoutingPoliciesTest { assertEquals("DNS records are removed", List.of(), aliasDataOf(endpoint1)); assertEquals("DNS records are removed", List.of(), aliasDataOf(endpoint2)); assertEquals("DNS records are removed", List.of(), aliasDataOf(endpoint3)); - Set<RoutingPolicy> policies = tester.controller().curator().readRoutingPolicies(app1.id()); + Set<RoutingPolicy> policies = tester.controller().curator().readRoutingPolicies(instance1.id()); assertEquals(clustersPerZone * numberOfDeployments, policies.size()); assertTrue("Rotation membership is removed from all policies", policies.stream().allMatch(policy -> policy.endpoints().isEmpty())); @@ -144,7 +144,7 @@ public class RoutingPoliciesTest { // Deploy application int clustersPerZone = 2; int buildNumber = 42; - provisionLoadBalancers(clustersPerZone, app1.id(), zone1, zone2); + provisionLoadBalancers(clustersPerZone, instance1.id(), zone1, zone2); tester.deployCompletely(app1, applicationPackage, ++buildNumber); // Deployment creates records and policies for all clusters in all zones @@ -163,7 +163,7 @@ public class RoutingPoliciesTest { assertEquals(4, policies(instance1).size()); // Add 1 cluster in each zone and deploy - provisionLoadBalancers(clustersPerZone + 1, app1.id(), zone1, zone2); + provisionLoadBalancers(clustersPerZone + 1, instance1.id(), zone1, zone2); tester.deployCompletely(app1, applicationPackage, ++buildNumber); expectedRecords = Set.of( "c0.app1.tenant1.us-west-1.vespa.oath.cloud", @@ -177,7 +177,7 @@ public class RoutingPoliciesTest { assertEquals(6, policies(instance1).size()); // Deploy another application - provisionLoadBalancers(clustersPerZone, app2.id(), zone1, zone2); + provisionLoadBalancers(clustersPerZone, instance2.id(), zone1, zone2); tester.deployCompletely(app2, applicationPackage, ++buildNumber); expectedRecords = Set.of( "c0.app1.tenant1.us-west-1.vespa.oath.cloud", @@ -195,7 +195,7 @@ public class RoutingPoliciesTest { assertEquals(4, policies(instance2).size()); // Deploy removes cluster from app1 - provisionLoadBalancers(clustersPerZone, app1.id(), zone1, zone2); + provisionLoadBalancers(clustersPerZone, instance1.id(), zone1, zone2); tester.deployCompletely(app1, applicationPackage, ++buildNumber); expectedRecords = Set.of( "c0.app1.tenant1.us-west-1.vespa.oath.cloud", @@ -210,10 +210,10 @@ public class RoutingPoliciesTest { assertEquals(expectedRecords, recordNames()); // Remove app2 completely - tester.controller().applications().require(app2.id()).deployments().keySet() + tester.controller().applications().requireInstance(instance2.id()).deployments().keySet() .forEach(zone -> { - tester.configServer().removeLoadBalancers(app2.id(), zone); - tester.controller().applications().deactivate(app2.id(), zone); + tester.configServer().removeLoadBalancers(instance2.id(), zone); + tester.controller().applications().deactivate(instance2.id(), zone); }); tester.flushDnsRequests(); expectedRecords = Set.of( @@ -223,22 +223,22 @@ public class RoutingPoliciesTest { "c1.app1.tenant1.us-central-1.vespa.oath.cloud" ); assertEquals(expectedRecords, recordNames()); - assertTrue("Removes stale routing policies " + app2, tester.controller().applications().routingPolicies().get(app2.id()).isEmpty()); - assertEquals("Keeps routing policies for " + app1, 4, tester.controller().applications().routingPolicies().get(app1.id()).size()); + assertTrue("Removes stale routing policies " + app2, tester.controller().applications().routingPolicies().get(instance2.id()).isEmpty()); + assertEquals("Keeps routing policies for " + app1, 4, tester.controller().applications().routingPolicies().get(instance1.id()).size()); } @Test public void cluster_endpoints_resolve_from_policies() { - provisionLoadBalancers(3, app1.id(), zone1); + provisionLoadBalancers(3, instance1.id(), zone1); tester.deployCompletely(app1, applicationPackage); - tester.controllerTester().serviceRegistry().routingGeneratorMock().putEndpoints(new DeploymentId(app1.id(), zone1), Collections.emptyList()); + tester.controllerTester().serviceRegistry().routingGeneratorMock().putEndpoints(new DeploymentId(instance1.id(), zone1), Collections.emptyList()); assertEquals(Map.of(ClusterSpec.Id.from("c0"), URI.create("https://c0.app1.tenant1.us-west-1.vespa.oath.cloud/"), ClusterSpec.Id.from("c1"), URI.create("https://c1.app1.tenant1.us-west-1.vespa.oath.cloud/"), ClusterSpec.Id.from("c2"), URI.create("https://c2.app1.tenant1.us-west-1.vespa.oath.cloud/")), - tester.controller().applications().clusterEndpoints(new DeploymentId(app1.id(), zone1))); + tester.controller().applications().clusterEndpoints(new DeploymentId(instance1.id(), zone1))); } private Set<RoutingPolicy> policies(Instance instance) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java index f6121760567..e247c2af3c9 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java @@ -62,12 +62,12 @@ public class UpgraderTest { Application default2 = tester.createAndDeploy("default2", 5, "default"); Application conservative0 = tester.createAndDeploy("conservative0", 6, "conservative"); - Instance canary0default = tester.instance(canary0.id()); - Instance canary1default = tester.instance(canary1.id()); - Instance default0default = tester.instance(default0.id()); - Instance default1default = tester.instance(default1.id()); - Instance default2default = tester.instance(default2.id()); - Instance conservative0default = tester.instance(conservative0.id()); + Instance canary0default = tester.defaultInstance(canary0.id()); + Instance canary1default = tester.defaultInstance(canary1.id()); + Instance default0default = tester.defaultInstance(default0.id()); + Instance default1default = tester.defaultInstance(default1.id()); + Instance default2default = tester.defaultInstance(default2.id()); + Instance conservative0default = tester.defaultInstance(conservative0.id()); tester.upgrader().maintain(); tester.triggerUntilQuiescence(); @@ -134,9 +134,9 @@ public class UpgraderTest { tester.upgradeSystem(version3); assertEquals(version3, tester.controller().versionStatus().systemVersion().get().versionNumber()); tester.upgrader().maintain(); - tester.buildService().remove(ControllerTester.buildJob(canary0.id(), stagingTest)); - tester.buildService().remove(ControllerTester.buildJob(canary1.id(), systemTest)); - tester.buildService().remove(ControllerTester.buildJob(canary1.id(), stagingTest)); + tester.buildService().remove(ControllerTester.buildJob(canary0default.id(), stagingTest)); + tester.buildService().remove(ControllerTester.buildJob(canary1default.id(), systemTest)); + tester.buildService().remove(ControllerTester.buildJob(canary1default.id(), stagingTest)); tester.triggerUntilQuiescence(); assertEquals("New system version: Should upgrade Canaries", 4, tester.buildService().jobs().size()); @@ -204,11 +204,11 @@ public class UpgraderTest { tester.triggerUntilQuiescence(); assertEquals("Upgrade of defaults are scheduled", 10, tester.buildService().jobs().size()); - assertEquals(version4, tester.instance(default0.id()).change().platform().get()); - assertEquals(version4, tester.instance(default1.id()).change().platform().get()); - assertEquals(version4, tester.instance(default2.id()).change().platform().get()); - assertEquals(version4, tester.instance(default3.id()).change().platform().get()); - assertEquals(version4, tester.instance(default4.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default0.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default1.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default2.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default3.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default4.id()).change().platform().get()); tester.completeUpgrade(default0, version4, "default"); // State: Default applications started upgrading to version4 (and one completed) @@ -224,11 +224,11 @@ public class UpgraderTest { tester.triggerUntilQuiescence(); assertEquals("Upgrade of defaults are scheduled", 10, tester.buildService().jobs().size()); - assertEquals(version5, tester.instance(default0.id()).change().platform().get()); - assertEquals(version4, tester.instance(default1.id()).change().platform().get()); - assertEquals(version4, tester.instance(default2.id()).change().platform().get()); - assertEquals(version4, tester.instance(default3.id()).change().platform().get()); - assertEquals(version4, tester.instance(default4.id()).change().platform().get()); + assertEquals(version5, tester.defaultInstance(default0.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default1.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default2.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default3.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default4.id()).change().platform().get()); tester.completeUpgrade(default1, version4, "default"); tester.completeUpgrade(default2, version4, "default"); @@ -260,7 +260,7 @@ public class UpgraderTest { assertEquals("Upgrade of defaults are scheduled on " + version4 + " instead, since " + version5 + " is broken: " + "This is default3 since it failed upgrade on both " + version4 + " and " + version5, 2, tester.buildService().jobs().size()); - assertEquals(version4, tester.instance(default3.id()).change().platform().get()); + assertEquals(version4, tester.defaultInstance(default3.id()).change().platform().get()); } @Test @@ -347,7 +347,7 @@ public class UpgraderTest { tester.upgradeSystem(version); Application app = tester.createApplication("app1", "tenant1", 1, 11L); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); tester.jobCompletion(component).application(app).uploadArtifact(applicationPackage).submit(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, systemTest); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, stagingTest); @@ -370,8 +370,8 @@ public class UpgraderTest { // staging-test fails and failure is recorded tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), false, stagingTest); - assertTrue("Failure is recorded", tester.instance(app.id()).deploymentJobs().hasFailures()); - assertTrue("Application has pending change", tester.instance(app.id()).change().hasTargets()); + assertTrue("Failure is recorded", tester.defaultInstance(app.id()).deploymentJobs().hasFailures()); + assertTrue("Application has pending change", tester.defaultInstance(app.id()).change().hasTargets()); // New version is released version = Version.fromString("6.4"); @@ -382,7 +382,7 @@ public class UpgraderTest { tester.upgrader().maintain(); tester.triggerUntilQuiescence(); tester.jobCompletion(stagingTest).application(app).unsuccessful().submit(); - assertTrue("Application still has failures", tester.instance(app.id()).deploymentJobs().hasFailures()); + assertTrue("Application still has failures", tester.defaultInstance(app.id()).deploymentJobs().hasFailures()); assertEquals(2, tester.buildService().jobs().size()); // Upgrader runs again, nothing happens as test jobs are already running. @@ -435,7 +435,7 @@ public class UpgraderTest { tester.triggerUntilQuiescence(); // apps pass system-test, but do not trigger next jobs as upgrade is cancelled - assertFalse("No change present", tester.applications().require(default4.id()).change().hasTargets()); + assertFalse("No change present", tester.applications().requireInstance(default4.id().defaultInstance()).change().hasTargets()); tester.jobCompletion(systemTest).application(default0).submit(); tester.jobCompletion(systemTest).application(default1).submit(); tester.jobCompletion(systemTest).application(default2).submit(); @@ -510,7 +510,7 @@ public class UpgraderTest { tester.upgrader().maintain(); tester.triggerUntilQuiescence(); assertEquals("Upgrade scheduled for remaining apps", 10, tester.buildService().jobs().size()); - assertEquals("default4 is still upgrading to 5.1", v1, tester.instance(default4.id()).change().platform().get()); + assertEquals("default4 is still upgrading to 5.1", v1, tester.defaultInstance(default4.id()).change().platform().get()); // 4/5 applications fail (in the last prod zone) and lowers confidence tester.completeUpgradeWithError(default0, v2, "default", productionUsEast3); @@ -520,8 +520,8 @@ public class UpgraderTest { tester.upgradeSystem(v2); assertEquals(VespaVersion.Confidence.broken, tester.controller().versionStatus().systemVersion().get().confidence()); - assertEquals(v2, tester.instance("default0").deployments().get(ZoneId.from("prod.us-west-1")).version()); - assertEquals(v0, tester.instance("default0").deployments().get(ZoneId.from("prod.us-east-3")).version()); + assertEquals(v2, tester.defaultInstance("default0").deployments().get(ZoneId.from("prod.us-west-1")).version()); + assertEquals(v0, tester.defaultInstance("default0").deployments().get(ZoneId.from("prod.us-east-3")).version()); tester.upgrader().maintain(); tester.buildService().clear(); tester.triggerUntilQuiescence(); @@ -529,16 +529,16 @@ public class UpgraderTest { assertEquals("Upgrade to 5.1 scheduled for apps not completely on 5.1 or 5.2", 10, tester.buildService().jobs().size()); // The tester code for completing upgrades does not handle this scenario, so we trigger each step manually (for one app) - tester.deployAndNotify(tester.instance("default0").id(), DeploymentTester.applicationPackage("default"), true, systemTest); - tester.deployAndNotify(tester.instance("default0").id(), DeploymentTester.applicationPackage("default"), true, stagingTest); + tester.deployAndNotify(tester.defaultInstance("default0").id(), DeploymentTester.applicationPackage("default"), true, systemTest); + tester.deployAndNotify(tester.defaultInstance("default0").id(), DeploymentTester.applicationPackage("default"), true, stagingTest); // prod zone on 5.2 (usWest1) is skipped, but we still trigger the next zone from triggerReadyJobs: - tester.deployAndNotify(tester.instance("default0").id(), DeploymentTester.applicationPackage("default"), true, productionUsEast3); + tester.deployAndNotify(tester.defaultInstance("default0").id(), DeploymentTester.applicationPackage("default"), true, productionUsEast3); // Resulting state: - assertEquals(v2, tester.instance("default0").deployments().get(ZoneId.from("prod.us-west-1")).version()); + assertEquals(v2, tester.defaultInstance("default0").deployments().get(ZoneId.from("prod.us-west-1")).version()); assertEquals("Last zone is upgraded to v1", - v1, tester.instance("default0").deployments().get(ZoneId.from("prod.us-east-3")).version()); - assertFalse(tester.instance("default0").change().hasTargets()); + v1, tester.defaultInstance("default0").deployments().get(ZoneId.from("prod.us-east-3")).version()); + assertFalse(tester.defaultInstance("default0").change().hasTargets()); } @Test @@ -653,7 +653,7 @@ public class UpgraderTest { .build(); Application app = tester.createAndDeploy("app1", 1, applicationPackage); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); // New version is released version = Version.fromString("6.3"); @@ -691,7 +691,7 @@ public class UpgraderTest { .build(); Application app = tester.createAndDeploy("app1", 1, applicationPackage); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); // New version is released version = Version.fromString("6.3"); @@ -748,7 +748,7 @@ public class UpgraderTest { assertTrue("All jobs consumed", tester.buildService().jobs().isEmpty()); // App is completely upgraded to the latest version - for (Deployment deployment : tester.applications().require(app.id()).deployments().values()) + for (Deployment deployment : tester.applications().requireInstance(instance.id()).deployments().values()) assertEquals(version, deployment.version()); } @@ -822,7 +822,7 @@ public class UpgraderTest { tester.jobCompletion(stagingTest).application(default4).unsuccessful().submit(); // 5th app never reports back and has a dead job, but no ongoing change - Instance deadLocked = tester.applications().require(default4.id()); + Instance deadLocked = tester.applications().requireInstance(default4.id().defaultInstance()); tester.assertRunning(systemTest, deadLocked.id()); assertFalse("No change present", deadLocked.change().hasTargets()); @@ -850,10 +850,10 @@ public class UpgraderTest { tester.completeUpgrade(default2, version, defaultApplicationPackageV2); tester.completeUpgrade(default3, version, defaultApplicationPackageV2); - assertEquals(version, tester.instance(default0.id()).oldestDeployedPlatform().get()); - assertEquals(version, tester.instance(default1.id()).oldestDeployedPlatform().get()); - assertEquals(version, tester.instance(default2.id()).oldestDeployedPlatform().get()); - assertEquals(version, tester.instance(default3.id()).oldestDeployedPlatform().get()); + assertEquals(version, tester.defaultInstance(default0.id()).oldestDeployedPlatform().get()); + assertEquals(version, tester.defaultInstance(default1.id()).oldestDeployedPlatform().get()); + assertEquals(version, tester.defaultInstance(default2.id()).oldestDeployedPlatform().get()); + assertEquals(version, tester.defaultInstance(default3.id()).oldestDeployedPlatform().get()); } @Test @@ -878,7 +878,7 @@ public class UpgraderTest { // Dev deployment which should be ignored Application dev0 = tester.createApplication("dev0", "tenant1", 7, 1L); - tester.controllerTester().deploy(dev0.id(), ZoneId.from(Environment.dev, RegionName.from("dev-region"))); + tester.controllerTester().deploy(dev0.id().defaultInstance(), ZoneId.from(Environment.dev, RegionName.from("dev-region"))); // New version is released and canaries upgrade version = Version.fromString("6.3"); @@ -959,7 +959,7 @@ public class UpgraderTest { Application canary0 = tester.createAndDeploy("canary0", 1, "canary"); Application default0 = tester.createAndDeploy("default0", 2, default0ApplicationPackage); tester.applications().lockApplicationOrThrow(default0.id(), a -> tester.applications().store(a.withMajorVersion(6))); - assertEquals(OptionalInt.of(6), tester.applications().get(default0.id()).get().majorVersion()); + assertEquals(OptionalInt.of(6), tester.applications().getInstance(default0.id().defaultInstance()).get().majorVersion()); // New major version is released version = Version.fromString("7.0"); @@ -1047,7 +1047,7 @@ public class UpgraderTest { .build(); Application app = tester.createAndDeploy("app1", 1, applicationPackage); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); // New version is released version = Version.fromString("6.3"); @@ -1076,7 +1076,7 @@ public class UpgraderTest { tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsWest1); assertTrue("All jobs consumed", tester.buildService().jobs().isEmpty()); - instance = tester.instance(app.id()); + instance = tester.defaultInstance(app.id()); for (Deployment deployment : instance.deployments().values()) { assertEquals(version, deployment.version()); assertEquals(applicationVersion, deployment.applicationVersion().id()); @@ -1101,7 +1101,7 @@ public class UpgraderTest { .build(); Application app = tester.createAndDeploy("app1", 1, applicationPackage); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); tester.jobCompletion(component).application(app).nextBuildNumber().uploadArtifact(applicationPackage).submit(); @@ -1154,7 +1154,7 @@ public class UpgraderTest { .build(); Application app = tester.createAndDeploy("app1", 1, applicationPackage); - Instance instance = tester.instance(app.id()); + Instance instance = tester.defaultInstance(app.id()); tester.jobCompletion(component).application(app).nextBuildNumber().uploadArtifact(applicationPackage).submit(); @@ -1179,16 +1179,16 @@ public class UpgraderTest { assertEquals(Collections.emptyList(), tester.buildService().jobs()); // No jobs left. tester.outstandingChangeDeployer().run(); - assertFalse(tester.instance(app.id()).change().hasTargets()); + assertFalse(tester.defaultInstance(app.id()).change().hasTargets()); tester.clock().advance(Duration.ofHours(2)); tester.outstandingChangeDeployer().run(); - assertTrue(tester.instance(app.id()).change().hasTargets()); + assertTrue(tester.defaultInstance(app.id()).change().hasTargets()); tester.readyJobTrigger().run(); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsWest1); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsCentral1); tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, productionUsEast3); - assertFalse(tester.instance(app.id()).change().hasTargets()); + assertFalse(tester.defaultInstance(app.id()).change().hasTargets()); } @Test @@ -1199,7 +1199,7 @@ public class UpgraderTest { // Create an application with pinned platform version. Application application = tester.createApplication("application", "tenant", 2, 3); - Instance instance = tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder().environment(Environment.prod) .region("us-east-3") .region("us-west-1") @@ -1207,33 +1207,33 @@ public class UpgraderTest { tester.deploymentTrigger().forceChange(application.id(), Change.empty().withPin()); tester.deployCompletely(application, applicationPackage); - assertFalse(tester.instance(application.id()).change().hasTargets()); - assertTrue(tester.instance(application.id()).change().isPinned()); - assertEquals(2, tester.instance(application.id()).deployments().size()); + assertFalse(tester.defaultInstance(application.id()).change().hasTargets()); + assertTrue(tester.defaultInstance(application.id()).change().isPinned()); + assertEquals(2, tester.defaultInstance(application.id()).deployments().size()); // Application does not upgrade. Version version1 = Version.fromString("6.3"); tester.upgradeSystem(version1); tester.upgrader().maintain(); - assertFalse(tester.instance(application.id()).change().hasTargets()); - assertTrue(tester.instance(application.id()).change().isPinned()); + assertFalse(tester.defaultInstance(application.id()).change().hasTargets()); + assertTrue(tester.defaultInstance(application.id()).change().isPinned()); // New application package is deployed. tester.deployCompletely(application, applicationPackage, BuildJob.defaultBuildNumber + 1); - assertFalse(tester.instance(application.id()).change().hasTargets()); - assertTrue(tester.instance(application.id()).change().isPinned()); + assertFalse(tester.defaultInstance(application.id()).change().hasTargets()); + assertTrue(tester.defaultInstance(application.id()).change().isPinned()); // Application upgrades to new version when pin is removed. tester.deploymentTrigger().cancelChange(application.id(), PIN); tester.upgrader().maintain(); - assertTrue(tester.instance(application.id()).change().hasTargets()); - assertFalse(tester.instance(application.id()).change().isPinned()); + assertTrue(tester.defaultInstance(application.id()).change().hasTargets()); + assertFalse(tester.defaultInstance(application.id()).change().isPinned()); // Application is pinned to new version, and upgrade is therefore not cancelled, even though confidence is broken. tester.deploymentTrigger().forceChange(application.id(), Change.empty().withPin()); tester.upgrader().maintain(); tester.readyJobTrigger().maintain(); - assertEquals(version1, tester.instance(application.id()).change().platform().get()); + assertEquals(version1, tester.defaultInstance(application.id()).change().platform().get()); // Application fails upgrade after one zone is complete, and is pinned again to the old version. tester.deployAndNotify(instance.id(), Optional.empty(), true, systemTest); @@ -1244,16 +1244,16 @@ public class UpgraderTest { tester.deploymentTrigger().cancelChange(application.id(), ALL); tester.deploymentTrigger().forceChange(application.id(), Change.of(version0).withPin()); tester.buildService().clear(); - assertEquals(version0, tester.instance(application.id()).change().platform().get()); + assertEquals(version0, tester.defaultInstance(application.id()).change().platform().get()); // Application downgrades to pinned version. tester.readyJobTrigger().maintain(); tester.deployAndNotify(instance.id(), Optional.empty(), true, systemTest); tester.deployAndNotify(instance.id(), Optional.empty(), true, stagingTest); tester.deployAndNotify(instance.id(), Optional.empty(), true, productionUsEast3); - assertTrue(tester.instance(application.id()).change().hasTargets()); + assertTrue(tester.defaultInstance(application.id()).change().hasTargets()); tester.deployAndNotify(instance.id(), Optional.empty(), true, productionUsWest1); - assertFalse(tester.instance(application.id()).change().hasTargets()); + assertFalse(tester.defaultInstance(application.id()).change().hasTargets()); } @Test @@ -1288,22 +1288,22 @@ public class UpgraderTest { // App 2 is allowed on new major and upgrades tester.controller().applications().lockApplicationIfPresent(app2.id(), app -> tester.applications().store(app.withMajorVersion(7))); tester.upgrader().maintain(); - assertEquals(version2, tester.controller().applications().require(app2.id()).change().platform().get()); + assertEquals(version2, tester.controller().applications().requireInstance(app2.id().defaultInstance()).change().platform().get()); // App 1 is unpinned and upgrades to latest 6 tester.controller().applications().lockApplicationIfPresent(app1.id(), app -> tester.controller().applications().store(app.withChange(app.get().change().withoutPin()))); tester.upgrader().maintain(); assertEquals("Application upgrades to latest allowed major", version1, - tester.controller().applications().require(app1.id()).change().platform().get()); + tester.controller().applications().requireInstance(app1.id().defaultInstance()).change().platform().get()); } @Test public void testsEachUpgradeCombinationWithFailingDeployments() { DeploymentTester tester = new DeploymentTester(); Application application = tester.createApplication("app1", "tenant1", 1, 1L); - Instance instance = tester.instance(application.id()); - Supplier<Instance> app = () -> tester.instance(application.id()); + Instance instance = tester.defaultInstance(application.id()); + Supplier<Instance> app = () -> tester.defaultInstance(application.id()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-central-1") 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 new file mode 100644 index 00000000000..5889b71bdd9 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java @@ -0,0 +1,250 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.persistence; + +import com.yahoo.component.Version; +import com.yahoo.config.application.api.DeploymentSpec; +import com.yahoo.config.application.api.ValidationOverrides; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.config.SlimeUtils; +import com.yahoo.vespa.hosted.controller.Application; +import com.yahoo.vespa.hosted.controller.Instance; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; +import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; +import com.yahoo.vespa.hosted.controller.api.integration.organization.User; +import com.yahoo.vespa.hosted.controller.application.AssignedRotation; +import com.yahoo.vespa.hosted.controller.application.Change; +import com.yahoo.vespa.hosted.controller.application.ClusterInfo; +import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; +import com.yahoo.vespa.hosted.controller.application.Deployment; +import com.yahoo.vespa.hosted.controller.application.DeploymentActivity; +import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; +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.TenantAndApplicationId; +import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; +import com.yahoo.vespa.hosted.controller.rotation.RotationId; +import com.yahoo.vespa.hosted.controller.rotation.RotationState; +import com.yahoo.vespa.hosted.controller.rotation.RotationStatus; +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.Set; + +import static com.yahoo.config.provision.SystemName.main; +import static java.util.Optional.empty; +import static org.junit.Assert.assertEquals; + +/** + * @author bratseth + */ + +public class ApplicationSerializerTest { + + private static final ApplicationSerializer APPLICATION_SERIALIZER = new ApplicationSerializer(); + private static final Path testData = Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/"); + private static final ZoneId zone1 = ZoneId.from("prod", "us-west-1"); + private static final ZoneId zone2 = ZoneId.from("prod", "us-east-3"); + + @Test + public void testSerialization() { + DeploymentSpec deploymentSpec = DeploymentSpec.fromXml("<deployment version='1.0'>" + + " <staging/>" + + "</deployment>"); + ValidationOverrides validationOverrides = ValidationOverrides.fromXml("<validation-overrides version='1.0'>" + + " <allow until='2017-06-15'>deployment-removal</allow>" + + "</validation-overrides>"); + + OptionalLong projectId = OptionalLong.of(123L); + + List<Deployment> deployments = new ArrayList<>(); + ApplicationVersion applicationVersion1 = ApplicationVersion.from(new SourceRevision("repo1", "branch1", "commit1"), 31); + ApplicationVersion applicationVersion2 = ApplicationVersion + .from(new SourceRevision("repo1", "branch1", "commit1"), 32, "a@b", + Version.fromString("6.3.1"), Instant.ofEpochMilli(496)); + Instant activityAt = Instant.parse("2018-06-01T10:15:30.00Z"); + deployments.add(new Deployment(zone1, applicationVersion1, Version.fromString("1.2.3"), Instant.ofEpochMilli(3))); // One deployment without cluster info and utils + deployments.add(new Deployment(zone2, applicationVersion2, Version.fromString("1.2.3"), Instant.ofEpochMilli(5), + createClusterUtils(3, 0.2), createClusterInfo(3, 4), + new DeploymentMetrics(2, 3, 4, 5, 6, + Optional.of(Instant.now().truncatedTo(ChronoUnit.MILLIS)), + Map.of(DeploymentMetrics.Warning.all, 3)), + DeploymentActivity.create(Optional.of(activityAt), Optional.of(activityAt), + OptionalDouble.of(200), OptionalDouble.of(10)))); + + List<JobStatus> statusList = new ArrayList<>(); + + statusList.add(JobStatus.initial(JobType.systemTest) + .withTriggering(Version.fromString("5.6.7"), ApplicationVersion.unknown, empty(), "Test", Instant.ofEpochMilli(7)) + .withCompletion(30, empty(), Instant.ofEpochMilli(8)) + .withPause(OptionalLong.of(1L << 32))); + statusList.add(JobStatus.initial(JobType.stagingTest) + .withTriggering(Version.fromString("5.6.6"), ApplicationVersion.unknown, empty(), "Test 2", Instant.ofEpochMilli(5)) + .withCompletion(11, Optional.of(JobError.unknown), Instant.ofEpochMilli(6))); + statusList.add(JobStatus.initial(JobType.from(main, zone1).get()) + .withTriggering(Version.fromString("5.6.6"), ApplicationVersion.unknown, deployments.stream().findFirst(), "Test 3", Instant.ofEpochMilli(6)) + .withCompletion(11, empty(), Instant.ofEpochMilli(7))); + + DeploymentJobs deploymentJobs = new DeploymentJobs(OptionalLong.empty(), statusList, empty(), true); + + var rotationStatus = RotationStatus.from(Map.of(new RotationId("my-rotation"), + Map.of(ZoneId.from("prod", "us-west-1"), RotationState.in, + ZoneId.from("prod", "us-east-3"), RotationState.out))); + + ApplicationId id1 = ApplicationId.from("t1", "a1", "i1"); + ApplicationId id3 = ApplicationId.from("t1", "a1", "i3"); + List<Instance> instances = List.of(new Instance(id1, + deployments, + deploymentJobs, + List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of())), + rotationStatus), + new Instance(id3, + List.of(), + new DeploymentJobs(OptionalLong.empty(), List.of(), empty(), true), + List.of(), + RotationStatus.EMPTY)); + + Application original = new Application(TenantAndApplicationId.from(id1), + Instant.now().truncatedTo(ChronoUnit.MILLIS), + deploymentSpec, + validationOverrides, + Change.of(Version.fromString("6.7")).withPin(), + Change.of(ApplicationVersion.from(new SourceRevision("repo", "master", "deadcafe"), 42)), + Optional.of(IssueId.from("4321")), + Optional.of(IssueId.from("1234")), + Optional.of(User.from("by-username")), + OptionalInt.of(7), + new ApplicationMetrics(0.5, 0.9), + Optional.of("-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"), + projectId, + true, + instances); + + Application serialized = APPLICATION_SERIALIZER.fromSlime(APPLICATION_SERIALIZER.toSlime(original)); + + assertEquals(original.id(), serialized.id()); + assertEquals(original.createdAt(), serialized.createdAt()); + + assertEquals(original.deploymentSpec().xmlForm(), serialized.deploymentSpec().xmlForm()); + assertEquals(original.validationOverrides().xmlForm(), serialized.validationOverrides().xmlForm()); + + assertEquals(original.projectId(), serialized.projectId()); + assertEquals(original.internal(), serialized.internal()); + assertEquals(original.deploymentIssueId(), serialized.deploymentIssueId()); + + assertEquals(0, serialized.require(id3.instance()).deployments().size()); + assertEquals(0, serialized.require(id3.instance()).deploymentJobs().jobStatus().size()); + assertEquals(0, serialized.require(id3.instance()).rotations().size()); + assertEquals(RotationStatus.EMPTY, serialized.require(id3.instance()).rotationStatus()); + + assertEquals(2, serialized.require(id1.instance()).deployments().size()); + assertEquals(original.require(id1.instance()).deployments().get(zone1).applicationVersion(), serialized.require(id1.instance()).deployments().get(zone1).applicationVersion()); + assertEquals(original.require(id1.instance()).deployments().get(zone2).applicationVersion(), serialized.require(id1.instance()).deployments().get(zone2).applicationVersion()); + assertEquals(original.require(id1.instance()).deployments().get(zone1).version(), serialized.require(id1.instance()).deployments().get(zone1).version()); + assertEquals(original.require(id1.instance()).deployments().get(zone2).version(), serialized.require(id1.instance()).deployments().get(zone2).version()); + assertEquals(original.require(id1.instance()).deployments().get(zone1).at(), serialized.require(id1.instance()).deployments().get(zone1).at()); + assertEquals(original.require(id1.instance()).deployments().get(zone2).at(), serialized.require(id1.instance()).deployments().get(zone2).at()); + assertEquals(original.require(id1.instance()).deployments().get(zone2).activity().lastQueried().get(), serialized.require(id1.instance()).deployments().get(zone2).activity().lastQueried().get()); + assertEquals(original.require(id1.instance()).deployments().get(zone2).activity().lastWritten().get(), serialized.require(id1.instance()).deployments().get(zone2).activity().lastWritten().get()); + + assertEquals(original.require(id1.instance()).deploymentJobs().projectId(), serialized.require(id1.instance()).deploymentJobs().projectId()); + assertEquals(original.require(id1.instance()).deploymentJobs().jobStatus().size(), serialized.require(id1.instance()).deploymentJobs().jobStatus().size()); + assertEquals( original.require(id1.instance()).deploymentJobs().jobStatus().get(JobType.systemTest), + serialized.require(id1.instance()).deploymentJobs().jobStatus().get(JobType.systemTest)); + assertEquals( original.require(id1.instance()).deploymentJobs().jobStatus().get(JobType.stagingTest), + serialized.require(id1.instance()).deploymentJobs().jobStatus().get(JobType.stagingTest)); + + assertEquals(original.outstandingChange(), serialized.outstandingChange()); + + assertEquals(original.ownershipIssueId(), serialized.ownershipIssueId()); + assertEquals(original.owner(), serialized.owner()); + assertEquals(original.majorVersion(), serialized.majorVersion()); + assertEquals(original.change(), serialized.change()); + assertEquals(original.pemDeployKey(), serialized.pemDeployKey()); + + assertEquals(original.require(id1.instance()).rotations(), serialized.require(id1.instance()).rotations()); + assertEquals(original.require(id1.instance()).rotationStatus(), serialized.require(id1.instance()).rotationStatus()); + + // Test cluster utilization + assertEquals(0, serialized.require(id1.instance()).deployments().get(zone1).clusterUtils().size()); + assertEquals(3, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().size()); + assertEquals(0.4, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().get(ClusterSpec.Id.from("id2")).getCpu(), 0.01); + assertEquals(0.2, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().get(ClusterSpec.Id.from("id1")).getCpu(), 0.01); + assertEquals(0.2, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().get(ClusterSpec.Id.from("id1")).getMemory(), 0.01); + + // Test cluster info + assertEquals(3, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().size()); + assertEquals(10, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorCost()); + assertEquals(ClusterSpec.Type.content, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getClusterType()); + assertEquals("flavor2", serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavor()); + assertEquals(4, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getHostnames().size()); + assertEquals(2, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorCPU(), Double.MIN_VALUE); + assertEquals(4, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorMem(), Double.MIN_VALUE); + assertEquals(50, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorDisk(), Double.MIN_VALUE); + + // Test metrics + assertEquals(original.metrics().queryServiceQuality(), serialized.metrics().queryServiceQuality(), Double.MIN_VALUE); + assertEquals(original.metrics().writeServiceQuality(), serialized.metrics().writeServiceQuality(), Double.MIN_VALUE); + assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().queriesPerSecond(), serialized.require(id1.instance()).deployments().get(zone2).metrics().queriesPerSecond(), Double.MIN_VALUE); + assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().writesPerSecond(), serialized.require(id1.instance()).deployments().get(zone2).metrics().writesPerSecond(), Double.MIN_VALUE); + assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().documentCount(), serialized.require(id1.instance()).deployments().get(zone2).metrics().documentCount(), Double.MIN_VALUE); + assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().queryLatencyMillis(), serialized.require(id1.instance()).deployments().get(zone2).metrics().queryLatencyMillis(), Double.MIN_VALUE); + assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().writeLatencyMillis(), serialized.require(id1.instance()).deployments().get(zone2).metrics().writeLatencyMillis(), Double.MIN_VALUE); + assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().instant(), serialized.require(id1.instance()).deployments().get(zone2).metrics().instant()); + assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().warnings(), serialized.require(id1.instance()).deployments().get(zone2).metrics().warnings()); + } + + private Map<ClusterSpec.Id, ClusterInfo> createClusterInfo(int clusters, int hosts) { + Map<ClusterSpec.Id, ClusterInfo> result = new HashMap<>(); + + for (int cluster = 0; cluster < clusters; cluster++) { + List<String> hostnames = new ArrayList<>(); + for (int host = 0; host < hosts; host++) { + hostnames.add("hostname" + cluster*host + host); + } + + result.put(ClusterSpec.Id.from("id" + cluster), new ClusterInfo("flavor" + cluster, 10, + 2, 4, 50, ClusterSpec.Type.content, hostnames)); + } + return result; + } + + private Map<ClusterSpec.Id, ClusterUtilization> createClusterUtils(int clusters, double inc) { + Map<ClusterSpec.Id, ClusterUtilization> result = new HashMap<>(); + + ClusterUtilization util = new ClusterUtilization(0,0,0,0); + for (int cluster = 0; cluster < clusters; cluster++) { + double agg = cluster*inc; + result.put(ClusterSpec.Id.from("id" + cluster), new ClusterUtilization( + util.getMemory()+ agg, + util.getCpu()+ agg, + util.getDisk() + agg, + util.getDiskBusy() + agg)); + } + return result; + } + + @Test + public void testCompleteApplicationDeserialization() throws Exception { + byte[] applicationJson = Files.readAllBytes(testData.resolve("complete-application.json")); + APPLICATION_SERIALIZER.fromSlime(SlimeUtils.jsonToSlime(applicationJson)); + // ok if no error + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializerTest.java index 2d41d50dba3..3bb5e3d0c2b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializerTest.java @@ -46,7 +46,6 @@ import java.util.OptionalLong; import java.util.Set; import static com.yahoo.config.provision.SystemName.main; -import static com.yahoo.vespa.hosted.controller.ControllerTester.writable; import static java.util.Optional.empty; import static org.junit.Assert.assertEquals; @@ -216,7 +215,7 @@ public class InstanceSerializerTest { @Test public void testCompleteApplicationDeserialization() throws Exception { - byte[] applicationJson = Files.readAllBytes(testData.resolve("complete-application.json")); + byte[] applicationJson = Files.readAllBytes(testData.resolve("complete-instance.json")); INSTANCE_SERIALIZER.fromSlime(SlimeUtils.jsonToSlime(applicationJson)); // ok if no error } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json index 28f505e88ec..9e3bb2beb3b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json @@ -1,299 +1,34 @@ { - "id": "tenant1:app1:default", + "id": "tenant1:app1", + "internal": true, + "deploymentIssueId": "321", "deploymentSpecField": "<deployment version='1.0'>\n <test />\n <!--<staging />-->\n <prod global-service-id=\"foo\">\n <region active=\"true\">us-east-3</region>\n <region active=\"true\">us-west-1</region>\n </prod>\n</deployment>\n", "validationOverrides": "<validation-overrides>\n <allow until=\"2016-04-28\" comment=\"Renaming content cluster\">content-cluster-removal</allow>\n <allow until=\"2016-08-22\" comment=\"Migrating us-east-3 to C-2E\">cluster-size-reduction</allow>\n <allow until=\"2017-06-30\" comment=\"Test Vespa upgrade tests\">force-automatic-tenant-upgrade-test</allow>\n</validation-overrides>\n", - "deployments": [ - { - "zone": { - "environment": "prod", - "region": "us-west-1" - }, - "version": "6.173.62", - "deployTime": 1510837817704, - "applicationPackageRevision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" - } - }, - "clusterInfo": { - "cluster1": { - "flavor": "d-3-16-100", - "cost": 9, - "flavorCpu": 0, - "flavorMem": 0, - "flavorDisk": 0, - "clusterType": "container", - "hostnames": [ - "node1", - "node2" - ] - }, - "cluster2": { - "flavor": "d-12-64-400", - "cost": 38, - "flavorCpu": 0, - "flavorMem": 0, - "flavorDisk": 0, - "clusterType": "content", - "hostnames": [ - "node3", - "node4", - "node5" - ] - }, - "cluster3": { - "flavor": "d-12-64-400", - "cost": 38, - "flavorCpu": 0, - "flavorMem": 0, - "flavorDisk": 0, - "clusterType": "content", - "hostnames": [ - "node6", - "node7", - "node8", - "node9" - ] - } - }, - "clusterUtils": { - "cluster1": { - "cpu": 0.1720353499228221, - "mem": 0.4986146831512451, - "disk": 0.0617671330041831, - "diskbusy": 0 - }, - "cluster2": { - "cpu": 0.07505730001866318, - "mem": 0.7936344432830811, - "disk": 0.2260549694485994, - "diskbusy": 0 - }, - "cluster3": { - "cpu": 0.01712671480989384, - "mem": 0.0225852754983035, - "disk": 0.006084436856721915, - "diskbusy": 0 - } - }, - "metrics": { - "queriesPerSecond": 1.25, - "writesPerSecond": 43.83199977874756, - "documentCount": 525880277.9999999, - "queryLatencyMillis": 5.607503938674927, - "writeLatencyMillis": 20.57866265104621 - } - }, - { - "zone": { - "environment": "test", - "region": "us-east-1" - }, - "version": "6.173.62", - "deployTime": 1511256872316, - "applicationPackageRevision": { - "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" - } - }, - "clusterInfo": {}, - "clusterUtils": {}, - "metrics": { - "queriesPerSecond": 0, - "writesPerSecond": 0, - "documentCount": 0, - "queryLatencyMillis": 0, - "writeLatencyMillis": 0 - } - }, - { - "zone": { - "environment": "dev", - "region": "us-east-1" - }, - "version": "6.173.62", - "deployTime": 1510597489464, - "applicationPackageRevision": { - "applicationPackageHash": "59b883f263c2a3c23dfab249730097d7e0e1ed32" - }, - "clusterInfo": { - "cluster1": { - "flavor": "d-2-8-50", - "cost": 5, - "flavorCpu": 0, - "flavorMem": 0, - "flavorDisk": 0, - "clusterType": "container", - "hostnames": [ - "node1" - ] - }, - "cluster2": { - "flavor": "d-2-8-50", - "cost": 5, - "flavorCpu": 0, - "flavorMem": 0, - "flavorDisk": 0, - "clusterType": "content", - "hostnames": [ - "node2" - ] - }, - "cluster3": { - "flavor": "d-2-8-50", - "cost": 5, - "flavorCpu": 0, - "flavorMem": 0, - "flavorDisk": 0, - "clusterType": "content", - "hostnames": [ - "node3" - ] - } - }, - "clusterUtils": { - "cluster1": { - "cpu": 0.191833330678661, - "mem": 0.4625738318415235, - "disk": 0.05582004563850269, - "diskbusy": 0 - }, - "cluster2": { - "cpu": 0.2227037978608054, - "mem": 0.2051752598416401, - "disk": 0.05471533698695047, - "diskbusy": 0 - }, - "cluster3": { - "cpu": 0.1869410834020498, - "mem": 0.1691722576000564, - "disk": 0.04977374774258153, - "diskbusy": 0 - } - }, - "metrics": { - "queriesPerSecond": 0, - "writesPerSecond": 0, - "documentCount": 30916, - "queryLatencyMillis": 0, - "writeLatencyMillis": 0 - } - }, - { - "zone": { - "environment": "prod", - "region": "us-east-3" - }, - "version": "6.173.62", - "deployTime": 1510817190016, - "applicationPackageRevision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" - } - }, - "clusterInfo": { - "cluster1": { - "flavor": "d-3-16-100", - "cost": 9, - "flavorCpu": 0, - "flavorMem": 0, - "flavorDisk": 0, - "clusterType": "container", - "hostnames": [ - "node1", - "node2" - ] - }, - "cluster2": { - "flavor": "d-12-64-400", - "cost": 38, - "flavorCpu": 0, - "flavorMem": 0, - "flavorDisk": 0, - "clusterType": "content", - "hostnames": [ - "node1", - "node2", - "node3" - ] - }, - "cluster3": { - "flavor": "d-12-64-400", - "cost": 38, - "flavorCpu": 0, - "flavorMem": 0, - "flavorDisk": 0, - "clusterType": "content", - "hostnames": [ - "node1", - "node2", - "node3", - "node4" - ] - } - }, - "clusterUtils": { - "cluster1": { - "cpu": 0.2295038983007097, - "mem": 0.4627357390237263, - "disk": 0.05559941525894966, - "diskbusy": 0 - }, - "cluster2": { - "cpu": 0.05340429087579549, - "mem": 0.8107630891552372, - "disk": 0.226444914138854, - "diskbusy": 0 - }, - "cluster3": { - "cpu": 0.02148227413975218, - "mem": 0.02162174219104161, - "disk": 0.006057760545243265, - "diskbusy": 0 - } - }, - "metrics": { - "queriesPerSecond": 1.734000012278557, - "writesPerSecond": 44.59999895095825, - "documentCount": 525868193.9999999, - "queryLatencyMillis": 5.65284947195106, - "writeLatencyMillis": 17.34593812832452 - } + "projectId": 102889, + "deployingField": { + "buildNumber": 42, + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" } - ], - "deploymentJobs": { - "projectId": 102889, - "jobStatus": [ - { - "jobType": "staging-test", - "lastTriggered": { - "id": -1, - "version": "6.173.62", - "revision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" - } + }, + "outstandingChangeField": false, + "queryQuality": 100, + "writeQuality": 99.99894341115082, + "pemDeployKey": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----", + "instances": [ + { + "instanceName": "default", + "deployments": [ + { + "zone": { + "environment": "prod", + "region": "us-west-1" }, - "upgrade": true, - "reason": "system-test completed", - "at": 1510830134259 - }, - "lastCompleted": { - "id": 1184, "version": "6.173.62", - "revision": { + "deployTime": 1510837817704, + "applicationPackageRevision": { "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", "sourceRevision": { "repositoryField": "git@git.host:user/repo.git", @@ -301,112 +36,181 @@ "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" } }, - "upgrade": true, - "reason": "system-test completed", - "at": 1510830684960 - }, - "lastSuccess": { - "id": 1184, - "version": "6.173.62", - "revision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + "clusterInfo": { + "cluster1": { + "flavor": "d-3-16-100", + "cost": 9, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "container", + "hostnames": [ + "node1", + "node2" + ] + }, + "cluster2": { + "flavor": "d-12-64-400", + "cost": 38, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node3", + "node4", + "node5" + ] + }, + "cluster3": { + "flavor": "d-12-64-400", + "cost": 38, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node6", + "node7", + "node8", + "node9" + ] } }, - "upgrade": true, - "reason": "system-test completed", - "at": 1510830684960 - } - }, - { - "jobType": "component", - "lastCompleted": { - "id": 849, - "version": "6.174.156", - "upgrade": false, - "reason": "Application commit", - "at": 1511217733555 - }, - "lastSuccess": { - "id": 849, - "version": "6.174.156", - "upgrade": false, - "reason": "Application commit", - "at": 1511217733555 - } - }, - { - "jobType": "production-us-east-3", - "lastTriggered": { - "id": -1, - "version": "6.173.62", - "revision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + "clusterUtils": { + "cluster1": { + "cpu": 0.1720353499228221, + "mem": 0.4986146831512451, + "disk": 0.0617671330041831, + "diskbusy": 0 + }, + "cluster2": { + "cpu": 0.07505730001866318, + "mem": 0.7936344432830811, + "disk": 0.2260549694485994, + "diskbusy": 0 + }, + "cluster3": { + "cpu": 0.01712671480989384, + "mem": 0.0225852754983035, + "disk": 0.006084436856721915, + "diskbusy": 0 } }, - "upgrade": true, - "reason": "staging-test completed", - "at": 1510830685127 + "metrics": { + "queriesPerSecond": 1.25, + "writesPerSecond": 43.83199977874756, + "documentCount": 525880277.9999999, + "queryLatencyMillis": 5.607503938674927, + "writeLatencyMillis": 20.57866265104621 + } }, - "lastCompleted": { - "id": 923, + { + "zone": { + "environment": "test", + "region": "us-east-1" + }, "version": "6.173.62", - "revision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "deployTime": 1511256872316, + "applicationPackageRevision": { + "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", "sourceRevision": { "repositoryField": "git@git.host:user/repo.git", "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" } }, - "upgrade": true, - "reason": "staging-test completed", - "at": 1510837650046 + "clusterInfo": {}, + "clusterUtils": {}, + "metrics": { + "queriesPerSecond": 0, + "writesPerSecond": 0, + "documentCount": 0, + "queryLatencyMillis": 0, + "writeLatencyMillis": 0 + } }, - "lastSuccess": { - "id": 923, + { + "zone": { + "environment": "dev", + "region": "us-east-1" + }, "version": "6.173.62", - "revision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + "deployTime": 1510597489464, + "applicationPackageRevision": { + "applicationPackageHash": "59b883f263c2a3c23dfab249730097d7e0e1ed32" + }, + "clusterInfo": { + "cluster1": { + "flavor": "d-2-8-50", + "cost": 5, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "container", + "hostnames": [ + "node1" + ] + }, + "cluster2": { + "flavor": "d-2-8-50", + "cost": 5, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node2" + ] + }, + "cluster3": { + "flavor": "d-2-8-50", + "cost": 5, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node3" + ] } }, - "upgrade": true, - "reason": "staging-test completed", - "at": 1510837650046 - } - }, - { - "jobType": "production-us-west-1", - "lastTriggered": { - "id": -1, - "version": "6.173.62", - "revision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + "clusterUtils": { + "cluster1": { + "cpu": 0.191833330678661, + "mem": 0.4625738318415235, + "disk": 0.05582004563850269, + "diskbusy": 0 + }, + "cluster2": { + "cpu": 0.2227037978608054, + "mem": 0.2051752598416401, + "disk": 0.05471533698695047, + "diskbusy": 0 + }, + "cluster3": { + "cpu": 0.1869410834020498, + "mem": 0.1691722576000564, + "disk": 0.04977374774258153, + "diskbusy": 0 } }, - "upgrade": true, - "reason": "production-us-east-3 completed", - "at": 1510837650139 + "metrics": { + "queriesPerSecond": 0, + "writesPerSecond": 0, + "documentCount": 30916, + "queryLatencyMillis": 0, + "writeLatencyMillis": 0 + } }, - "lastCompleted": { - "id": 646, + { + "zone": { + "environment": "prod", + "region": "us-east-3" + }, "version": "6.173.62", - "revision": { + "deployTime": 1510817190016, + "applicationPackageRevision": { "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", "sourceRevision": { "repositoryField": "git@git.host:user/repo.git", @@ -414,121 +218,333 @@ "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" } }, - "upgrade": true, - "reason": "production-us-east-3 completed", - "at": 1510843559162 - }, - "lastSuccess": { - "id": 646, - "version": "6.173.62", - "revision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + "clusterInfo": { + "cluster1": { + "flavor": "d-3-16-100", + "cost": 9, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "container", + "hostnames": [ + "node1", + "node2" + ] + }, + "cluster2": { + "flavor": "d-12-64-400", + "cost": 38, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node1", + "node2", + "node3" + ] + }, + "cluster3": { + "flavor": "d-12-64-400", + "cost": 38, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node1", + "node2", + "node3", + "node4" + ] } }, - "upgrade": true, - "reason": "production-us-east-3 completed", - "at": 1510843559162 + "clusterUtils": { + "cluster1": { + "cpu": 0.2295038983007097, + "mem": 0.4627357390237263, + "disk": 0.05559941525894966, + "diskbusy": 0 + }, + "cluster2": { + "cpu": 0.05340429087579549, + "mem": 0.8107630891552372, + "disk": 0.226444914138854, + "diskbusy": 0 + }, + "cluster3": { + "cpu": 0.02148227413975218, + "mem": 0.02162174219104161, + "disk": 0.006057760545243265, + "diskbusy": 0 + } + }, + "metrics": { + "queriesPerSecond": 1.734000012278557, + "writesPerSecond": 44.59999895095825, + "documentCount": 525868193.9999999, + "queryLatencyMillis": 5.65284947195106, + "writeLatencyMillis": 17.34593812832452 + } } - }, - { - "jobType": "system-test", - "jobError": "unknown", - "lastTriggered": { - "id": -1, - "version": "6.173.62", - "revision": { - "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + ], + "deploymentJobs": { + "jobStatus": [ + { + "jobType": "staging-test", + "lastTriggered": { + "id": -1, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "system-test completed", + "at": 1510830134259 + }, + "lastCompleted": { + "id": 1184, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "system-test completed", + "at": 1510830684960 + }, + "lastSuccess": { + "id": 1184, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "system-test completed", + "at": 1510830684960 } }, - "upgrade": false, - "reason": "Available change in component", - "at": 1511256608649 - }, - "lastCompleted": { - "id": 1686, - "version": "6.173.62", - "revision": { - "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + { + "jobType": "component", + "lastCompleted": { + "id": 849, + "version": "6.174.156", + "upgrade": false, + "reason": "Application commit", + "at": 1511217733555 + }, + "lastSuccess": { + "id": 849, + "version": "6.174.156", + "upgrade": false, + "reason": "Application commit", + "at": 1511217733555 } }, - "upgrade": false, - "reason": "Available change in component", - "at": 1511256603353 - }, - "firstFailing": { - "id": 1659, - "version": "6.173.62", - "revision": { - "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + { + "jobType": "production-us-east-3", + "lastTriggered": { + "id": -1, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "staging-test completed", + "at": 1510830685127 + }, + "lastCompleted": { + "id": 923, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "staging-test completed", + "at": 1510837650046 + }, + "lastSuccess": { + "id": 923, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "staging-test completed", + "at": 1510837650046 } }, - "upgrade": false, - "reason": "component completed", - "at": 1511219070725 - }, - "lastSuccess": { - "id": 1658, - "version": "6.173.62", - "revision": { - "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + { + "jobType": "production-us-west-1", + "lastTriggered": { + "id": -1, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "production-us-east-3 completed", + "at": 1510837650139 + }, + "lastCompleted": { + "id": 646, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "production-us-east-3 completed", + "at": 1510843559162 + }, + "lastSuccess": { + "id": 646, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "production-us-east-3 completed", + "at": 1510843559162 } }, - "upgrade": true, - "reason": "Upgrading to 6.173.62", - "at": 1511175754163 + { + "jobType": "system-test", + "jobError": "unknown", + "lastTriggered": { + "id": -1, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + } + }, + "upgrade": false, + "reason": "Available change in component", + "at": 1511256608649 + }, + "lastCompleted": { + "id": 1686, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + } + }, + "upgrade": false, + "reason": "Available change in component", + "at": 1511256603353 + }, + "firstFailing": { + "id": 1659, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + } + }, + "upgrade": false, + "reason": "component completed", + "at": 1511219070725 + }, + "lastSuccess": { + "id": 1658, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "Upgrading to 6.173.62", + "at": 1511175754163 + } + } + ] + }, + "assignedRotations": [ + { + "rotationId": "rotation-foo", + "clusterId": "qrs", + "endpointId": "default" } - } - ] - }, - "deployingField": { - "buildNumber": 42, - "sourceRevision": { - "repositoryField": "git@git.host:user/repo.git", - "branchField": "origin/master", - "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" - } - }, - "outstandingChangeField": false, - "queryQuality": 100, - "writeQuality": 99.99894341115082, - "pemDeployKey": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----", - "assignedRotations": [ - { - "rotationId": "rotation-foo", - "clusterId": "qrs", - "endpointId": "default" - } - ], - "rotationStatus2": [ - { - "rotationId": "rotation-foo", - "status": [ + ], + "rotationStatus2": [ { - "environment": "prod", - "region": "us-east-3", - "state": "in" + "rotationId": "rotation-foo", + "status": [ + { + "environment": "prod", + "region": "us-east-3", + "state": "in" + } + ] } ] + }, + { + "instanceName": "empty-instance", + "deployments": [], + "deploymentJobs": { + "jobStatus": [] + }, + "assignedRotations": [], + "rotationStatus2": [] } ] } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-instance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-instance.json new file mode 100644 index 00000000000..28f505e88ec --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-instance.json @@ -0,0 +1,534 @@ +{ + "id": "tenant1:app1:default", + "deploymentSpecField": "<deployment version='1.0'>\n <test />\n <!--<staging />-->\n <prod global-service-id=\"foo\">\n <region active=\"true\">us-east-3</region>\n <region active=\"true\">us-west-1</region>\n </prod>\n</deployment>\n", + "validationOverrides": "<validation-overrides>\n <allow until=\"2016-04-28\" comment=\"Renaming content cluster\">content-cluster-removal</allow>\n <allow until=\"2016-08-22\" comment=\"Migrating us-east-3 to C-2E\">cluster-size-reduction</allow>\n <allow until=\"2017-06-30\" comment=\"Test Vespa upgrade tests\">force-automatic-tenant-upgrade-test</allow>\n</validation-overrides>\n", + "deployments": [ + { + "zone": { + "environment": "prod", + "region": "us-west-1" + }, + "version": "6.173.62", + "deployTime": 1510837817704, + "applicationPackageRevision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "clusterInfo": { + "cluster1": { + "flavor": "d-3-16-100", + "cost": 9, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "container", + "hostnames": [ + "node1", + "node2" + ] + }, + "cluster2": { + "flavor": "d-12-64-400", + "cost": 38, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node3", + "node4", + "node5" + ] + }, + "cluster3": { + "flavor": "d-12-64-400", + "cost": 38, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node6", + "node7", + "node8", + "node9" + ] + } + }, + "clusterUtils": { + "cluster1": { + "cpu": 0.1720353499228221, + "mem": 0.4986146831512451, + "disk": 0.0617671330041831, + "diskbusy": 0 + }, + "cluster2": { + "cpu": 0.07505730001866318, + "mem": 0.7936344432830811, + "disk": 0.2260549694485994, + "diskbusy": 0 + }, + "cluster3": { + "cpu": 0.01712671480989384, + "mem": 0.0225852754983035, + "disk": 0.006084436856721915, + "diskbusy": 0 + } + }, + "metrics": { + "queriesPerSecond": 1.25, + "writesPerSecond": 43.83199977874756, + "documentCount": 525880277.9999999, + "queryLatencyMillis": 5.607503938674927, + "writeLatencyMillis": 20.57866265104621 + } + }, + { + "zone": { + "environment": "test", + "region": "us-east-1" + }, + "version": "6.173.62", + "deployTime": 1511256872316, + "applicationPackageRevision": { + "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + } + }, + "clusterInfo": {}, + "clusterUtils": {}, + "metrics": { + "queriesPerSecond": 0, + "writesPerSecond": 0, + "documentCount": 0, + "queryLatencyMillis": 0, + "writeLatencyMillis": 0 + } + }, + { + "zone": { + "environment": "dev", + "region": "us-east-1" + }, + "version": "6.173.62", + "deployTime": 1510597489464, + "applicationPackageRevision": { + "applicationPackageHash": "59b883f263c2a3c23dfab249730097d7e0e1ed32" + }, + "clusterInfo": { + "cluster1": { + "flavor": "d-2-8-50", + "cost": 5, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "container", + "hostnames": [ + "node1" + ] + }, + "cluster2": { + "flavor": "d-2-8-50", + "cost": 5, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node2" + ] + }, + "cluster3": { + "flavor": "d-2-8-50", + "cost": 5, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node3" + ] + } + }, + "clusterUtils": { + "cluster1": { + "cpu": 0.191833330678661, + "mem": 0.4625738318415235, + "disk": 0.05582004563850269, + "diskbusy": 0 + }, + "cluster2": { + "cpu": 0.2227037978608054, + "mem": 0.2051752598416401, + "disk": 0.05471533698695047, + "diskbusy": 0 + }, + "cluster3": { + "cpu": 0.1869410834020498, + "mem": 0.1691722576000564, + "disk": 0.04977374774258153, + "diskbusy": 0 + } + }, + "metrics": { + "queriesPerSecond": 0, + "writesPerSecond": 0, + "documentCount": 30916, + "queryLatencyMillis": 0, + "writeLatencyMillis": 0 + } + }, + { + "zone": { + "environment": "prod", + "region": "us-east-3" + }, + "version": "6.173.62", + "deployTime": 1510817190016, + "applicationPackageRevision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "clusterInfo": { + "cluster1": { + "flavor": "d-3-16-100", + "cost": 9, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "container", + "hostnames": [ + "node1", + "node2" + ] + }, + "cluster2": { + "flavor": "d-12-64-400", + "cost": 38, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node1", + "node2", + "node3" + ] + }, + "cluster3": { + "flavor": "d-12-64-400", + "cost": 38, + "flavorCpu": 0, + "flavorMem": 0, + "flavorDisk": 0, + "clusterType": "content", + "hostnames": [ + "node1", + "node2", + "node3", + "node4" + ] + } + }, + "clusterUtils": { + "cluster1": { + "cpu": 0.2295038983007097, + "mem": 0.4627357390237263, + "disk": 0.05559941525894966, + "diskbusy": 0 + }, + "cluster2": { + "cpu": 0.05340429087579549, + "mem": 0.8107630891552372, + "disk": 0.226444914138854, + "diskbusy": 0 + }, + "cluster3": { + "cpu": 0.02148227413975218, + "mem": 0.02162174219104161, + "disk": 0.006057760545243265, + "diskbusy": 0 + } + }, + "metrics": { + "queriesPerSecond": 1.734000012278557, + "writesPerSecond": 44.59999895095825, + "documentCount": 525868193.9999999, + "queryLatencyMillis": 5.65284947195106, + "writeLatencyMillis": 17.34593812832452 + } + } + ], + "deploymentJobs": { + "projectId": 102889, + "jobStatus": [ + { + "jobType": "staging-test", + "lastTriggered": { + "id": -1, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "system-test completed", + "at": 1510830134259 + }, + "lastCompleted": { + "id": 1184, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "system-test completed", + "at": 1510830684960 + }, + "lastSuccess": { + "id": 1184, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "system-test completed", + "at": 1510830684960 + } + }, + { + "jobType": "component", + "lastCompleted": { + "id": 849, + "version": "6.174.156", + "upgrade": false, + "reason": "Application commit", + "at": 1511217733555 + }, + "lastSuccess": { + "id": 849, + "version": "6.174.156", + "upgrade": false, + "reason": "Application commit", + "at": 1511217733555 + } + }, + { + "jobType": "production-us-east-3", + "lastTriggered": { + "id": -1, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "staging-test completed", + "at": 1510830685127 + }, + "lastCompleted": { + "id": 923, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "staging-test completed", + "at": 1510837650046 + }, + "lastSuccess": { + "id": 923, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "staging-test completed", + "at": 1510837650046 + } + }, + { + "jobType": "production-us-west-1", + "lastTriggered": { + "id": -1, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "production-us-east-3 completed", + "at": 1510837650139 + }, + "lastCompleted": { + "id": 646, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "production-us-east-3 completed", + "at": 1510843559162 + }, + "lastSuccess": { + "id": 646, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "production-us-east-3 completed", + "at": 1510843559162 + } + }, + { + "jobType": "system-test", + "jobError": "unknown", + "lastTriggered": { + "id": -1, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + } + }, + "upgrade": false, + "reason": "Available change in component", + "at": 1511256608649 + }, + "lastCompleted": { + "id": 1686, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + } + }, + "upgrade": false, + "reason": "Available change in component", + "at": 1511256603353 + }, + "firstFailing": { + "id": 1659, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + } + }, + "upgrade": false, + "reason": "component completed", + "at": 1511219070725 + }, + "lastSuccess": { + "id": 1658, + "version": "6.173.62", + "revision": { + "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87", + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b" + } + }, + "upgrade": true, + "reason": "Upgrading to 6.173.62", + "at": 1511175754163 + } + } + ] + }, + "deployingField": { + "buildNumber": 42, + "sourceRevision": { + "repositoryField": "git@git.host:user/repo.git", + "branchField": "origin/master", + "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a" + } + }, + "outstandingChangeField": false, + "queryQuality": 100, + "writeQuality": 99.99894341115082, + "pemDeployKey": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----", + "assignedRotations": [ + { + "rotationId": "rotation-foo", + "clusterId": "qrs", + "endpointId": "default" + } + ], + "rotationStatus2": [ + { + "rotationId": "rotation-foo", + "status": [ + { + "environment": "prod", + "region": "us-east-3", + "state": "in" + } + ] + } + ] +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java index 290969623d8..2d8c937097a 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java @@ -22,6 +22,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockBuildService; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock; @@ -94,16 +95,17 @@ public class ContainerControllerTester { .uploadArtifact(applicationPackage) .submit(); DeploymentSteps steps = controller().applications().deploymentTrigger().steps(applicationPackage.deploymentSpec()); + // TODO jonmv: Connect instances from deployment spec to deployments below. boolean succeeding = true; for (var job : steps.jobs()) { if (!succeeding) return; var zone = job.zone(controller().system()); - deploy(application.id(), applicationPackage, zone); + deploy(application.id().defaultInstance(), applicationPackage, zone); if (failStaging && zone.environment() == Environment.staging) { succeeding = false; } if (zone.environment().isTest()) { - controller().applications().deactivate(application.id(), zone); + controller().applications().deactivate(application.id().defaultInstance(), zone); } jobCompletion(job).application(application).success(succeeding).projectId(projectId).submit(); } @@ -127,7 +129,7 @@ public class ContainerControllerTester { /* * Authorize action on tenantDomain/application for a given screwdriverId */ - public void authorize(AthenzDomain tenantDomain, ScrewdriverId screwdriverId, ApplicationAction action, ApplicationId id) { + public void authorize(AthenzDomain tenantDomain, ScrewdriverId screwdriverId, ApplicationAction action, TenantAndApplicationId id) { AthenzClientFactoryMock mock = (AthenzClientFactoryMock) containerTester.container().components() .getComponent(AthenzClientFactoryMock.class.getName()); 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 7158c293d7a..836e3c07763 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 @@ -50,6 +50,7 @@ import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.RoutingPolicy; import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock; @@ -319,7 +320,7 @@ public class ApplicationApiTest extends ControllerContainerTest { new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(app2.application().value())); // Trigger upgrade and then application change - controllerTester.controller().applications().deploymentTrigger().triggerChange(app2, Change.of(Version.fromString("7.0"))); + controllerTester.controller().applications().deploymentTrigger().triggerChange(TenantAndApplicationId.from(app2), Change.of(Version.fromString("7.0"))); controllerTester.jobCompletion(JobType.component) .application(app2) @@ -398,7 +399,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(USER_ID), new File("deployment.json")); - addIssues(controllerTester, ApplicationId.from("tenant1", "application1", "instance1")); + addIssues(controllerTester, TenantAndApplicationId.from("tenant1", "application1")); // GET at root, with "&recursive=deployment", returns info about all tenants, their applications and their deployments tester.assertResponse(request("/application/v4/", GET) .userIdentity(USER_ID) @@ -433,19 +434,19 @@ public class ApplicationApiTest extends ControllerContainerTest { // DELETE (cancel) ongoing change tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", DELETE) .userIdentity(HOSTED_VESPA_OPERATOR), - "{\"message\":\"Changed deployment from 'application change to 1.0.42-commit1' to 'no change' for application 'tenant1.application1.instance1'\"}"); + "{\"message\":\"Changed deployment from 'application change to 1.0.42-commit1' to 'no change' for application 'tenant1.application1'\"}"); // DELETE (cancel) again is a no-op tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", DELETE) .userIdentity(USER_ID) .data("{\"cancel\":\"all\"}"), - "{\"message\":\"No deployment in progress for application 'tenant1.application1.instance1' at this time\"}"); + "{\"message\":\"No deployment in progress for application 'tenant1.application1' at this time\"}"); // POST pinning to a given version to an application tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/pin", POST) .userIdentity(USER_ID) .data("6.1.0"), - "{\"message\":\"Triggered pin to 6.1 for tenant1.application1.instance1\"}"); + "{\"message\":\"Triggered pin to 6.1 for tenant1.application1\"}"); assertTrue("Action is logged to audit log", tester.controller().auditLogger().readLog().entries().stream() .anyMatch(entry -> entry.resource().equals("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/pin"))); @@ -457,7 +458,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // DELETE only the pin to a given version tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/pin", DELETE) .userIdentity(USER_ID), - "{\"message\":\"Changed deployment from 'pin to 6.1' to 'upgrade to 6.1' for application 'tenant1.application1.instance1'\"}"); + "{\"message\":\"Changed deployment from 'pin to 6.1' to 'upgrade to 6.1' for application 'tenant1.application1'\"}"); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET) .userIdentity(USER_ID), "{\"platform\":\"6.1\",\"pinned\":false}"); @@ -465,21 +466,21 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/pin", POST) .userIdentity(USER_ID) .data("6.1"), - "{\"message\":\"Triggered pin to 6.1 for tenant1.application1.instance1\"}"); + "{\"message\":\"Triggered pin to 6.1 for tenant1.application1\"}"); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET) .userIdentity(USER_ID), "{\"platform\":\"6.1\",\"pinned\":true}"); // DELETE only the version, but leave the pin tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/platform", DELETE) .userIdentity(USER_ID), - "{\"message\":\"Changed deployment from 'pin to 6.1' to 'pin to current platform' for application 'tenant1.application1.instance1'\"}"); + "{\"message\":\"Changed deployment from 'pin to 6.1' to 'pin to current platform' for application 'tenant1.application1'\"}"); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET) .userIdentity(USER_ID), "{\"pinned\":true}"); // DELETE also the pin to a given version tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/pin", DELETE) .userIdentity(USER_ID), - "{\"message\":\"Changed deployment from 'pin to current platform' to 'no change' for application 'tenant1.application1.instance1'\"}"); + "{\"message\":\"Changed deployment from 'pin to current platform' to 'no change' for application 'tenant1.application1'\"}"); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET) .userIdentity(USER_ID), "{}"); @@ -686,7 +687,7 @@ public class ApplicationApiTest extends ControllerContainerTest { new File("tenant-without-applications.json")); } - private void addIssues(ContainerControllerTester tester, ApplicationId id) { + private void addIssues(ContainerControllerTester tester, TenantAndApplicationId id) { tester.controller().applications().lockApplicationOrThrow(id, application -> tester.controller().applications().store(application.withDeploymentIssueId(IssueId.from("123")) .withOwnershipIssueId(IssueId.from("321")) @@ -1250,7 +1251,7 @@ public class ApplicationApiTest extends ControllerContainerTest { controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application.id()); controllerTester.jobCompletion(JobType.component) - .application(application.id()) + .application(application) .projectId(screwdriverProjectId) .uploadArtifact(applicationPackage) .submit(); @@ -1280,9 +1281,9 @@ public class ApplicationApiTest extends ControllerContainerTest { Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application.id()); - // Allow systemtest to succeed by notifying completion of system test + // Allow systemtest to succeed by notifying completion of component controllerTester.jobCompletion(JobType.component) - .application(application.id()) + .application(application) .projectId(screwdriverProjectId) .uploadArtifact(applicationPackage) .submit(); @@ -1379,7 +1380,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Allow systemtest to succeed by notifying completion of system test controllerTester.jobCompletion(JobType.component) - .application(application.id()) + .application(application) .projectId(screwdriverProjectId) .uploadArtifact(applicationPackage) .submit(); @@ -1414,9 +1415,9 @@ public class ApplicationApiTest extends ControllerContainerTest { Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application.id()); - // Allow systemtest to succeed by notifying completion of system test + // Allow systemtest to succeed by notifying completion of component controllerTester.jobCompletion(JobType.component) - .application(application.id()) + .application(application) .projectId(screwdriverProjectId) .uploadArtifact(applicationPackage) .submit(); @@ -1449,7 +1450,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .application(app) .projectId(projectId); job.type(JobType.component).uploadArtifact(applicationPackage).submit(); - controllerTester.deploy(app.id(), applicationPackage, TEST_ZONE); + controllerTester.deploy(app.id().defaultInstance(), applicationPackage, TEST_ZONE); job.type(JobType.systemTest).submit(); // Notifying about job started not by the controller fails @@ -1459,7 +1460,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .get(); tester.assertResponse(request, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Notified of completion " + "of system-test for tenant1.application1, but that has not been triggered; last was " + - controllerTester.controller().applications().require(app.id()).deploymentJobs().jobStatus().get(JobType.systemTest).lastTriggered().get().at() + "\"}", 400); + controllerTester.controller().applications().requireInstance(app.id().defaultInstance()).deploymentJobs().jobStatus().get(JobType.systemTest).lastTriggered().get().at() + "\"}", 400); // Notifying about unknown job fails request = request("/application/v4/tenant/tenant1/application/application1/jobreport", POST) @@ -1472,14 +1473,14 @@ public class ApplicationApiTest extends ControllerContainerTest { // ... and assert it was recorded JobStatus recordedStatus = - tester.controller().applications().get(app.id()).get().deploymentJobs().jobStatus().get(JobType.component); + tester.controller().applications().getInstance(app.id().defaultInstance()).get().deploymentJobs().jobStatus().get(JobType.component); assertNotNull("Status was recorded", recordedStatus); assertTrue(recordedStatus.isSuccess()); assertEquals(vespaVersion, recordedStatus.lastCompleted().get().platform()); recordedStatus = - tester.controller().applications().get(app.id()).get().deploymentJobs().jobStatus().get(JobType.productionApNortheast2); + tester.controller().applications().getInstance(app.id().defaultInstance()).get().deploymentJobs().jobStatus().get(JobType.productionApNortheast2); assertNull("Status of never-triggered jobs is empty", recordedStatus); assertTrue("All jobs have been run", tester.controller().applications().deploymentTrigger().jobsToRun().isEmpty()); } @@ -1501,17 +1502,16 @@ public class ApplicationApiTest extends ControllerContainerTest { .projectId(projectId); job.type(JobType.component).uploadArtifact(applicationPackage).submit(); - controllerTester.deploy(app.id(), applicationPackage, TEST_ZONE); + controllerTester.deploy(app.id().defaultInstance(), applicationPackage, TEST_ZONE); job.type(JobType.systemTest).submit(); - controllerTester.deploy(app.id(), applicationPackage, STAGING_ZONE); + controllerTester.deploy(app.id().defaultInstance(), applicationPackage, STAGING_ZONE); job.type(JobType.stagingTest).error(DeploymentJobs.JobError.outOfCapacity).submit(); // Appropriate error is recorded - JobStatus jobStatus = tester.controller().applications().get(app.id()) - .get() - .deploymentJobs() - .jobStatus() - .get(JobType.stagingTest); + JobStatus jobStatus = tester.controller().applications().getInstance(app.id().defaultInstance()).get() + .deploymentJobs() + .jobStatus() + .get(JobType.stagingTest); assertFalse(jobStatus.isSuccess()); assertEquals(DeploymentJobs.JobError.outOfCapacity, jobStatus.jobError().get()); } @@ -1524,12 +1524,12 @@ public class ApplicationApiTest extends ControllerContainerTest { .region("us-west-1") .build(); controllerTester.deployCompletely(app, applicationPackage, 1, false); - RoutingPolicy policy = new RoutingPolicy(app.id(), + RoutingPolicy policy = new RoutingPolicy(app.id().defaultInstance(), ClusterSpec.Id.from("default"), ZoneId.from(Environment.prod, RegionName.from("us-west-1")), HostName.from("lb-0-canonical-name"), Optional.of("dns-zone-1"), Set.of(EndpointId.of("c0"))); - tester.controller().curator().writeRoutingPolicies(app.id(), Set.of(policy)); + tester.controller().curator().writeRoutingPolicies(app.id().defaultInstance(), Set.of(policy)); // GET application tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET) @@ -1722,30 +1722,32 @@ public class ApplicationApiTest extends ControllerContainerTest { * This sets these values as if the maintainers has been ran. */ private void setDeploymentMaintainedInfo(ContainerControllerTester controllerTester) { - for (Instance instance : controllerTester.controller().applications().asList()) { - controllerTester.controller().applications().lockOrThrow(instance.id(), lockedApplication -> { + for (Application application : controllerTester.controller().applications().asList()) { + controllerTester.controller().applications().lockApplicationOrThrow(application.id(), lockedApplication -> { lockedApplication = lockedApplication.with(new ApplicationMetrics(0.5, 0.7)); - for (Deployment deployment : instance.deployments().values()) { - Map<ClusterSpec.Id, ClusterInfo> clusterInfo = new HashMap<>(); - List<String> hostnames = new ArrayList<>(); - hostnames.add("host1"); - hostnames.add("host2"); - clusterInfo.put(ClusterSpec.Id.from("cluster1"), - new ClusterInfo("flavor1", 37, 2, 4, 50, - ClusterSpec.Type.content, hostnames)); - Map<ClusterSpec.Id, ClusterUtilization> clusterUtils = new HashMap<>(); - clusterUtils.put(ClusterSpec.Id.from("cluster1"), new ClusterUtilization(0.3, 0.6, 0.4, 0.3)); - DeploymentMetrics metrics = new DeploymentMetrics(1, 2, 3, 4, 5, - Optional.of(Instant.ofEpochMilli(123123)), Map.of()); - - lockedApplication = lockedApplication - .withClusterInfo(deployment.zone(), clusterInfo) - .withClusterUtilization(deployment.zone(), clusterUtils) - .with(deployment.zone(), metrics) - .recordActivityAt(Instant.parse("2018-06-01T10:15:30.00Z"), deployment.zone()); + for (Instance instance : application.instances().values()) { + for (Deployment deployment : instance.deployments().values()) { + Map<ClusterSpec.Id, ClusterInfo> clusterInfo = new HashMap<>(); + List<String> hostnames = new ArrayList<>(); + hostnames.add("host1"); + hostnames.add("host2"); + clusterInfo.put(ClusterSpec.Id.from("cluster1"), + new ClusterInfo("flavor1", 37, 2, 4, 50, + ClusterSpec.Type.content, hostnames)); + Map<ClusterSpec.Id, ClusterUtilization> clusterUtils = new HashMap<>(); + clusterUtils.put(ClusterSpec.Id.from("cluster1"), new ClusterUtilization(0.3, 0.6, 0.4, 0.3)); + DeploymentMetrics metrics = new DeploymentMetrics(1, 2, 3, 4, 5, + Optional.of(Instant.ofEpochMilli(123123)), Map.of()); + + lockedApplication = lockedApplication.with(instance.name(), + lockedInstance -> lockedInstance.withClusterInfo(deployment.zone(), clusterInfo) + .withClusterUtilization(deployment.zone(), clusterUtils) + .with(deployment.zone(), metrics) + .recordActivityAt(Instant.parse("2018-06-01T10:15:30.00Z"), deployment.zone())); + } + controllerTester.controller().applications().store(lockedApplication); } - controllerTester.controller().applications().store(lockedApplication); }); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java index 2f009d16254..fb5f28e1963 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java @@ -33,7 +33,7 @@ import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobTy import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.stagingTest; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.systemTest; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Status.FAILURE; -import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.appId; +import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.instanceId; import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.applicationPackage; import static com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester.testerId; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentFailed; @@ -69,17 +69,17 @@ public class JobControllerApiHandlerHelperTest { // us-east-3 eats the deployment failure and fails before deployment, while us-west-1 fails after. tester.configServer().throwOnNextPrepare(new ConfigServerException(URI.create("url"), "ERROR!", INVALID_APPLICATION_PACKAGE, null)); tester.runner().run(); - assertEquals(deploymentFailed, tester.jobs().last(appId, productionUsEast3).get().status()); + assertEquals(deploymentFailed, tester.jobs().last(instanceId, productionUsEast3).get().status()); ZoneId usWest1 = productionUsWest1.zone(tester.tester().controller().system()); - tester.configServer().convergeServices(appId, usWest1); + tester.configServer().convergeServices(instanceId, usWest1); tester.configServer().convergeServices(testerId.id(), usWest1); - tester.setEndpoints(appId, usWest1); + tester.setEndpoints(instanceId, usWest1); tester.setEndpoints(testerId.id(), usWest1); tester.runner().run(); tester.cloud().set(FAILURE); tester.runner().run(); - assertEquals(testFailure, tester.jobs().last(appId, productionUsWest1).get().status()); + assertEquals(testFailure, tester.jobs().last(instanceId, productionUsWest1).get().status()); assertEquals(revision2, tester.instance().deployments().get(productionUsCentral1.zone(tester.tester().controller().system())).applicationVersion()); assertEquals(revision1, tester.instance().deployments().get(productionUsEast3.zone(tester.tester().controller().system())).applicationVersion()); assertEquals(revision2, tester.instance().deployments().get(productionUsWest1.zone(tester.tester().controller().system())).applicationVersion()); @@ -93,17 +93,17 @@ public class JobControllerApiHandlerHelperTest { tester.tester().readyJobTrigger().maintain(); // Starts a run for us-central-1. tester.tester().readyJobTrigger().maintain(); // Starts a new staging test run. tester.runner().run(); - assertEquals(running, tester.jobs().last(appId, productionUsCentral1).get().status()); - assertEquals(running, tester.jobs().last(appId, stagingTest).get().status()); + assertEquals(running, tester.jobs().last(instanceId, productionUsCentral1).get().status()); + assertEquals(running, tester.jobs().last(instanceId, stagingTest).get().status()); // Staging is expired, and the job fails and won't be retried immediately. - tester.tester().controller().applications().deactivate(appId, stagingTest.zone(tester.tester().controller().system())); + tester.tester().controller().applications().deactivate(instanceId, stagingTest.zone(tester.tester().controller().system())); tester.runner().run(); - assertEquals(installationFailed, tester.jobs().last(appId, stagingTest).get().status()); + assertEquals(installationFailed, tester.jobs().last(instanceId, stagingTest).get().status()); tester.clock().advance(Duration.ofMillis(100_000)); // More than the minute within which there are immediate retries. tester.tester().readyJobTrigger().maintain(); - assertEquals(installationFailed, tester.jobs().last(appId, stagingTest).get().status()); + assertEquals(installationFailed, tester.jobs().last(instanceId, stagingTest).get().status()); // System upgrades to a new version, which won't yet start. Version platform = new Version("7.1"); @@ -114,13 +114,13 @@ public class JobControllerApiHandlerHelperTest { // Only us-east-3 is verified, on revision1. // staging-test has 4 runs: one success without sources on revision1, one success from revision1 to revision2, // one success from revision2 to revision3 and one failure from revision1 to revision3. - assertResponse(JobControllerApiHandlerHelper.runResponse(tester.jobs().runs(appId, stagingTest), URI.create("https://some.url:43/root")), "staging-runs.json"); - assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(appId, productionUsEast3).get().id(), "0"), "us-east-3-log-without-first.json"); - assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.tester().controller(), appId, URI.create("https://some.url:43/root/")), "overview.json"); + assertResponse(JobControllerApiHandlerHelper.runResponse(tester.jobs().runs(instanceId, stagingTest), URI.create("https://some.url:43/root")), "staging-runs.json"); + assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(instanceId, productionUsEast3).get().id(), "0"), "us-east-3-log-without-first.json"); + assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.tester().controller(), instanceId, URI.create("https://some.url:43/root/")), "overview.json"); - tester.jobs().deploy(appId, JobType.devAwsUsEast2a, Optional.empty(), applicationPackage); + tester.jobs().deploy(instanceId, JobType.devAwsUsEast2a, Optional.empty(), applicationPackage); tester.runJob(JobType.devAwsUsEast2a); - assertResponse(JobControllerApiHandlerHelper.runResponse(tester.jobs().runs(appId, devAwsUsEast2a), URI.create("https://some.url:43/root")), "dev-aws-us-east-2a-runs.json"); + assertResponse(JobControllerApiHandlerHelper.runResponse(tester.jobs().runs(instanceId, devAwsUsEast2a), URI.create("https://some.url:43/root")), "dev-aws-us-east-2a-runs.json"); } @Test @@ -129,17 +129,17 @@ public class JobControllerApiHandlerHelperTest { tester.clock().setInstant(Instant.EPOCH); ZoneId zone = JobType.devUsEast1.zone(tester.tester().controller().system()); - tester.jobs().deploy(appId, JobType.devUsEast1, Optional.empty(), applicationPackage); + tester.jobs().deploy(instanceId, JobType.devUsEast1, Optional.empty(), applicationPackage); tester.configServer().setLogStream("1554970337.935104\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n"); - assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(appId, devUsEast1).get().id(), null), "dev-us-east-1-log-first-part.json"); + assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(instanceId, devUsEast1).get().id(), null), "dev-us-east-1-log-first-part.json"); tester.configServer().setLogStream("Nope, this won't be logged"); - tester.configServer().convergeServices(appId, zone); - tester.setEndpoints(appId, zone); + tester.configServer().convergeServices(instanceId, zone); + tester.setEndpoints(instanceId, zone); tester.runner().run(); - assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.tester().controller(), appId, URI.create("https://some.url:43/root")), "dev-overview.json"); - assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(appId, devUsEast1).get().id(), "9"), "dev-us-east-1-log-second-part.json"); + assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.tester().controller(), instanceId, URI.create("https://some.url:43/root")), "dev-overview.json"); + assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(instanceId, devUsEast1).get().id(), "9"), "dev-us-east-1-log-second-part.json"); } @Test @@ -153,7 +153,7 @@ public class JobControllerApiHandlerHelperTest { Optional.of(applicationPackage), new DeployOptions(true, Optional.empty(), false, false)); - assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.tester().controller(), appId, URI.create("https://some.url:43/root/")), + assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.tester().controller(), instanceId, URI.create("https://some.url:43/root/")), "jobs-direct-deployment.json"); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json index ad7e4f00027..c9ed2ad3391 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json @@ -1,4 +1,4 @@ { "error-code": "BAD_REQUEST", - "message": "Could not delete 'application 'tenant1.application1.instance1'': It has active deployments in: dev.us-west-1, prod.us-central-1" + "message": "Could not delete 'application 'tenant1.application1'': It has active deployments in: dev.us-west-1, prod.us-central-1" } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java index 35ec5b0e37e..8db759d855d 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java @@ -26,7 +26,7 @@ public class BadgeApiTest extends ControllerContainerTest { .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain"), AthenzService.from("service")) .region("us-west-1") .build(); - tester.controller().jobController().submit(application.id(), + tester.controller().jobController().submit(application.id().defaultInstance(), new SourceRevision("repository", "branch", "commit"), "foo@bar", 123, diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java index 128ea0e6f7e..084b235943e 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java @@ -48,7 +48,7 @@ public class DeploymentApiTest extends ControllerContainerTest { // Deploy once so that job information is stored, then remove the deployment tester.deployCompletely(instanceWithoutDeployment, applicationPackage, 3L, false); - tester.controller().applications().deactivate(instanceWithoutDeployment.id(), ZoneId.from("prod", "us-west-1")); + tester.controller().applications().deactivate(instanceWithoutDeployment.id().defaultInstance(), ZoneId.from("prod", "us-west-1")); // New version released version = Version.fromString("5.1"); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java index 820aa61617f..3b7d55f8cef 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java @@ -10,6 +10,7 @@ import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.api.role.Role; import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper; import org.junit.Before; import org.junit.Test; @@ -42,7 +43,8 @@ public class SignatureFilterTest { "PbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" + "-----END EC PRIVATE KEY-----\n"; - private static final ApplicationId id = ApplicationId.from("my-tenant", "my-app", "default"); + private static final TenantAndApplicationId appId = TenantAndApplicationId.from("my-tenant", "my-app"); + private static final ApplicationId id = appId.defaultInstance(); private ControllerTester tester; private ApplicationController applications; @@ -77,12 +79,12 @@ public class SignatureFilterTest { assertNull(signed.getAttribute(SecurityContext.ATTRIBUTE_NAME)); // Signed request gets no role when a non-matching key is stored for the application. - applications.lockOrThrow(id, application -> applications.store(application.withPemDeployKey(otherPublicKey))); + applications.lockApplicationOrThrow(appId, application -> applications.store(application.withPemDeployKey(otherPublicKey))); filter.filter(signed); assertNull(signed.getAttribute(SecurityContext.ATTRIBUTE_NAME)); // Signed request gets a build service role when a matching key is stored for the application. - applications.lockOrThrow(id, application -> applications.store(application.withPemDeployKey(publicKey))); + applications.lockApplicationOrThrow(appId, application -> applications.store(application.withPemDeployKey(publicKey))); assertTrue(filter.filter(signed).isEmpty()); SecurityContext securityContext = (SecurityContext) signed.getAttribute(SecurityContext.ATTRIBUTE_NAME); assertEquals("buildService@my-tenant.my-app", securityContext.principal().getName()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java index c563a4950de..25ae7c88e84 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java @@ -59,7 +59,7 @@ public class RotationRepositoryTest { tester = new DeploymentTester(new ControllerTester(rotationsConfig)); repository = tester.controller().applications().rotationRepository(); application = tester.createApplication("app1", "tenant1", 11L, 1L); - instance = tester.instance(application.id()); + instance = tester.defaultInstance(application.id()); } @Test @@ -68,12 +68,12 @@ public class RotationRepositoryTest { tester.deployCompletely(application, applicationPackage); Rotation expected = new Rotation(new RotationId("foo-1"), "foo-1.com"); - instance = tester.applications().require(instance.id()); + instance = tester.applications().requireInstance(instance.id()); assertEquals(List.of(expected.id()), rotationIds(instance.rotations())); assertEquals(URI.create("https://app1--tenant1.global.vespa.oath.cloud:4443/"), instance.endpointsIn(SystemName.main).main().get().url()); try (RotationLock lock = repository.lock()) { - Rotation rotation = repository.getOrAssignRotation(tester.applications().require(instance.id()), lock); + Rotation rotation = repository.getOrAssignRotation(application.deploymentSpec(), tester.applications().requireInstance(instance.id()), lock); assertEquals(expected, rotation); } @@ -85,7 +85,7 @@ public class RotationRepositoryTest { .searchDefinition("search foo { }") // Update application package so there is something to deploy .build(); tester.deployCompletely(application, applicationPackage, 43); - assertEquals(List.of(expected.id()), rotationIds(tester.applications().require(instance.id()).rotations())); + assertEquals(List.of(expected.id()), rotationIds(tester.applications().requireInstance(instance.id()).rotations())); } @Test @@ -95,10 +95,10 @@ public class RotationRepositoryTest { Application application2 = tester.createApplication("app2", "tenant2", 22L, 2L); tester.deployCompletely(application2, applicationPackage); - Instance instance2 = tester.applications().require(application2.id()); + Instance instance2 = tester.defaultInstance(application2.id()); try (RotationLock lock = repository.lock()) { - Rotation rotation = repository.getOrAssignRotation(instance2, lock); + Rotation rotation = repository.getOrAssignRotation(application2.deploymentSpec(), instance2, lock); Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com"); assertEquals(assignedRotation, rotation); } @@ -139,7 +139,7 @@ public class RotationRepositoryTest { .region("us-west-1") .build(); tester.deployCompletely(application, applicationPackage); - assertTrue(tester.instance(application.id()).rotations().isEmpty()); + assertTrue(tester.defaultInstance(application.id()).rotations().isEmpty()); } @Test @@ -151,8 +151,8 @@ public class RotationRepositoryTest { .build(); Application application2 = tester.createApplication("app2", "tenant2", 22L, 2L); tester.deployCompletely(application2, applicationPackage); - assertEquals(List.of(new RotationId("foo-1")), rotationIds(tester.instance(application2.id()).rotations())); - assertEquals("https://cd--app2--tenant2.global.vespa.oath.cloud:4443/", tester.instance(application2.id()) + assertEquals(List.of(new RotationId("foo-1")), rotationIds(tester.defaultInstance(application2.id()).rotations())); + assertEquals("https://cd--app2--tenant2.global.vespa.oath.cloud:4443/", tester.defaultInstance(application2.id()) .endpointsIn(SystemName.cd).main().get().url().toString()); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java index 3aae5b9b2ae..ba8309de286 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java @@ -158,13 +158,13 @@ public class VersionStatusTest { VespaVersion v1 = versions.get(0); assertEquals(version1, v1.versionNumber()); assertEquals("No applications are failing on version1.", ImmutableSet.of(), v1.statistics().failing()); - assertEquals("All applications have at least one active production deployment on version 1.", ImmutableSet.of(app1.id(), app2.id(), app3.id()), v1.statistics().production()); + assertEquals("All applications have at least one active production deployment on version 1.", ImmutableSet.of(app1.id().defaultInstance(), app2.id().defaultInstance(), app3.id().defaultInstance()), v1.statistics().production()); assertEquals("No applications have active deployment jobs on version1.", ImmutableSet.of(), v1.statistics().deploying()); VespaVersion v2 = versions.get(1); assertEquals(version2, v2.versionNumber()); - assertEquals("All applications have failed on version2 in at least one zone.", ImmutableSet.of(app1.id(), app2.id(), app3.id()), v2.statistics().failing()); - assertEquals("Only app2 has successfully deployed to production on version2.", ImmutableSet.of(app2.id()), v2.statistics().production()); + assertEquals("All applications have failed on version2 in at least one zone.", ImmutableSet.of(app1.id().defaultInstance(), app2.id().defaultInstance(), app3.id().defaultInstance()), v2.statistics().failing()); + assertEquals("Only app2 has successfully deployed to production on version2.", ImmutableSet.of(app2.id().defaultInstance()), v2.statistics().production()); // Should test the below, but can't easily be done with current test framework. This test passes in DeploymentApiTest. // assertEquals("All applications are being retried on version2.", ImmutableSet.of(app1.id(), app2.id(), app3.id()), v2.statistics().deploying()); } |