diff options
author | Jon Marius Venstad <venstad@gmail.com> | 2020-01-07 12:03:51 +0100 |
---|---|---|
committer | Jon Marius Venstad <venstad@gmail.com> | 2020-01-07 15:28:23 +0100 |
commit | 52df4364530a1dac305ff7da1c785a0e8988b707 (patch) | |
tree | a6dd75fb4eae764a25fa19aa721b5ff6bf099b57 /controller-server | |
parent | b5aaff70bd4ebdd5b659550b055a41e517378c5b (diff) |
Move changes to instances
Diffstat (limited to 'controller-server')
31 files changed, 624 insertions, 462 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 b962b260bf9..e37d6accd89 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,6 +1,7 @@ // 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.google.common.collect.ImmutableSortedMap; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentSpec; @@ -45,7 +46,6 @@ public class Application { private final ValidationOverrides validationOverrides; private final Optional<ApplicationVersion> latestVersion; private final OptionalLong projectId; - private final Change change; private final Optional<IssueId> deploymentIssueId; private final Optional<IssueId> ownershipIssueId; private final Optional<User> owner; @@ -56,21 +56,20 @@ public class Application { /** Creates an empty application. */ public Application(TenantAndApplicationId id, Instant now) { - this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Change.empty(), + this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(0, 0), Set.of(), OptionalLong.empty(), Optional.empty(), List.of()); } // DO NOT USE! For serialization purposes, only. public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, - Change change, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner, + Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys, OptionalLong projectId, Optional<ApplicationVersion> latestVersion, 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.change = Objects.requireNonNull(change, "change 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"); @@ -116,12 +115,6 @@ public class Application { return get(instance).orElseThrow(() -> new IllegalArgumentException("Unknown instance '" + instance + "' in '" + id + "'")); } - /** - * Returns base change for this application, i.e., the change that is deployed outside block windows. - * This is empty when no change is currently under deployment. - */ - public Change change() { return change; } - /** Returns ID of any open deployment issue filed for this */ public Optional<IssueId> deploymentIssueId() { return deploymentIssueId; 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 ecea7b09782..8339e1b8fdd 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 @@ -393,7 +393,7 @@ public class ApplicationController { applicationPackage = getApplicationPackage(instanceId, applicationVersion); applicationPackage = withTesterCertificate(applicationPackage, instanceId, jobType); - validateRun(application.get(), instance, zone, platformVersion, applicationVersion); + validateRun(application.get().require(instance), zone, platformVersion, applicationVersion); } if (controller.zoneRegistry().zones().directlyRouted().ids().contains(zone)) { @@ -887,14 +887,14 @@ public class ApplicationController { } /** Verify that we don't downgrade an existing production deployment. */ - private void validateRun(Application application, InstanceName instance, ZoneId zone, Version platformVersion, ApplicationVersion applicationVersion) { - Deployment deployment = application.require(instance).deployments().get(zone); + private void validateRun(Instance instance, ZoneId zone, Version platformVersion, ApplicationVersion applicationVersion) { + Deployment deployment = instance.deployments().get(zone); if ( zone.environment().isProduction() && deployment != null - && ( platformVersion.compareTo(deployment.version()) < 0 && ! application.change().isPinned() + && ( platformVersion.compareTo(deployment.version()) < 0 && ! instance.change().isPinned() || applicationVersion.compareTo(deployment.applicationVersion()) < 0)) 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).", - application.id().instance(instance), zone, platformVersion, applicationVersion, deployment.version(), deployment.applicationVersion())); + instance.id(), 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/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java index 09666110f70..3c5bc3b98ad 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 @@ -36,7 +36,6 @@ public class LockedApplication { private final Instant createdAt; private final DeploymentSpec deploymentSpec; private final ValidationOverrides validationOverrides; - private final Change change; private final Optional<IssueId> deploymentIssueId; private final Optional<IssueId> ownershipIssueId; private final Optional<User> owner; @@ -55,14 +54,14 @@ 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.change(), + application.deploymentSpec(), application.validationOverrides(), application.deploymentIssueId(), application.ownershipIssueId(), application.owner(), application.majorVersion(), application.metrics(), application.deployKeys(), application.projectId(), application.latestVersion(), application.instances()); } private LockedApplication(Lock lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, - ValidationOverrides validationOverrides, Change change, + ValidationOverrides validationOverrides, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys, OptionalLong projectId, Optional<ApplicationVersion> latestVersion, @@ -72,7 +71,6 @@ public class LockedApplication { this.createdAt = createdAt; this.deploymentSpec = deploymentSpec; this.validationOverrides = validationOverrides; - this.change = change; this.deploymentIssueId = deploymentIssueId; this.ownershipIssueId = ownershipIssueId; this.owner = owner; @@ -86,7 +84,7 @@ public class LockedApplication { /** Returns a read-only copy of this */ public Application get() { - return new Application(id, createdAt, deploymentSpec, validationOverrides, change, + return new Application(id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances.values()); } @@ -94,7 +92,7 @@ public class LockedApplication { public LockedApplication withNewInstance(InstanceName instance) { var instances = new HashMap<>(this.instances); instances.put(instance, new Instance(id.instance(instance))); - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } @@ -102,7 +100,7 @@ public class LockedApplication { 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, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } @@ -110,69 +108,63 @@ public class LockedApplication { public LockedApplication without(InstanceName instance) { var instances = new HashMap<>(this.instances); instances.remove(instance); - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } public LockedApplication withNewSubmission(ApplicationVersion latestVersion) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, Optional.of(latestVersion), instances); } public LockedApplication withProjectId(OptionalLong projectId) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } public LockedApplication withDeploymentIssueId(IssueId issueId) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, Optional.ofNullable(issueId), ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } public LockedApplication with(DeploymentSpec deploymentSpec) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } public LockedApplication with(ValidationOverrides validationOverrides) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, - deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, latestVersion, instances); - } - - public LockedApplication withChange(Change change) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } public LockedApplication withOwnershipIssueId(IssueId issueId) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, Optional.of(issueId), owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } public LockedApplication withOwner(User owner) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, Optional.of(owner), majorVersion, metrics, deployKeys, projectId, latestVersion, 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, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion == null ? OptionalInt.empty() : OptionalInt.of(majorVersion), metrics, deployKeys, projectId, latestVersion, instances); } public LockedApplication with(ApplicationMetrics metrics) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } @@ -180,7 +172,7 @@ public class LockedApplication { public LockedApplication withDeployKey(PublicKey pemDeployKey) { Set<PublicKey> keys = new LinkedHashSet<>(deployKeys); keys.add(pemDeployKey); - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys, projectId, latestVersion, instances); } @@ -188,7 +180,7 @@ public class LockedApplication { public LockedApplication withoutDeployKey(PublicKey pemDeployKey) { Set<PublicKey> keys = new LinkedHashSet<>(deployKeys); keys.remove(pemDeployKey); - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys, projectId, latestVersion, instances); } 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 e82029e53bf..10f99395170 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 @@ -49,11 +49,6 @@ public class ApplicationList extends AbstractFilteringList<Application, Applicat // ----------------------------------- Filters - /** Returns the subset of applications which are currently deploying a change */ - public ApplicationList deploying() { - return matching(application -> application.change().hasTargets()); - } - /** Returns the subset of applications which have at least one production deployment */ public ApplicationList withProductionDeployment() { return matching(application -> application.instances().values().stream() diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java index af63e8e5da7..8d082222721 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java @@ -8,6 +8,8 @@ import java.util.Objects; import java.util.Optional; import java.util.StringJoiner; +import static java.util.Objects.requireNonNull; + /** * The changes to an application we currently wish to complete deploying. * A goal of the system is to deploy platform and application versions separately. @@ -28,17 +30,17 @@ public final class Change { /** The application version we are changing to, or empty if none */ private final Optional<ApplicationVersion> application; + /** Whether this change is a pin to its contained Vespa version, or to the application's current. */ private final boolean pinned; private Change(Optional<Version> platform, Optional<ApplicationVersion> application, boolean pinned) { - Objects.requireNonNull(platform, "platform cannot be null"); - Objects.requireNonNull(application, "application cannot be null"); + this.platform = requireNonNull(platform, "platform cannot be null"); + this.application = requireNonNull(application, "application cannot be null"); if (application.isPresent() && application.get().isUnknown()) { throw new IllegalArgumentException("Application version to deploy must be a known version"); } - this.platform = platform; - this.application = application; this.pinned = pinned; + } public Change withoutPlatform() { 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 new file mode 100644 index 00000000000..cfe84658ce5 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java @@ -0,0 +1,154 @@ +package com.yahoo.vespa.hosted.controller.application; + +import com.google.common.collect.ImmutableMap; +import com.yahoo.collections.AbstractFilteringList; +import com.yahoo.component.Version; +import com.yahoo.config.application.api.DeploymentSpec; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.InstanceName; +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.deployment.DeploymentStatus; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList; +import com.yahoo.vespa.hosted.controller.deployment.RunStatus; + +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; + +public class InstanceList extends AbstractFilteringList<ApplicationId, InstanceList> { + + private final Map<ApplicationId, DeploymentStatus> statuses; + + private InstanceList(Collection<? extends ApplicationId> items, boolean negate, Map<ApplicationId, DeploymentStatus> statuses) { + super(items, negate, (i, n) -> new InstanceList(i, n, statuses)); + this.statuses = statuses; + } + + public static InstanceList from(DeploymentStatusList statuses) { + ImmutableMap.Builder<ApplicationId, DeploymentStatus> builder = ImmutableMap.builder(); + for (DeploymentStatus status : statuses.asList()) + for (InstanceName instance : status.application().deploymentSpec().instanceNames()) + builder.put(status.application().id().instance(instance), status); + Map<ApplicationId, DeploymentStatus> map = builder.build(); + return new InstanceList(map.keySet(), false, map); + } + + /** + * Returns the subset of instances that aren't pinned to an an earlier major version than the given one. + * + * @param targetMajorVersion the target major version which applications returned allows upgrading to + * @param defaultMajorVersion the default major version to assume for applications not specifying one + */ + public InstanceList allowMajorVersion(int targetMajorVersion, int defaultMajorVersion) { + return matching(id -> targetMajorVersion <= application(id).deploymentSpec().majorVersion() + .orElse(application(id).majorVersion() + .orElse(defaultMajorVersion))); + } + + /** Returns the subset of instances that are allowed to start deploying its outstanding revision at the given time */ + public InstanceList canChangeRevisionAt(Instant instant) { + return matching(id -> { + Change change = statuses.get(id).outstandingChange(id.instance()); + return change.hasTargets() && statuses.get(id).instanceSteps().get(id.instance()) + .readyAt(change) + .map(readyAt -> ! readyAt.isAfter(instant)).orElse(false); + }); + } + + /** Returns the subset of instances that are allowed to upgrade to the given version at the given time */ + public InstanceList canUpgradeAt(Version version, Instant instant) { + return matching(id -> statuses.get(id).instanceSteps().get(id.instance()) + .readyAt(Change.of(version)) + .map(readyAt -> ! readyAt.isAfter(instant)).orElse(false)); + } + + /** Returns the subset of instances which have at least one productiog deployment */ + public InstanceList withProductionDeployment() { + return matching(id -> instance(id).productionDeployments().size() > 0); + } + + /** Returns the subset of instances which have at least one deployment on a lower version than the given one */ + public InstanceList onLowerVersionThan(Version version) { + return matching(id -> instance(id).productionDeployments().values().stream() + .anyMatch(deployment -> deployment.version().isBefore(version))); + } + + /** Returns the subset of instances which have changes left to deploy; blocked, or deploying */ + public InstanceList withChanges() { + return matching(id -> instance(id).change().hasTargets() || statuses.get(id).outstandingChange(id.instance()).hasTargets()); + } + + /** Returns the subset of instances which are currently deploying a change */ + public InstanceList deploying() { + return matching(id -> instance(id).change().hasTargets()); + } + + /** Returns the subset of instances which currently have failing jobs on the given version */ + public InstanceList failingOn(Version version) { + return matching(id -> ! statuses.get(id).instanceJobs().get(id).failing().lastCompleted().on(version).isEmpty()); + } + + /** Returns the subset of instances which are not pinned to a certain Vespa version. */ + public InstanceList unpinned() { + return matching(id -> ! instance(id).change().isPinned()); + } + + /** Returns the subset of instances which are not currently failing any jobs. */ + public InstanceList failing() { + return matching(id -> ! statuses.get(id).instanceJobs().get(id).failing().not().withStatus(RunStatus.outOfCapacity).isEmpty()); + } + + /** Returns the subset of instances which are currently failing an upgrade. */ + public InstanceList failingUpgrade() { + return matching(id -> ! statuses.get(id).instanceJobs().get(id).failing().not().failingApplicationChange().isEmpty()); + } + + /** Returns the subset of instances which are upgrading (to any version), not considering block windows. */ + public InstanceList upgrading() { + return matching(id -> instance(id).change().platform().isPresent()); + } + + /** Returns the subset of instances which are currently upgrading to the given version */ + public InstanceList upgradingTo(Version version) { + return upgradingTo(List.of(version)); + } + + + /** Returns the subset of instances which are currently upgrading to the given version */ + public InstanceList upgradingTo(Collection<Version> versions) { + return matching(id -> versions.stream().anyMatch(version -> instance(id).change().platform().equals(Optional.of(version)))); + } + + public InstanceList with(DeploymentSpec.UpgradePolicy policy) { + return matching(id -> application(id).deploymentSpec().requireInstance(id.instance()).upgradePolicy() == policy); + } + + /** Returns the subset of instances which started failing on the given version */ + public InstanceList startedFailingOn(Version version) { + return matching(id -> ! statuses.get(id).instanceJobs().get(id).firstFailing().on(version).isEmpty()); + } + + /** Returns this list sorted by increasing oldest production deployment version. Applications without any deployments are ordered first. */ + public InstanceList byIncreasingDeployedVersion() { + return sortedBy(comparing(id -> instance(id).productionDeployments().values().stream() + .map(Deployment::version) + .min(naturalOrder()) + .orElse(Version.emptyVersion))); + } + + private Application application(ApplicationId id) { + return statuses.get(id).application(); + } + + private Instance instance(ApplicationId id) { + return application(id).require(id.instance()); + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java index b2b3720a9fd..0c5766ea54b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java @@ -12,15 +12,17 @@ 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.Application; +import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.Deployment; +import com.yahoo.vespa.hosted.controller.versions.VersionStatus; import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -40,6 +42,7 @@ import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toUnmodifiableList; +import static java.util.stream.Collectors.toUnmodifiableMap; /** * Status of the deployment jobs of an {@link Application}. @@ -77,14 +80,17 @@ public class DeploymentStatus { private final JobList allJobs; private final SystemName system; private final Version systemVersion; + private final Instant now; private final Map<JobId, StepStatus> jobSteps; private final List<StepStatus> allSteps; - public DeploymentStatus(Application application, Map<JobId, JobStatus> allJobs, SystemName system, Version systemVersion) { + public DeploymentStatus(Application application, Map<JobId, JobStatus> allJobs, SystemName system, + Version systemVersion, Instant now) { this.application = requireNonNull(application); this.allJobs = JobList.from(allJobs.values()); - this.system = system; - this.systemVersion = systemVersion; + this.system = requireNonNull(system); + this.systemVersion = requireNonNull(systemVersion); + this.now = requireNonNull(now); List<StepStatus> allSteps = new ArrayList<>(); this.jobSteps = jobDependencies(application.deploymentSpec(), allSteps); this.allSteps = List.copyOf(allSteps); @@ -127,14 +133,16 @@ public class DeploymentStatus { * and any test jobs for any oustanding change, which will likely be needed to lated deploy this change. */ public Map<JobId, List<Versions>> jobsToRun() { - Map<JobId, List<Versions>> jobs = jobsToRun(application().change()); - if (outstandingChange().isEmpty()) - return jobs; + Map<InstanceName, Change> changes = new LinkedHashMap<>(); + for (InstanceName instance : application.deploymentSpec().instanceNames()) + changes.put(instance, application.require(instance).change()); + Map<JobId, List<Versions>> jobs = jobsToRun(changes); // Add test jobs for any outstanding change. - var testJobs = jobsToRun(outstandingChange().onTopOf(application.change())) - .entrySet().stream() - .filter(entry -> ! entry.getKey().type().isProduction()); + for (InstanceName instance : application.deploymentSpec().instanceNames()) + changes.put(instance, outstandingChange(instance).onTopOf(application.require(instance).change())); + var testJobs = jobsToRun(changes).entrySet().stream() + .filter(entry -> ! entry.getKey().type().isProduction()); return Stream.concat(jobs.entrySet().stream(), testJobs) .collect(collectingAndThen(toMap(Map.Entry::getKey, @@ -145,8 +153,9 @@ public class DeploymentStatus { } /** The set of jobs that need to run for the given change to be considered complete. */ - public Map<JobId, List<Versions>> jobsToRun(Change change) { - Map<JobId, Versions> productionJobs = productionJobs(change); + public Map<JobId, List<Versions>> jobsToRun(Map<InstanceName, Change> changes) { + Map<JobId, Versions> productionJobs = new LinkedHashMap<>(); + changes.forEach((instance, change) -> productionJobs.putAll(productionJobs(instance, change))); Map<JobId, List<Versions>> testJobs = testJobs(productionJobs); Map<JobId, List<Versions>> jobs = new LinkedHashMap<>(testJobs); productionJobs.forEach((job, versions) -> jobs.put(job, List.of(versions))); @@ -156,6 +165,14 @@ public class DeploymentStatus { /** The step status for all steps in the deployment spec of this, which are jobs, in the same order as in the deployment spec. */ public Map<JobId, StepStatus> jobSteps() { return jobSteps; } + public Map<InstanceName, StepStatus> instanceSteps() { + ImmutableMap.Builder<InstanceName, StepStatus> instances = ImmutableMap.builder(); + for (StepStatus status : allSteps) + if (status instanceof InstanceStatus) + instances.put(status.instance(), status); + return instances.build(); + } + /** The step status for all steps in the deployment spec of this, in the same order as in the deployment spec. */ public List<StepStatus> allSteps() { return allSteps; } @@ -168,10 +185,10 @@ public class DeploymentStatus { * The change of this application's latest submission, if this upgrades any of its production deployments, * and has not yet started rolling out, due to some other change or a block window being present at the time of submission. */ - public Change outstandingChange() { + public Change outstandingChange(InstanceName instance) { return application.latestVersion().map(Change::of) - .filter(change -> application.change().application().map(change::upgrades).orElse(true)) - .filter(change -> ! jobsToRun(change).isEmpty()) + .filter(change -> application.require(instance).change().application().map(change::upgrades).orElse(true)) + .filter(change -> ! jobsToRun(Map.of(instance, change)).isEmpty()) .orElse(Change.empty()); } @@ -190,13 +207,15 @@ public class DeploymentStatus { } /** The production jobs that need to run to complete roll-out of the given change to production. */ - public Map<JobId, Versions> productionJobs(Change change) { - Map<JobId, Versions> jobs = new LinkedHashMap<>(); + public Map<JobId, Versions> productionJobs(InstanceName instance, Change change) { + ImmutableMap.Builder<JobId, Versions> jobs = ImmutableMap.builder(); jobSteps.forEach((job, step) -> { - if (job.type().isProduction() && step.completedAt(change, Optional.of(job)).isEmpty()) + if ( job.application().instance().equals(instance) + && job.type().isProduction() + && step.completedAt(change).isEmpty()) jobs.put(job, Versions.from(change, application, deploymentFor(job), systemVersion)); }); - return ImmutableMap.copyOf(jobs); + return jobs.build(); } /** The test jobs that need to run prior to the given production deployment jobs. */ @@ -237,7 +256,7 @@ public class DeploymentStatus { Map<JobId, StepStatus> dependencies = new LinkedHashMap<>(); List<StepStatus> previous = List.of(); for (DeploymentSpec.Step step : spec.steps()) - previous = fillStep(dependencies, allSteps, step, previous, spec.instanceNames().get(0)); + previous = fillStep(dependencies, allSteps, step, previous, null); return ImmutableMap.copyOf(dependencies); } @@ -246,8 +265,11 @@ public class DeploymentStatus { private List<StepStatus> fillStep(Map<JobId, StepStatus> dependencies, List<StepStatus> allSteps, DeploymentSpec.Step step, List<StepStatus> previous, InstanceName instance) { if (step.steps().isEmpty()) { + if (instance == null) + return previous; // Ignore test and staging outside all instances. + if ( ! step.delay().isZero()) { - StepStatus stepStatus = new DelayStatus((DeploymentSpec.Delay) step, previous); + StepStatus stepStatus = new DelayStatus((DeploymentSpec.Delay) step, previous, instance); allSteps.add(stepStatus); return List.of(stepStatus); } @@ -281,17 +303,19 @@ public class DeploymentStatus { return previous; } - // TODO jonmv: Make instance status as well, including block-change and upgrade policy, to keep track of change; - // set it equal to application's when dependencies are completed. if (step instanceof DeploymentInstanceSpec) { - instance = ((DeploymentInstanceSpec) step).name(); + DeploymentInstanceSpec spec = ((DeploymentInstanceSpec) step); + StepStatus instanceStatus = new InstanceStatus(spec, previous, now, application.require(spec.name())); + instance = spec.name(); + allSteps.add(instanceStatus); + previous = List.of(instanceStatus); for (JobType test : List.of(systemTest, stagingTest)) { JobId job = new JobId(application.id().instance(instance), test); if ( ! dependencies.containsKey(job)) { - var stepStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(test.environment()), List.of(), + var testStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(test.environment()), List.of(), this, job.application().instance(), test, false); - dependencies.put(job, stepStatus); - allSteps.add(stepStatus); + dependencies.put(job, testStatus); + allSteps.add(testStatus); } } } @@ -341,17 +365,13 @@ public class DeploymentStatus { private final StepType type; private final DeploymentSpec.Step step; private final List<StepStatus> dependencies; - private final Optional<InstanceName> instance; - - private StepStatus(StepType type, DeploymentSpec.Step step, List<StepStatus> dependencies) { - this(type, step, dependencies, null); - } + private final InstanceName instance; private StepStatus(StepType type, DeploymentSpec.Step step, List<StepStatus> dependencies, InstanceName instance) { this.type = requireNonNull(type); this.step = requireNonNull(step); this.dependencies = List.copyOf(dependencies); - this.instance = Optional.ofNullable(instance); + this.instance = instance; } /** The type of step this is. */ @@ -363,18 +383,23 @@ public class DeploymentStatus { /** The list of steps that need to be complete before this may start. */ public final List<StepStatus> dependencies() { return dependencies; } - /** The instance of this, if any. */ - public final Optional<InstanceName> instance() { return instance; } + /** The instance of this. */ + public final InstanceName instance() { return instance; } /** The id of the job this corresponds to, if any. */ public Optional<JobId> job() { return Optional.empty(); } /** The time at which this is, or was, complete on the given change and / or versions. */ - public abstract Optional<Instant> completedAt(Change change, Optional<JobId> dependent); + public Optional<Instant> completedAt(Change change) { return completedAt(change, Optional.empty()); } + + /** The time at which this is, or was, complete on the given change and / or versions. */ + abstract Optional<Instant> completedAt(Change change, Optional<JobId> dependent); + + /** The time at which this step is ready to run the specified change and / or versions. */ + public Optional<Instant> readyAt(Change change) { return readyAt(change, Optional.empty()); } /** The time at which this step is ready to run the specified change and / or versions. */ - // TODO jonmv: Hide the dependent parameter from the public. - public Optional<Instant> readyAt(Change change, Optional<JobId> dependent) { + Optional<Instant> readyAt(Change change, Optional<JobId> dependent) { return dependenciesCompletedAt(change, dependent) .map(ready -> Stream.of(blockedUntil(change), pausedUntil(), @@ -384,7 +409,7 @@ public class DeploymentStatus { } /** The time at which all dependencies completed on the given change and / or versions. */ - public Optional<Instant> dependenciesCompletedAt(Change change, Optional<JobId> dependent) { + Optional<Instant> dependenciesCompletedAt(Change change, Optional<JobId> dependent) { return dependencies.stream().allMatch(step -> step.completedAt(change, dependent).isPresent()) ? dependencies.stream().map(step -> step.completedAt(change, dependent).get()) .max(naturalOrder()) @@ -393,7 +418,6 @@ public class DeploymentStatus { } /** The time until which this step is blocked by a change blocker. */ - // TODO jonmv: readyAt for instance can be delayed by block window. Upgrade policy / confidence is something different. public Optional<Instant> blockedUntil(Change change) { return Optional.empty(); } /** The time until which this step is paused by user intervention. */ @@ -410,8 +434,8 @@ public class DeploymentStatus { private static class DelayStatus extends StepStatus { - private DelayStatus(DeploymentSpec.Delay step, List<StepStatus> dependencies) { - super(StepType.delay, step, dependencies); + private DelayStatus(DeploymentSpec.Delay step, List<StepStatus> dependencies, InstanceName instance) { + super(StepType.delay, step, dependencies, instance); } @Override @@ -422,6 +446,51 @@ public class DeploymentStatus { } + private static class InstanceStatus extends StepStatus { + + private final DeploymentInstanceSpec spec; + private final Instant now; + private final Instance instance; + + private InstanceStatus(DeploymentInstanceSpec spec, List<StepStatus> dependencies, Instant now, + Instance instance) { + super(StepType.instance, spec, dependencies, spec.name()); + this.spec = spec; + this.now = now; + this.instance = instance; + } + + /** Time of completion of its dependencies, if all parts of the given change are contained in the change for this instance. */ + @Override + public Optional<Instant> completedAt(Change change, Optional<JobId> dependent) { + return (change.platform().isEmpty() || change.platform().equals(instance.change().platform())) + && (change.application().isEmpty() || change.application().equals(instance.change().application())) + ? dependenciesCompletedAt(change, dependent) + : Optional.empty(); + } + + @Override + public Optional<Instant> blockedUntil(Change change) { + for (Instant current = now; now.plus(Duration.ofDays(7)).isAfter(current); ) { + boolean blocked = false; + for (DeploymentSpec.ChangeBlocker blocker : spec.changeBlocker()) { + while ( blocker.window().includes(current) + && now.plus(Duration.ofDays(7)).isAfter(current) + && ( change.platform().isPresent() && blocker.blocksVersions() + || change.application().isPresent() && blocker.blocksRevisions())) { + blocked = true; + current = current.plus(Duration.ofHours(1)).truncatedTo(ChronoUnit.HOURS); + } + } + if ( ! blocked) + return current == now ? Optional.empty() : Optional.of(current); + } + return Optional.of(Instant.MAX); + } + + } + + private static abstract class JobStepStatus extends StepStatus { private final JobStatus job; @@ -485,7 +554,7 @@ public class DeploymentStatus { && ! existingDeployment.map(Deployment::version).equals(change.platform())) return Optional.empty(); - Change fullChange = status.application().change(); + Change fullChange = status.application().require(instance).change(); if (existingDeployment.map(deployment -> ! (change.upgrades(deployment.version()) || change.upgrades(deployment.applicationVersion())) && (fullChange.downgrades(deployment.version()) || fullChange.downgrades(deployment.applicationVersion()))) .orElse(false)) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatusList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatusList.java index c3f6947ee4a..c14493a0b72 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatusList.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatusList.java @@ -28,63 +28,10 @@ public class DeploymentStatusList extends AbstractFilteringList<DeploymentStatus return new DeploymentStatusList(status, false); } - public ApplicationList asApplicationList() { - return ApplicationList.from(mapToList(DeploymentStatus::application)); - } - - public DeploymentStatusList forApplications(Collection<TenantAndApplicationId> applications) { - return matching(status -> applications.contains(status.application().id())); - } - - public DeploymentStatusList withProductionDeployment() { - return matching(status -> status.application().productionDeployments().values().stream() - .anyMatch(deployments -> ! deployments.isEmpty())); - } - - public DeploymentStatusList failing() { - return matching(DeploymentStatus::hasFailures); - } - - public DeploymentStatusList failingUpgrade() { - return matching(status -> status.instanceJobs().values().stream() - .anyMatch(jobs -> ! jobs.failing().not().failingApplicationChange().isEmpty())); - } - - /** Returns the subset of applications which are upgrading (to any version), not considering block windows. */ - public DeploymentStatusList upgrading() { - return matching(status -> status.application().change().platform().isPresent()); - } - - /** Returns the subset of applications which are currently upgrading to the given version */ - public DeploymentStatusList upgradingTo(Version version) { - return upgradingTo(List.of(version)); - } - - /** Returns the subset of applications which are currently upgrading to the given version */ - public DeploymentStatusList upgradingTo(Collection<Version> versions) { - return matching(status -> versions.stream().anyMatch(version -> status.application().change().platform().equals(Optional.of(version)))); - } - - /** Returns the subset of applications which are not pinned to a certain Vespa version. */ - public DeploymentStatusList unpinned() { - return matching(status -> ! status.application().change().isPinned()); - } - - /** Returns the subset of applications which has the given upgrade policy */ - // TODO jonmv: Make this instance based when instances are orchestrated, and deployments reported per instance. - public DeploymentStatusList with(DeploymentSpec.UpgradePolicy policy) { - return matching(status -> status.application().deploymentSpec().instances().stream() - .anyMatch(instance -> instance.upgradePolicy() == policy)); - } - /** Returns the subset of applications which have changes left to deploy; blocked, or deploying */ public DeploymentStatusList withChanges() { - return matching(status -> status.application().change().hasTargets() || status.outstandingChange().hasTargets()); - } - - /** Returns the subset of applications which are currently deploying a change */ - public DeploymentStatusList deploying() { - return matching(status -> status.application().change().hasTargets()); + return matching(status -> status.application().instances().values().stream() + .anyMatch(instance -> instance.change().hasTargets() || status.outstandingChange(instance.name()).hasTargets())); } /** Returns the subset of applications which have been failing an upgrade to the given version since the given instant */ @@ -99,24 +46,6 @@ public class DeploymentStatusList extends AbstractFilteringList<DeploymentStatus .anyMatch(jobs -> failingApplicationChangeSince(jobs, threshold))); } - /** Returns the subset of applications which currently have failing jobs on the given version */ - public DeploymentStatusList failingOn(Version version) { - return matching(status -> status.instanceJobs().values().stream() - .anyMatch(jobs -> failingOn(jobs, version))); - } - - /** Returns the subset of applications which started failing on the given version */ - public DeploymentStatusList startedFailingOn(Version version) { - return matching(status -> status.instanceJobs().values().stream() - .anyMatch(jobs -> ! jobs.firstFailing().on(version).isEmpty())); - } - - private static boolean failingOn(JobList jobs, Version version) { - return ! jobs.failing() - .lastCompleted().on(version) - .isEmpty(); - } - private static boolean failingUpgradeToVersionSince(JobList jobs, Version version, Instant threshold) { return ! jobs.not().failingApplicationChange() .firstFailing().endedBefore(threshold) 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 482df205af1..16fafeca6d0 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 @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.deployment; import com.yahoo.config.application.api.DeploymentInstanceSpec; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.InstanceName; import com.yahoo.log.LogLevel; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.ApplicationController; @@ -16,7 +17,9 @@ 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.Change; import com.yahoo.vespa.hosted.controller.application.Deployment; +import com.yahoo.vespa.hosted.controller.application.InstanceList; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; +import com.yahoo.vespa.hosted.controller.versions.VersionStatus; import java.time.Clock; import java.time.Duration; @@ -29,6 +32,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.OptionalLong; +import java.util.function.Function; import java.util.function.Supplier; import java.util.logging.Logger; @@ -39,7 +43,7 @@ import static java.util.stream.Collectors.toList; /** * Responsible for scheduling deployment jobs in a build system and keeping - * {@link Application#change()} in sync with what is scheduled. + * {@link Instance#change()} in sync with what is scheduled. * * This class is multi-thread safe. * @@ -74,16 +78,22 @@ public class DeploymentTrigger { } applications().lockApplicationOrThrow(id, application -> { - if (acceptNewApplicationVersion(application.get())) { - application = application.withChange(application.get().change().with(version)); - for (Run run : jobs.active(id)) - if ( ! run.id().type().environment().isManuallyDeployed()) - jobs.abort(run.id()); - } - application = application.withProjectId(OptionalLong.of(projectId)); application = application.withNewSubmission(version); - applications().store(application.withChange(remainingChange(application.get()))); + DeploymentStatus status = jobs.deploymentStatus(application.get()); + for (ApplicationId instanceId : InstanceList.from(DeploymentStatusList.from(List.of(status))) + .canChangeRevisionAt(clock.instant()).asList()) + if (acceptNewApplicationVersion(status, instanceId.instance())) { + application = application.with(instanceId.instance(), + instance -> instance.withChange(status.outstandingChange(instance.name()).onTopOf(instance.change()))); + for (Run run : jobs.active(instanceId)) + if ( ! run.id().type().environment().isManuallyDeployed()) + jobs.abort(run.id()); + } + + for (InstanceName instanceName : application.get().instances().keySet()) + application = application.with(instanceName, instance -> instance.withChange(remainingChange(instance, status))); + applications().store(application); }); } @@ -98,7 +108,8 @@ public class DeploymentTrigger { } applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> - applications().store(application.withChange(remainingChange(application.get())))); + applications().store(application.with(id.instance(), + instance -> instance.withChange(remainingChange(instance, jobs.deploymentStatus(application.get())))))); } /** @@ -149,13 +160,13 @@ public class DeploymentTrigger { Instance instance = application.require(applicationId.instance()); DeploymentStatus status = jobs.deploymentStatus(application); JobId job = new JobId(instance.id(), jobType); - Versions versions = Versions.from(application.change(), application, status.deploymentFor(job), controller.systemVersion()); + Versions versions = Versions.from(instance.change(), application, status.deploymentFor(job), controller.systemVersion()); String reason = "Job triggered manually by " + user; Map<JobId, List<Versions>> jobs = status.testJobs(Map.of(job, versions)); if (jobs.isEmpty() || ! requireTests) jobs = Map.of(job, List.of(versions)); jobs.forEach((jobId, versionsList) -> { - trigger(deploymentJob(instance, versionsList.get(0), application.change(), jobId.type(), status.jobs().get(jobId).get(), reason, clock.instant())); + trigger(deploymentJob(instance, versionsList.get(0), jobId.type(), status.jobs().get(jobId).get(), reason, clock.instant())); }); return List.copyOf(jobs.keySet()); } @@ -171,33 +182,42 @@ public class DeploymentTrigger { } /** Triggers a change of this application, unless it already has a change. */ - public void triggerChange(TenantAndApplicationId applicationId, Change change) { - applications().lockApplicationOrThrow(applicationId, application -> { - if ( ! application.get().change().hasTargets()) - forceChange(applicationId, change); + public void triggerChange(ApplicationId instanceId, Change change) { + applications().lockApplicationOrThrow(TenantAndApplicationId.from(instanceId), application -> { + if ( ! application.get().require(instanceId.instance()).change().hasTargets()) + forceChange(instanceId, change); }); } - /** Overrides the given application's platform and application changes with any contained in the given change. */ - public void forceChange(TenantAndApplicationId applicationId, Change change) { - applications().lockApplicationOrThrow(applicationId, application -> { - applications().store(application.withChange(change.onTopOf(application.get().change()))); + public void triggerNewRevision(ApplicationId id) { + applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { + DeploymentStatus status = jobs.deploymentStatus(application.get()); + triggerChange(id, status.outstandingChange(id.instance())); + }); + } + + /** Overrides the given instance's platform and application changes with any contained in the given change. */ + public void forceChange(ApplicationId instanceId, Change change) { + applications().lockApplicationOrThrow(TenantAndApplicationId.from(instanceId), application -> { + applications().store(application.with(instanceId.instance(), + instance -> instance.withChange(change.onTopOf(application.get().require(instanceId.instance()).change())))); }); } /** Cancels the indicated part of the given application's change. */ - public void cancelChange(TenantAndApplicationId applicationId, ChangesToCancel cancellation) { - applications().lockApplicationOrThrow(applicationId, application -> { + public void cancelChange(ApplicationId instanceId, ChangesToCancel cancellation) { + applications().lockApplicationOrThrow(TenantAndApplicationId.from(instanceId), application -> { Change change; switch (cancellation) { case ALL: change = Change.empty(); break; case VERSIONS: change = Change.empty().withPin(); break; - case PLATFORM: change = application.get().change().withoutPlatform(); break; - case APPLICATION: change = application.get().change().withoutApplication(); break; - case PIN: change = application.get().change().withoutPin(); break; + case PLATFORM: change = application.get().require(instanceId.instance()).change().withoutPlatform(); break; + case APPLICATION: change = application.get().require(instanceId.instance()).change().withoutApplication(); break; + case PIN: change = application.get().require(instanceId.instance()).change().withoutPin(); break; default: throw new IllegalArgumentException("Unknown cancellation choice '" + cancellation + "'!"); } - applications().store(application.withChange(change)); + applications().store(application.with(instanceId.instance(), + instance -> instance.withChange(change))); }); } @@ -234,19 +254,18 @@ public class DeploymentTrigger { List<Job> jobs = new ArrayList<>(); status.jobsToRun().forEach((job, versionsList) -> { for (Versions versions : versionsList) - status.jobSteps().get(job).readyAt(status.application().change(), Optional.empty()) + status.jobSteps().get(job).readyAt(status.application().require(job.application().instance()).change()) .filter(readyAt -> ! clock.instant().isBefore(readyAt)) .filter(__ -> ! status.jobs().get(job).get().isRunning()) .filter(__ -> ! (job.type().isProduction() && isSuspendedInAnotherZone(status.application(), job))) .ifPresent(readyAt -> { - jobs.add(deploymentJob(status.application().require(job.application().instance()), - versions, - status.application().change(), - job.type(), - status.instanceJobs(job.application().instance()).get(job.type()), - "unknown reason", - readyAt)); - }); + jobs.add(deploymentJob(status.application().require(job.application().instance()), + versions, + job.type(), + status.instanceJobs(job.application().instance()).get(job.type()), + "unknown reason", + readyAt)); + }); }); return Collections.unmodifiableList(jobs); } @@ -330,28 +349,25 @@ public class DeploymentTrigger { // ---------- Change management o_O ---------- - private boolean acceptNewApplicationVersion(Application application) { - if ( ! application.deploymentSpec().instances().stream() - .allMatch(instance -> instance.canChangeRevisionAt(clock.instant()))) return false; - if (application.change().application().isPresent()) return true; // Replacing a previous application change is ok. - if (jobs.deploymentStatus(application).hasFailures()) return true; // Allow changes to fix upgrade problems. - return application.change().platform().isEmpty(); + private boolean acceptNewApplicationVersion(DeploymentStatus status, InstanceName instance) { + if (status.application().require(instance).change().application().isPresent()) return true; // Replacing a previous application change is ok. + if (status.hasFailures()) return true; // Allow changes to fix upgrade problems. + return status.application().require(instance).change().platform().isEmpty(); } - private Change remainingChange(Application application) { - Change change = application.change(); - DeploymentStatus status = jobs.deploymentStatus(application); - if (status.jobsToRun(status.application().change().withoutApplication()).isEmpty()) + private Change remainingChange(Instance instance, DeploymentStatus status) { + Change change = instance.change(); + if (status.jobsToRun(Map.of(instance.name(), instance.change().withoutApplication())).isEmpty()) change = change.withoutPlatform(); - if (status.jobsToRun(status.application().change().withoutPlatform()).isEmpty()) + if (status.jobsToRun(Map.of(instance.name(), instance.change().withoutPlatform())).isEmpty()) change = change.withoutApplication(); return change; } // ---------- Version and job helpers ---------- - private Job deploymentJob(Instance instance, Versions versions, Change change, JobType jobType, JobStatus jobStatus, String reason, Instant availableSince) { - return new Job(instance, versions, jobType, availableSince, jobStatus.isOutOfCapacity(), change.application().isPresent()); + private Job deploymentJob(Instance instance, Versions versions, JobType jobType, JobStatus jobStatus, String reason, Instant availableSince) { + return new Job(instance, versions, jobType, availableSince, jobStatus.isOutOfCapacity(), instance.change().application().isPresent()); } // ---------- Data containers ---------- 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 5798a11758d..20c64751335 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 @@ -588,7 +588,7 @@ public class InternalStepRunner implements StepRunner { private void sendNotification(Run run, DualLogger logger) { Application application = controller.applications().requireApplication(TenantAndApplicationId.from(run.id().application())); Notifications notifications = application.deploymentSpec().requireInstance(run.id().application().instance()).notifications(); - boolean newCommit = application.change().application() + boolean newCommit = application.require(run.id().application().instance()).change().application() .map(run.versions().targetApplication()::equals) .orElse(false); When when = newCommit ? failingCommit : failing; 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 61b86df6190..f9ad2ebf553 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 @@ -270,7 +270,7 @@ public class JobController { .collect(toUnmodifiableList()); } - /** Returns a list of all active runs for the given instance. */ + /** Returns a list of all active runs for the given application. */ public List<Run> active(TenantAndApplicationId id) { return copyOf(controller.applications().requireApplication(id).instances().keySet().stream() .flatMap(name -> Stream.of(JobType.values()) @@ -280,6 +280,15 @@ public class JobController { .iterator()); } + /** Returns a list of all active runs for the given instance. */ + public List<Run> active(ApplicationId id) { + return copyOf(Stream.of(JobType.values()) + .map(type -> last(id, type)) + .flatMap(Optional::stream) + .filter(run -> ! run.hasEnded()) + .iterator()); + } + /** Returns the job status of the given job, possibly empty. */ public JobStatus jobStatus(JobId id) { return new JobStatus(id, runs(id)); @@ -292,7 +301,8 @@ public class JobController { .collect(toUnmodifiableMap(job -> job, job -> jobStatus(job))), controller.system(), - controller.systemVersion()); + controller.systemVersion(), + controller.clock().instant()); } /** Adds deployment status to each of the given applications. */ 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 87d4f3f0b9a..c4135120e3d 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 @@ -96,11 +96,6 @@ public class Versions { targetApplication.id()); } - /** Create versions using change contained in application */ - 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, Application application, Optional<Deployment> deployment, Version defaultPlatformVersion) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java index e43877cb7e5..ec209c5ed98 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java @@ -60,8 +60,8 @@ public class ControllerMaintenance extends AbstractComponent { deploymentExpirer = new DeploymentExpirer(controller, maintenanceInterval, jobControl); deploymentIssueReporter = new DeploymentIssueReporter(controller, controller.serviceRegistry().deploymentIssues(), maintenanceInterval, jobControl); metricsReporter = new MetricsReporter(controller, metric, jobControl); - outstandingChangeDeployer = new OutstandingChangeDeployer(controller, Duration.ofMinutes(1), jobControl); - versionStatusUpdater = new VersionStatusUpdater(controller, Duration.ofMinutes(1), jobControl); + outstandingChangeDeployer = new OutstandingChangeDeployer(controller, Duration.ofMinutes(3), jobControl); + versionStatusUpdater = new VersionStatusUpdater(controller, Duration.ofMinutes(3), jobControl); upgrader = new Upgrader(controller, maintenanceInterval, jobControl, curator); readyJobsTrigger = new ReadyJobsTrigger(controller, Duration.ofMinutes(1), jobControl); clusterInfoMaintainer = new ClusterInfoMaintainer(controller, Duration.ofHours(2), jobControl); 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 d398012a71b..9f3dcfb38a8 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,10 +1,18 @@ // 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.config.application.api.DeploymentInstanceSpec; +import com.yahoo.config.application.api.DeploymentSpec; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.InstanceName; 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.Change; +import com.yahoo.vespa.hosted.controller.application.InstanceList; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList; import java.time.Duration; @@ -21,14 +29,12 @@ public class OutstandingChangeDeployer extends Maintainer { @Override protected void maintain() { - for (Application application : controller().applications().asList()) { - Change change = controller().jobController().deploymentStatus(application).outstandingChange(); - if ( change.hasTargets() - && application.deploymentSpec().instances().stream() - .allMatch(instance -> instance.canChangeRevisionAt(controller().clock().instant()))) { - controller().applications().deploymentTrigger().triggerChange(application.id(), change); - } - } + ApplicationList applications = ApplicationList.from(controller().applications().asList()) + .withProductionDeployment() + .withDeploymentSpec(); + for (ApplicationId instance : InstanceList.from(controller().jobController().deploymentStatuses(applications)) + .canChangeRevisionAt(controller().clock().instant()).asList()) + controller().applications().deploymentTrigger().triggerNewRevision(instance); } } 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 49a505475c6..da24d758320 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 @@ -3,19 +3,18 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy; +import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.curator.Lock; -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.Change; -import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList; +import com.yahoo.vespa.hosted.controller.application.InstanceList; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence; import java.time.Duration; import java.util.Collection; -import java.util.Comparator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -26,6 +25,8 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.PLATFORM; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; /** * Maintenance job which schedules applications for Vespa version upgrade @@ -57,35 +58,35 @@ public class Upgrader extends Maintainer { // Cancel upgrades to broken targets (let other ongoing upgrades complete to avoid starvation) for (VespaVersion version : controller().versionStatus().versions()) { if (version.confidence() == Confidence.broken) - cancelUpgradesOf(applications().upgradingTo(version.versionNumber()) - .not().with(UpgradePolicy.canary), + cancelUpgradesOf(instances().upgradingTo(version.versionNumber()) + .not().with(UpgradePolicy.canary), version.versionNumber() + " is broken"); } // Canaries should always try the canary target - cancelUpgradesOf(applications().upgrading() - .not().upgradingTo(canaryTarget) - .with(UpgradePolicy.canary), + cancelUpgradesOf(instances().upgrading() + .not().upgradingTo(canaryTarget) + .with(UpgradePolicy.canary), "Outdated target version for Canaries"); // Cancel *failed* upgrades to earlier versions, as the new version may fix it String reason = "Failing on outdated version"; - cancelUpgradesOf(applications().upgrading() - .failing() - .not().upgradingTo(defaultTargets) - .with(UpgradePolicy.defaultPolicy), + cancelUpgradesOf(instances().upgrading() + .failing() + .not().upgradingTo(defaultTargets) + .with(UpgradePolicy.defaultPolicy), reason); - cancelUpgradesOf(applications().upgrading() - .failing() - .not().upgradingTo(conservativeTargets) - .with(UpgradePolicy.conservative), + cancelUpgradesOf(instances().upgrading() + .failing() + .not().upgradingTo(conservativeTargets) + .with(UpgradePolicy.conservative), reason); // Schedule the right upgrades - DeploymentStatusList applications = applications(); - upgrade(applications.with(UpgradePolicy.canary), canaryTarget, applications.size()); - defaultTargets.forEach(target -> upgrade(applications.with(UpgradePolicy.defaultPolicy), target, numberOfApplicationsToUpgrade())); - conservativeTargets.forEach(target -> upgrade(applications.with(UpgradePolicy.conservative), target, numberOfApplicationsToUpgrade())); + InstanceList instances = instances(); + upgrade(instances.with(UpgradePolicy.canary), canaryTarget, instances.size()); + defaultTargets.forEach(target -> upgrade(instances.with(UpgradePolicy.defaultPolicy), target, numberOfApplicationsToUpgrade())); + conservativeTargets.forEach(target -> upgrade(instances.with(UpgradePolicy.conservative), target, numberOfApplicationsToUpgrade())); } /** Returns the target versions for given confidence, one per major version in the system */ @@ -97,33 +98,34 @@ public class Upgrader extends Maintainer { .map(VespaVersion::versionNumber) .collect(Collectors.toMap(Version::getMajor, // Key on major version Function.identity(), // Use version as value - BinaryOperator.<Version>maxBy(Comparator.naturalOrder()))) // Pick highest version when merging versions within this major + BinaryOperator.<Version>maxBy(naturalOrder()))) // Pick highest version when merging versions within this major .values(); } - /** Returns a list of all applications, except those which are pinned — these should not be manipulated by the Upgrader */ - private DeploymentStatusList applications() { - return controller().jobController().deploymentStatuses(ApplicationList.from(controller().applications().asList())).unpinned(); + /** Returns a list of all production application instances, except those which are pinned, which we should not manipulate here. */ + private InstanceList instances() { + return InstanceList.from(controller().jobController().deploymentStatuses(ApplicationList.from(controller().applications().asList()))) + .withProductionDeployment() + .unpinned(); } - private void upgrade(DeploymentStatusList statuses, Version version, int numberToUpgrade) { - statuses.withProductionDeployment() - .not().deploying() // wait with applications deploying an application change or already upgrading - .not().failingOn(version) // try to upgrade only if it hasn't failed on this version - .asApplicationList() - .onLowerVersionThan(version) - .allowMajorVersion(version.getMajor(), targetMajorVersion().orElse(version.getMajor())) - .canUpgradeAt(controller().clock().instant()) // wait with applications that are currently blocking upgrades - .byIncreasingDeployedVersion() // start with lowest versions - .first(numberToUpgrade) - .asList().forEach(application -> controller().applications().deploymentTrigger().triggerChange(application.id(), Change.of(version))); + private void upgrade(InstanceList instances, Version version, int numberToUpgrade) { + instances.not().failingOn(version) + .allowMajorVersion(version.getMajor(), targetMajorVersion().orElse(version.getMajor())) + .not().deploying() + .onLowerVersionThan(version) + .canUpgradeAt(version, controller().clock().instant()) + .byIncreasingDeployedVersion() + .first(numberToUpgrade).asList() + .forEach(instance -> controller().applications().deploymentTrigger().triggerChange(instance, Change.of(version))); } - private void cancelUpgradesOf(DeploymentStatusList applications, String reason) { - if (applications.isEmpty()) return; - log.info("Cancelling upgrading of " + applications.asList().size() + " applications: " + reason); - for (Application application : applications.asApplicationList().asList()) - controller().applications().deploymentTrigger().cancelChange(application.id(), PLATFORM); + private void cancelUpgradesOf(InstanceList instances, String reason) { + instances = instances.unpinned(); + if (instances.isEmpty()) return; + log.info("Cancelling upgrading of " + instances.asList().size() + " instances: " + reason); + for (ApplicationId instance : instances.asList()) + controller().applications().deploymentTrigger().cancelChange(instance, 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 4d6c21fcd1f..acbae53ad2c 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 @@ -173,7 +173,6 @@ public class ApplicationSerializer { 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())); - toSlime(application.change(), root, deployingField); application.owner().ifPresent(owner -> root.setString(ownerField, owner.username())); application.majorVersion().ifPresent(majorVersion -> root.setLong(majorVersionField, majorVersion)); root.setDouble(queryQualityField, application.metrics().queryServiceQuality()); @@ -336,7 +335,6 @@ public class ApplicationSerializer { 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()); - Change deploying = changeFromSlime(root.field(deployingField)); 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); @@ -348,7 +346,7 @@ public class ApplicationSerializer { OptionalLong projectId = Serializers.optionalLong(root.field(projectIdField)); Optional<ApplicationVersion> latestVersion = latestVersionFromSlime(root.field(latestVersionField)); - return new Application(id, createdAt, deploymentSpec, validationOverrides, deploying, + return new Application(id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, latestVersion, instances); } 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 ec5392f1224..59bc410797c 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 @@ -214,13 +214,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deployment")) return JobControllerApiHandlerHelper.overviewResponse(controller, TenantAndApplicationId.from(path.get("tenant"), path.get("application")), request.getUri()); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/package")) return applicationPackage(path.get("tenant"), path.get("application"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return deploying(path.get("tenant"), path.get("application"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/pin")) return deploying(path.get("tenant"), path.get("application"), request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return deploying(path.get("tenant"), path.get("application"), "default", request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/pin")) return deploying(path.get("tenant"), path.get("application"), "default", request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/metering")) return metering(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance")) return applications(path.get("tenant"), Optional.of(path.get("application")), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return instance(path.get("tenant"), path.get("application"), path.get("instance"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return deploying(path.get("tenant"), path.get("application"), request); // TODO jonmv: remove - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/pin")) return deploying(path.get("tenant"), path.get("application"), request); // TODO jonmv: remove + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return deploying(path.get("tenant"), path.get("application"), path.get("instance"), request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/pin")) return deploying(path.get("tenant"), path.get("application"), path.get("instance"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job")) return JobControllerApiHandlerHelper.jobTypeResponse(controller, appIdFromPath(path), request.getUri()); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.runResponse(controller.jobController().runs(appIdFromPath(path), jobTypeFromPath(path)), request.getUri()); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/test-config")) return testConfig(appIdFromPath(path), jobTypeFromPath(path)); @@ -257,16 +257,16 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}")) return createTenant(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/key")) return addDeveloperKey(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return createApplication(path.get("tenant"), path.get("application"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/platform")) return deployPlatform(path.get("tenant"), path.get("application"), false, request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), true, request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/application")) return deployApplication(path.get("tenant"), path.get("application"), request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/platform")) return deployPlatform(path.get("tenant"), path.get("application"), "default", false, request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), "default", true, request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/application")) return deployApplication(path.get("tenant"), path.get("application"), "default", request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/key")) return addDeployKey(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit")) return submit(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return createInstance(path.get("tenant"), path.get("application"), path.get("instance"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploy/{jobtype}")) return jobDeploy(appIdFromPath(path), jobTypeFromPath(path), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/platform")) return deployPlatform(path.get("tenant"), path.get("application"), false, request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), true, request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/application")) return deployApplication(path.get("tenant"), path.get("application"), request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/platform")) return deployPlatform(path.get("tenant"), path.get("application"), path.get("instance"), false, request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), path.get("instance"), true, request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/application")) return deployApplication(path.get("tenant"), path.get("application"), path.get("instance"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/submit")) return submit(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return trigger(appIdFromPath(path), jobTypeFromPath(path), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/pause")) return pause(appIdFromPath(path), jobTypeFromPath(path)); @@ -289,12 +289,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/key")) return removeDeveloperKey(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return deleteApplication(path.get("tenant"), path.get("application"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), "all"); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("choice")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), "default", "all"); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), "default", path.get("choice")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/key")) return removeDeployKey(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return deleteInstance(path.get("tenant"), path.get("application"), path.get("instance"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), "all"); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("choice")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("instance"), "all"); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("choice")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.abortJobResponse(controller.jobController(), appIdFromPath(path), jobTypeFromPath(path)); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return deactivate(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), true, request); @@ -704,13 +704,16 @@ public class ApplicationApiHandler extends LoggingRequestHandler { application.projectId().ifPresent(id -> object.setLong("projectId", id)); - // Currently deploying change - if ( ! application.change().isEmpty()) - toSlime(object.setObject("deploying"), application.change()); + // TODO jonmv: Remove this when users are updated. + application.instances().values().stream().findFirst().ifPresent(instance -> { + // Currently deploying change + if ( ! instance.change().isEmpty()) + toSlime(object.setObject("deploying"), instance.change()); - // Outstanding change - if ( ! status.outstandingChange().isEmpty()) - toSlime(object.setObject("outstandingChange"), status.outstandingChange()); + // Outstanding change + if ( ! status.outstandingChange(instance.name()).isEmpty()) + toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name())); + }); // Compile version. The version that should be used when building an application object.setString("compileVersion", compileVersion(application.id()).toFullString()); @@ -849,15 +852,16 @@ public class ApplicationApiHandler extends LoggingRequestHandler { application.projectId().ifPresent(id -> object.setLong("projectId", id)); - // Currently deploying change - if ( ! application.change().isEmpty()) { - toSlime(object.setObject("deploying"), application.change()); - } + // TODO jonmv: Remove when users are updated. + application.instances().values().stream().findFirst().ifPresent(firstInstance -> { + // Currently deploying change + if ( ! firstInstance.change().isEmpty()) + toSlime(object.setObject("deploying"), firstInstance.change()); - // Outstanding change - if ( ! status.outstandingChange().isEmpty()) { - toSlime(object.setObject("outstandingChange"), status.outstandingChange()); - } + // Outstanding change + if ( ! status.outstandingChange(firstInstance.name()).isEmpty()) + toSlime(object.setObject("outstandingChange"), status.outstandingChange(firstInstance.name())); + }); if (application.deploymentSpec().instance(instance.name()).isPresent()) { // Jobs sorted according to deployment spec @@ -1251,14 +1255,14 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(slime); } - private HttpResponse deploying(String tenant, String application, HttpRequest request) { - Application app = controller.applications().requireApplication(TenantAndApplicationId.from(tenant, application)); + private HttpResponse deploying(String tenantName, String applicationName, String instanceName, HttpRequest request) { + Instance instance = controller.applications().requireInstance(ApplicationId.from(tenantName, applicationName, instanceName)); Slime slime = new Slime(); Cursor root = slime.setObject(); - if ( ! app.change().isEmpty()) { - app.change().platform().ifPresent(version -> root.setString("platform", version.toString())); - app.change().application().ifPresent(applicationVersion -> root.setString("application", applicationVersion.id())); - root.setBool("pinned", app.change().isPinned()); + if ( ! instance.change().isEmpty()) { + instance.change().platform().ifPresent(version -> root.setString("platform", version.toString())); + instance.change().application().ifPresent(applicationVersion -> root.setString("application", applicationVersion.id())); + root.setBool("pinned", instance.change().isPinned()); } return new SlimeJsonResponse(slime); } @@ -1357,12 +1361,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } /** Trigger deployment of the given Vespa version if a valid one is given, e.g., "7.8.9". */ - private HttpResponse deployPlatform(String tenantName, String applicationName, boolean pin, HttpRequest request) { + private HttpResponse deployPlatform(String tenantName, String applicationName, String instanceName, boolean pin, HttpRequest request) { request = controller.auditLogger().log(request); String versionString = readToString(request.getData()); - TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName); + ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName); StringBuilder response = new StringBuilder(); - controller.applications().lockApplicationOrThrow(id, application -> { + controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { Version version = Version.fromString(versionString); if (version.equals(Version.emptyVersion)) version = controller.systemVersion(); @@ -1385,11 +1389,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } /** Trigger deployment to the last known application package for the given application. */ - private HttpResponse deployApplication(String tenantName, String applicationName, HttpRequest request) { + private HttpResponse deployApplication(String tenantName, String applicationName, String instanceName, HttpRequest request) { controller.auditLogger().log(request); - TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName); + ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName); StringBuilder response = new StringBuilder(); - controller.applications().lockApplicationOrThrow(id, application -> { + controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { Change change = Change.of(application.get().latestVersion().get()); controller.applications().deploymentTrigger().forceChange(id, change); response.append("Triggered " + change + " for " + id); @@ -1398,11 +1402,11 @@ 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 choice) { - TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName); + private HttpResponse cancelDeploy(String tenantName, String applicationName, String instanceName, String choice) { + ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName); StringBuilder response = new StringBuilder(); - controller.applications().lockApplicationOrThrow(id, application -> { - Change change = application.get().change(); + controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { + Change change = application.get().require(id.instance()).change(); if (change.isEmpty()) { response.append("No deployment in progress for " + application + " at this time"); return; @@ -1411,7 +1415,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().requireApplication(id).change() + "' for " + application); + controller.applications().requireInstance(id).change() + "' for " + id); }); return new MessageResponse(response.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 8c348f2f635..8635fcaf3ab 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 @@ -80,7 +80,7 @@ class JobControllerApiHandlerHelper { Application application = controller.applications().requireApplication(TenantAndApplicationId.from(id)); DeploymentStatus deploymentStatus = controller.jobController().deploymentStatus(application); Instance instance = application.require(id.instance()); - Change change = application.change(); + Change change = instance.change(); Optional<DeploymentInstanceSpec> spec = application.deploymentSpec().instance(id.instance()); Optional<DeploymentSteps> steps = spec.map(s -> new DeploymentSteps(s, controller::system)); List<JobType> jobs = deploymentStatus.jobSteps().keySet().stream() @@ -210,9 +210,9 @@ class JobControllerApiHandlerHelper { } else lastPlatformObject.setString("pending", - application.change().isEmpty() + instance.change().isEmpty() ? "Waiting for upgrade slot" - : "Waiting for " + application.change() + " to complete"); + : "Waiting for " + instance.change() + " to complete"); } private static void lastApplicationToSlime(Cursor lastApplicationObject, Application application, Instance instance, Map<JobType, JobStatus> status, Change change, List<JobType> productionJobs, Controller controller) { @@ -320,7 +320,7 @@ class JobControllerApiHandlerHelper { break; for (JobType stepType : steps.toJobs(step)) { if (pendingProduction.containsKey(stepType)) { - Versions jobVersions = Versions.from(application.change(), + Versions jobVersions = Versions.from(instance.change(), application, Optional.ofNullable(instance.deployments().get(stepType.zone(controller.system()))), controller.systemVersion()); @@ -524,17 +524,17 @@ class JobControllerApiHandlerHelper { responseObject.setString("tenant", id.tenant().value()); responseObject.setString("application", id.application().value()); - Change change = status.application().change(); Map<JobId, List<Versions>> jobsToRun = status.jobsToRun(); Cursor stepsArray = responseObject.setArray("steps"); for (DeploymentStatus.StepStatus stepStatus : status.allSteps()) { + Change change = status.application().require(stepStatus.instance()).change(); Cursor stepObject = stepsArray.addObject(); stepObject.setString("type", stepStatus.type().name()); stepStatus.dependencies().stream() .map(status.allSteps()::indexOf) .forEach(stepObject.setArray("dependencies")::addLong); stepObject.setBool("declared", stepStatus.isDeclared()); - stepStatus.instance().ifPresent(instance -> stepObject.setString("instance", instance.value())); + stepObject.setString("instance", stepStatus.instance().value()); stepStatus.job().ifPresent(job -> { stepObject.setString("jobName", job.type().jobName()); @@ -565,8 +565,8 @@ class JobControllerApiHandlerHelper { Cursor runObject = toRunArray.addObject(); toSlime(runObject.setObject("versions"), versions); - stepStatus.readyAt(change, Optional.empty()).ifPresent(ready -> runObject.setLong("readyAt", ready.toEpochMilli())); - stepStatus.readyAt(change, Optional.empty()) + stepStatus.readyAt(change).ifPresent(ready -> runObject.setLong("readyAt", ready.toEpochMilli())); + stepStatus.readyAt(change) .filter(controller.clock().instant()::isBefore) .ifPresent(until -> runObject.setLong("delayedUntil", until.toEpochMilli())); stepStatus.pausedUntil().ifPresent(until -> runObject.setLong("pausedUntil", until.toEpochMilli())); 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 2b523bdc12a..1f1dcbb8001 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 @@ -86,6 +86,10 @@ public class DeploymentApiHandler extends LoggingRequestHandler { Slime slime = new Slime(); Cursor root = slime.setObject(); Cursor platformArray = root.setArray("versions"); + Map<ApplicationId, JobList> jobs = controller.jobController().deploymentStatuses(ApplicationList.from(controller.applications().asList())) + .asList().stream() + .flatMap(status -> status.instanceJobs().entrySet().stream()) + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); for (VespaVersion version : controller.versionStatus().versions()) { Cursor versionObject = platformArray.addObject(); versionObject.setString("version", version.versionNumber().toString()); @@ -101,10 +105,6 @@ public class DeploymentApiHandler extends LoggingRequestHandler { configServerObject.setString("hostname", hostname.value()); } - Map<ApplicationId, JobList> jobs = controller.jobController().deploymentStatuses(ApplicationList.from(controller.applications().asList())) - .asList().stream() - .flatMap(status -> status.instanceJobs().entrySet().stream()) - .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); Cursor failingArray = versionObject.setArray("failingApplications"); for (ApplicationId id : version.statistics().failing()) { if (jobs.containsKey(id)) 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 1c4fe391a35..1b2928c57b1 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 @@ -195,7 +195,7 @@ public class VersionStatus { versionMap.put(infrastructureVersion, DeploymentStatistics.empty(infrastructureVersion)); } - for (DeploymentStatus status : statuses.withProductionDeployment().asList()) { + for (DeploymentStatus status : statuses.asList()) { for (Instance instance : status.application().instances().values()) for (Deployment deployment : instance.productionDeployments().values()) versionMap.computeIfAbsent(deployment.version(), DeploymentStatistics::empty); 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 bc6fd093e2f..f722eb4f6bb 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 @@ -5,6 +5,7 @@ import com.yahoo.component.Version; 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 com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList; @@ -48,13 +49,13 @@ public class VespaVersion implements Comparable<VespaVersion> { } public static Confidence confidenceFrom(DeploymentStatistics statistics, Controller controller) { - DeploymentStatusList all = controller.jobController().deploymentStatuses(ApplicationList.from(controller.applications().asList())) - .withProductionDeployment(); + InstanceList all = InstanceList.from(controller.jobController().deploymentStatuses(ApplicationList.from(controller.applications().asList()))) + .withProductionDeployment(); // 'production on this': All deployment jobs upgrading to this version have completed without failure - DeploymentStatusList productionOnThis = all.forApplications(statistics.production().stream().map(TenantAndApplicationId::from).collect(Collectors.toList())) + InstanceList productionOnThis = all.matching(statistics.production()::contains) .not().failingUpgrade() .not().upgradingTo(statistics.version()); - DeploymentStatusList failingOnThis = all.forApplications(statistics.failing().stream().map(TenantAndApplicationId::from).collect(Collectors.toList())); + InstanceList failingOnThis = all.matching(statistics.failing()::contains); // 'broken' if any Canary fails if ( ! failingOnThis.with(UpgradePolicy.canary).isEmpty()) @@ -166,10 +167,10 @@ public class VespaVersion implements Comparable<VespaVersion> { } private static boolean nonCanaryApplicationsBroken(Version version, - DeploymentStatusList failingOnThis, - DeploymentStatusList productionOnThis) { - DeploymentStatusList failingNonCanaries = failingOnThis.startedFailingOn(version).not().with(UpgradePolicy.canary); - DeploymentStatusList productionNonCanaries = productionOnThis.not().with(UpgradePolicy.canary); + InstanceList failingOnThis, + InstanceList productionOnThis) { + InstanceList failingNonCanaries = failingOnThis.startedFailingOn(version).not().with(UpgradePolicy.canary); + InstanceList productionNonCanaries = productionOnThis.not().with(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 35e4b018eed..bc98775c00e 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 @@ -80,11 +80,11 @@ public class ControllerTest { context.submit(applicationPackage); assertEquals("Application version is known from completion of initial job", ApplicationVersion.from(DeploymentContext.defaultSourceRevision, 1, "a@b", new Version("6.1"), Instant.ofEpochSecond(1)), - context.application().change().application().get()); + context.instance().change().application().get()); context.runJob(systemTest); context.runJob(stagingTest); - ApplicationVersion applicationVersion = context.application().change().application().get(); + ApplicationVersion applicationVersion = context.instance().change().application().get(); assertFalse("Application version has been set during deployment", applicationVersion.isUnknown()); tester.triggerJobs(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java index 309af7e450b..e4b5e77b377 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java @@ -188,10 +188,6 @@ public class ApplicationPackageBuilder { xml.append(athenzIdentityAttributes); } xml.append(">\n"); - if (explicitSystemTest) - xml.append(" <test />\n"); - if (explicitStagingTest) - xml.append(" <staging />\n"); xml.append(" <instance id='").append(instances).append("'>\n"); if (upgradePolicy != null) { xml.append(" <upgrade policy='"); @@ -200,6 +196,10 @@ public class ApplicationPackageBuilder { } xml.append(notifications); xml.append(blockChange); + if (explicitSystemTest) + xml.append(" <test />\n"); + if (explicitStagingTest) + xml.append(" <staging />\n"); xml.append(" <"); xml.append(environment.value()); if (globalServiceId != null) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java index fcf2ef97b49..120b9bad468 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java @@ -92,11 +92,8 @@ public class DeploymentContext { private final TesterId testerId; private final JobController jobs; private final RoutingGeneratorMock routing; - private final MockTesterCloud cloud; private final JobRunner runner; - private final ControllerTester tester; - private final ReadyJobsTrigger readyJobsTrigger; - private final NameServiceDispatcher nameServiceDispatcher;; + private final DeploymentTester tester; private ApplicationVersion lastSubmission = null; private boolean deferDnsUpdates = false; @@ -108,18 +105,15 @@ public class DeploymentContext { this.testerId = TesterId.of(instanceId); this.jobs = tester.controller().jobController(); this.runner = tester.runner(); - this.tester = tester.controllerTester(); - this.routing = this.tester.serviceRegistry().routingGeneratorMock(); - this.cloud = this.tester.serviceRegistry().testerCloud(); - this.readyJobsTrigger = tester.readyJobsTrigger(); - this.nameServiceDispatcher = tester.nameServiceDispatcher(); + this.tester = tester; + this.routing = tester.controllerTester().serviceRegistry().routingGeneratorMock(); createTenantAndApplication(); } private void createTenantAndApplication() { try { - var tenant = tester.createTenant(instanceId.tenant().value()); - tester.createApplication(tenant.value(), instanceId.application().value(), instanceId.instance().value()); + var tenant = tester.controllerTester().createTenant(instanceId.tenant().value()); + tester.controllerTester().createApplication(tenant.value(), instanceId.application().value(), instanceId.instance().value()); } catch (IllegalArgumentException ignored) { } // Tenant and or application may already exist with custom setup. } @@ -156,30 +150,30 @@ public class DeploymentContext { /** Completely deploy the latest change */ public DeploymentContext deploy() { - assertNotNull("Application package submitted", lastSubmission); + assertTrue("Application package submitted", application().latestVersion().isPresent()); assertFalse("Submission is not already deployed", application().instances().values().stream() .anyMatch(instance -> instance.deployments().values().stream() .anyMatch(deployment -> deployment.applicationVersion().equals(lastSubmission)))); - assertEquals(lastSubmission, application().change().application().get()); + assertEquals(application().latestVersion(), instance().change().application()); completeRollout(); - assertFalse(application().change().hasTargets()); + assertFalse(instance().change().hasTargets()); return this; } /** Upgrade platform of this to given version */ public DeploymentContext deployPlatform(Version version) { - assertEquals(application().change().platform().get(), version); + assertEquals(instance().change().platform().get(), version); assertFalse(application().instances().values().stream() .anyMatch(instance -> instance.deployments().values().stream() .anyMatch(deployment -> deployment.version().equals(version)))); - assertEquals(version, application().change().platform().get()); - assertFalse(application().change().application().isPresent()); + assertEquals(version, instance().change().platform().get()); + assertFalse(instance().change().application().isPresent()); completeRollout(); assertTrue(application().productionDeployments().values().stream() - .allMatch(deployments -> deployments.stream() - .allMatch(deployment -> deployment.version().equals(version)))); + .allMatch(deployments -> deployments.stream() + .allMatch(deployment -> deployment.version().equals(version)))); for (var spec : application().deploymentSpec().instances()) for (JobType type : new DeploymentSteps(spec, tester.controller()::system).productionJobs()) @@ -187,7 +181,7 @@ public class DeploymentContext { .list(type.zone(tester.controller().system()), applicationId.defaultInstance()).stream() // TODO jonmv: support more .allMatch(node -> node.currentVersion().equals(version))); - assertFalse(application().change().hasTargets()); + assertFalse(instance().change().hasTargets()); return this; } @@ -200,7 +194,7 @@ public class DeploymentContext { /** Flush all pending DNS updates */ public DeploymentContext flushDnsUpdates() { - nameServiceDispatcher.run(); + tester.nameServiceDispatcher().run(); assertTrue("All name service requests dispatched", tester.controller().curator().readNameServiceQueue().requests().isEmpty()); return this; @@ -279,7 +273,7 @@ public class DeploymentContext { else throw new AssertionError("Job '" + run.id() + "' was run twice"); - assertFalse("Change should have no targets, but was " + application().change(), application().change().hasTargets()); + assertFalse("Change should have no targets, but was " + instance().change(), instance().change().hasTargets()); if (!deferDnsUpdates) { flushDnsUpdates(); } @@ -370,12 +364,12 @@ public class DeploymentContext { /** Deploy default application package, start a run for that change and return its ID */ public RunId newRun(JobType type) { submit(); - readyJobsTrigger.maintain(); + tester.readyJobsTrigger().maintain(); if (type.isProduction()) { runJob(JobType.systemTest); runJob(JobType.stagingTest); - readyJobsTrigger.maintain(); + tester.readyJobsTrigger().maintain(); } Run run = jobs.active().stream() @@ -523,7 +517,7 @@ public class DeploymentContext { assertEquals(Step.Status.succeeded, jobs.run(id).get().stepStatuses().get(Step.startTests)); assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.endTests)); - cloud.set(TesterCloud.Status.SUCCESS); + tester.cloud().set(TesterCloud.Status.SUCCESS); runner.advance(currentRun(job)); assertTrue(jobs.run(id).get().hasEnded()); assertFalse(jobs.run(id).get().hasFailed()); 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 8cd938dccf6..855c24a38ab 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 @@ -126,7 +126,7 @@ public class DeploymentTriggerTest { .collect(Collectors.toCollection(() -> EnumSet.noneOf(JobType.class)))); app.deploy(); - assertEquals(Change.empty(), app.application().change()); + assertEquals(Change.empty(), app.instance().change()); tester.controllerTester().upgradeSystem(new Version("8.9")); tester.upgrader().maintain(); @@ -264,7 +264,7 @@ public class DeploymentTriggerTest { tester.configServer().setSuspended(application.deploymentIdIn(ZoneId.from("prod", "us-central-1")), false); tester.triggerJobs(); application.runJob(productionUsWest1).runJob(productionUsEast3); - assertEquals(Change.empty(), application.application().change()); + assertEquals(Change.empty(), application.instance().change()); } @Test @@ -292,11 +292,11 @@ public class DeploymentTriggerTest { assertEquals(0, tester.jobs().active().size()); app.submit(applicationPackage); - assertTrue(app.deploymentStatus().outstandingChange().hasTargets()); + assertTrue(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets()); app.runJob(systemTest).runJob(stagingTest); tester.outstandingChangeDeployer().run(); - assertTrue(app.deploymentStatus().outstandingChange().hasTargets()); + assertTrue(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets()); tester.triggerJobs(); assertEquals(emptyList(), tester.jobs().active()); @@ -304,7 +304,7 @@ public class DeploymentTriggerTest { tester.clock().advance(Duration.ofHours(2)); // ---------------- Exit block window: 20:30 tester.outstandingChangeDeployer().run(); - assertFalse(app.deploymentStatus().outstandingChange().hasTargets()); + assertFalse(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets()); tester.triggerJobs(); // Tests already run for the blocked production job. app.assertRunning(productionUsWest1); @@ -337,7 +337,7 @@ public class DeploymentTriggerTest { tester.outstandingChangeDeployer().run(); app.runJob(productionUsWest1); assertEquals(1, app.instanceJobs().get(productionUsWest1).lastSuccess().get().versions().targetApplication().buildNumber().getAsLong()); - assertEquals(2, app.deploymentStatus().outstandingChange().application().get().buildNumber().getAsLong()); + assertEquals(2, app.deploymentStatus().outstandingChange(app.instance().name()).application().get().buildNumber().getAsLong()); tester.triggerJobs(); // Platform upgrade keeps rolling, since it began before block window, and tests for the new revision have also started. @@ -346,18 +346,18 @@ public class DeploymentTriggerTest { assertEquals(2, tester.jobs().active().size()); // Upgrade is done, and outstanding change rolls out when block window ends. - assertEquals(Change.empty(), app.application().change()); - assertTrue(app.deploymentStatus().outstandingChange().hasTargets()); + assertEquals(Change.empty(), app.instance().change()); + assertTrue(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets()); app.runJob(stagingTest).runJob(systemTest); tester.clock().advance(Duration.ofHours(1)); tester.outstandingChangeDeployer().run(); - assertTrue(app.application().change().hasTargets()); - assertFalse(app.deploymentStatus().outstandingChange().hasTargets()); + assertTrue(app.instance().change().hasTargets()); + assertFalse(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets()); app.runJob(productionUsWest1).runJob(productionUsEast3); - assertFalse(app.application().change().hasTargets()); + assertFalse(app.instance().change().hasTargets()); } @Test @@ -420,12 +420,12 @@ public class DeploymentTriggerTest { assertEquals(appVersion1, app.deployment(ZoneId.from("prod.us-central-1")).applicationVersion()); // Verify the application change is not removed when platform change is cancelled. - tester.deploymentTrigger().cancelChange(app.application().id(), PLATFORM); - assertEquals(Change.of(appVersion1), app.application().change()); + tester.deploymentTrigger().cancelChange(app.instanceId(), PLATFORM); + assertEquals(Change.of(appVersion1), app.instance().change()); // Now cancel the change as is done through the web API. - tester.deploymentTrigger().cancelChange(app.application().id(), ALL); - assertEquals(Change.empty(), app.application().change()); + tester.deploymentTrigger().cancelChange(app.instanceId(), ALL); + assertEquals(Change.empty(), app.instance().change()); // A new version is released, which should now deploy the currently deployed application version to avoid downgrades. Version version1 = new Version("6.2"); @@ -464,7 +464,7 @@ public class DeploymentTriggerTest { tester.upgrader().maintain(); // Deploy application2 to keep this version present in the system app2.deployPlatform(version1); - tester.deploymentTrigger().cancelChange(app1.application().id(), ALL); + tester.deploymentTrigger().cancelChange(app1.instanceId(), ALL); // version2 is released and application1 starts upgrading Version version2 = Version.fromString("7.2"); @@ -480,7 +480,7 @@ public class DeploymentTriggerTest { tester.upgrader().overrideConfidence(version2, VespaVersion.Confidence.broken); tester.controllerTester().computeVersionStatus(); tester.upgrader().maintain(); // Cancel upgrades to broken version - assertEquals("Change becomes latest non-broken version", Change.of(version1), app1.application().change()); + assertEquals("Change becomes latest non-broken version", Change.of(version1), app1.instance().change()); // version1 proceeds 'til the last job, where it fails; us-central-1 is skipped, as current change is strictly dominated by what's deployed there. app1.failDeployment(productionEuWest1); @@ -491,7 +491,7 @@ public class DeploymentTriggerTest { app1.submit(applicationPackage); ApplicationVersion revision2 = app1.lastSubmission().get(); app1.runJob(systemTest).runJob(stagingTest); - assertEquals(Change.of(version1).with(revision2), app1.application().change()); + assertEquals(Change.of(version1).with(revision2), app1.instance().change()); tester.triggerJobs(); app1.assertRunning(productionUsCentral1); assertEquals(version2, app1.instance().deployments().get(productionUsCentral1.zone(main)).version()); @@ -511,7 +511,7 @@ public class DeploymentTriggerTest { // Last job has a different deployment target, so tests need to run again. app1.runJob(systemTest).runJob(stagingTest).runJob(productionEuWest1); - assertFalse(app1.application().change().hasTargets()); + assertFalse(app1.instance().change().hasTargets()); assertFalse(app1.instanceJobs().get(productionUsCentral1).isSuccess()); } @@ -532,7 +532,7 @@ public class DeploymentTriggerTest { tester.upgrader().maintain(); app.runJob(systemTest).runJob(stagingTest); app.timeOutConvergence(productionEuWest1); - tester.deploymentTrigger().cancelChange(app.application().id(), PLATFORM); + tester.deploymentTrigger().cancelChange(app.instanceId(), PLATFORM); assertEquals(v2, app.deployment(productionEuWest1.zone(main)).version()); assertEquals(v1, app.deployment(productionUsEast3.zone(main)).version()); @@ -558,7 +558,7 @@ public class DeploymentTriggerTest { // Both jobs fail again, and must be re-triggered -- this is ok, as they are both already triggered on their current targets. app.failDeployment(productionEuWest1).failDeployment(productionUsEast3) .runJob(productionEuWest1).runJob(productionUsEast3); - assertFalse(app.application().change().hasTargets()); + assertFalse(app.instance().change().hasTargets()); assertEquals(2, app.instanceJobs().get(productionEuWest1).lastSuccess().get().versions().targetApplication().buildNumber().getAsLong()); assertEquals(2, app.instanceJobs().get(productionUsEast3).lastSuccess().get().versions().targetApplication().buildNumber().getAsLong()); } @@ -647,7 +647,7 @@ public class DeploymentTriggerTest { assertEquals("Application change preserves version, and new region gets oldest version too", version1, app1.application().oldestDeployedPlatform().get()); assertEquals(version1, tester.configServer().lastPrepareVersion().get()); - assertFalse("Change deployed", app1.application().change().hasTargets()); + assertFalse("Change deployed", app1.instance().change().hasTargets()); tester.upgrader().maintain(); app1.deployPlatform(version2); @@ -737,9 +737,9 @@ public class DeploymentTriggerTest { app3.abortJob(stagingTest); assertEquals(0, tester.jobs().active().size()); - assertTrue(app1.application().change().application().isPresent()); - assertFalse(app2.application().change().application().isPresent()); - assertFalse(app3.application().change().application().isPresent()); + assertTrue(app1.instance().change().application().isPresent()); + assertFalse(app2.instance().change().application().isPresent()); + assertFalse(app3.instance().change().application().isPresent()); tester.readyJobsTrigger().maintain(); app1.assertRunning(stagingTest); @@ -801,7 +801,7 @@ public class DeploymentTriggerTest { app.runJob(testUsEast3) .runJob(productionUsWest1).runJob(productionUsCentral1) .runJob(testUsCentral1).runJob(testUsWest1); - assertEquals(Change.empty(), app.application().change()); + assertEquals(Change.empty(), app.instance().change()); // Application starts upgrade, but is confidence is broken cancelled after first zone. Tests won't run. Version version0 = app.application().oldestDeployedPlatform().get(); @@ -820,25 +820,25 @@ public class DeploymentTriggerTest { tester.upgrader().maintain(); app.failDeployment(testUsEast3); app.assertNotRunning(testUsEast3); - assertEquals(Change.empty(), app.application().change()); + assertEquals(Change.empty(), app.instance().change()); // Application is pinned to previous version, and downgrades to that. Tests are re-run. - tester.deploymentTrigger().triggerChange(app.application().id(), Change.of(version0).withPin()); + tester.deploymentTrigger().triggerChange(app.instanceId(), Change.of(version0).withPin()); app.runJob(stagingTest).runJob(productionUsEast3); tester.clock().advance(Duration.ofMinutes(1)); app.failDeployment(testUsEast3); tester.clock().advance(Duration.ofMinutes(11)); // Job is cooling down after consecutive failures. app.runJob(testUsEast3); - assertEquals(Change.empty().withPin(), app.application().change()); + assertEquals(Change.empty().withPin(), app.instance().change()); } @Test public void testDeployComplicatedDeploymentSpec() { String complicatedDeploymentSpec = "<deployment version='1.0' athenz-domain='domain' athenz-service='service'>\n" + - " <staging />\n" + " <parallel>\n" + " <instance id='instance' athenz-service='in-service'>\n" + + " <staging />\n" + " <prod>\n" + " <parallel>\n" + " <region active='true'>us-west-1</region>\n" + @@ -875,11 +875,9 @@ public class DeploymentTriggerTest { " </endpoints>\n" + " </instance>\n" + " <instance id='other'>\n" + - // TODO jonmv: support change per instance — this instance will be upgraded later, but for now this is ignored. " <upgrade policy='conservative' />\n" + " <test />\n" + - // TODO jonmv: test this when change per instance. - // " <block-change days='sat' hours='10' time-zone='CET' />\n" + + " <block-change days='sat' hours='10' time-zone='CET' />\n" + " <prod>\n" + " <region active='true'>eu-west-1</region>\n" + " <test>eu-west-1</test>\n" + @@ -895,7 +893,7 @@ public class DeploymentTriggerTest { ApplicationPackage applicationPackage = ApplicationPackageBuilder.fromDeploymentXml(complicatedDeploymentSpec); var app1 = tester.newDeploymentContext("t", "a", "instance").submit(applicationPackage); - var app2 = tester.newDeploymentContext("t", "a", "other").submit(applicationPackage); + var app2 = tester.newDeploymentContext("t", "a", "other"); tester.triggerJobs(); assertEquals(2, tester.jobs().active().size()); @@ -955,6 +953,10 @@ public class DeploymentTriggerTest { app1.runJob(productionApSoutheast1); tester.triggerJobs(); assertEquals(List.of(), tester.jobs().active()); + + tester.atMondayMorning().clock().advance(Duration.ofDays(5).plusHours(5)); // Inside block window for second instance. + // TODO jonmv: More checks. + } @Test @@ -968,12 +970,12 @@ public class DeploymentTriggerTest { app.submit(); tester.triggerJobs(); tester.outstandingChangeDeployer().run(); - assertEquals(Change.of(version), app.application().change()); + assertEquals(Change.of(version), app.instance().change()); app.runJob(productionUsEast3).runJob(productionUsWest1); tester.triggerJobs(); tester.outstandingChangeDeployer().run(); - assertEquals(Change.of(app.lastSubmission().get()), app.application().change()); + assertEquals(Change.of(app.lastSubmission().get()), app.instance().change()); } } 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 0afe7377d40..ea946e132a9 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 @@ -144,7 +144,7 @@ public class DeploymentIssueReporterTest { // app2 is changed to be a canary app2.submit(canaryPackage).deploy(); assertEquals(canary, app2.application().deploymentSpec().requireInstance("default").upgradePolicy()); - assertEquals(Change.empty(), app2.application().change()); + assertEquals(Change.empty(), app2.instance().change()); // Bump system version to upgrade canary app2. Version version = Version.fromString("6.3"); 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 a949625baf4..99eb4f049b6 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 @@ -139,7 +139,7 @@ public class MetricsReporterTest { // Application change completes context.deploy(); - assertFalse("Change deployed", context.application().change().hasTargets()); + assertFalse("Change deployed", context.instance().change().hasTargets()); // New versions is released and upgrade fails in test environments Version version = Version.fromString("7.1"); @@ -159,7 +159,7 @@ public class MetricsReporterTest { // Upgrade eventually succeeds context.runJob(productionUsWest1); - assertFalse("Upgrade deployed", context.application().change().hasTargets()); + assertFalse("Upgrade deployed", context.instance().change().hasTargets()); reporter.maintain(); assertEquals(0, getDeploymentsFailingUpgrade(context.instanceId())); } 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 a822e4fca9f..442a2fd1853 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 @@ -40,17 +40,17 @@ public class OutstandingChangeDeployerTest { var app1 = tester.newDeploymentContext("tenant", "app1", "default").submit(applicationPackage).deploy(); Version version = new Version(6, 2); - tester.deploymentTrigger().triggerChange(app1.application().id(), Change.of(version)); + tester.deploymentTrigger().triggerChange(app1.instanceId(), Change.of(version)); tester.deploymentTrigger().triggerReadyJobs(); - assertEquals(Change.of(version), app1.application().change()); - assertFalse(app1.deploymentStatus().outstandingChange().hasTargets()); + assertEquals(Change.of(version), app1.instance().change()); + assertFalse(app1.deploymentStatus().outstandingChange(app1.instance().name()).hasTargets()); assertEquals(1, app1.application().latestVersion().get().buildNumber().getAsLong()); app1.submit(applicationPackage, new SourceRevision("repository1", "master", "cafed00d")); - assertTrue(app1.deploymentStatus().outstandingChange().hasTargets()); - assertEquals("1.0.2-cafed00d", app1.deploymentStatus().outstandingChange().application().get().id()); + assertTrue(app1.deploymentStatus().outstandingChange(app1.instance().name()).hasTargets()); + assertEquals("1.0.2-cafed00d", app1.deploymentStatus().outstandingChange(app1.instance().name()).application().get().id()); app1.assertRunning(JobType.systemTest); app1.assertRunning(JobType.stagingTest); assertEquals(2, tester.jobs().active().size()); @@ -58,7 +58,7 @@ public class OutstandingChangeDeployerTest { deployer.maintain(); tester.triggerJobs(); assertEquals("No effect as job is in progress", 2, tester.jobs().active().size()); - assertEquals("1.0.2-cafed00d", app1.deploymentStatus().outstandingChange().application().get().id()); + assertEquals("1.0.2-cafed00d", app1.deploymentStatus().outstandingChange(app1.instance().name()).application().get().id()); app1.runJob(JobType.systemTest).runJob(JobType.stagingTest).runJob(JobType.productionUsWest1) .runJob(JobType.stagingTest).runJob(JobType.systemTest); @@ -66,11 +66,11 @@ public class OutstandingChangeDeployerTest { deployer.maintain(); tester.triggerJobs(); - assertEquals("1.0.2-cafed00d", app1.application().change().application().get().id()); + assertEquals("1.0.2-cafed00d", app1.instance().change().application().get().id()); List<Run> runs = tester.jobs().active(); assertEquals(1, runs.size()); app1.assertRunning(JobType.productionUsWest1); - assertFalse(app1.deploymentStatus().outstandingChange().hasTargets()); + assertFalse(app1.deploymentStatus().outstandingChange(app1.instance().name()).hasTargets()); } } 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 c4a3a34a4f6..249c2644170 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 @@ -190,7 +190,7 @@ public class UpgraderTest { assertEquals("Upgrade of defaults are scheduled", 10, tester.jobs().active().size()); for (var context : List.of(default0, default1, default2, default3, default4)) - assertEquals(version4, context.application().change().platform().get()); + assertEquals(version4, context.instance().change().platform().get()); default0.deployPlatform(version4); @@ -207,9 +207,9 @@ public class UpgraderTest { tester.triggerJobs(); assertEquals("Upgrade of defaults are scheduled", 10, tester.jobs().active().size()); - assertEquals(version5, default0.application().change().platform().get()); + assertEquals(version5, default0.instance().change().platform().get()); for (var context : List.of(default1, default2, default3, default4)) - assertEquals(version4, context.application().change().platform().get()); + assertEquals(version4, context.instance().change().platform().get()); default1.deployPlatform(version4); default2.deployPlatform(version4); @@ -240,7 +240,7 @@ public class UpgraderTest { tester.upgrader().maintain(); - assertEquals(version4, default3.application().change().platform().get()); + assertEquals(version4, default3.instance().change().platform().get()); } @Test @@ -361,10 +361,10 @@ public class UpgraderTest { // We "manually" cancel upgrades to V1 so that we can use the applications to make V2 fail instead // But we keep one (default4) to avoid V1 being garbage collected - tester.deploymentTrigger().cancelChange(default0.application().id(), ALL); - tester.deploymentTrigger().cancelChange(default1.application().id(), ALL); - tester.deploymentTrigger().cancelChange(default2.application().id(), ALL); - tester.deploymentTrigger().cancelChange(default3.application().id(), ALL); + tester.deploymentTrigger().cancelChange(default0.instanceId(), ALL); + tester.deploymentTrigger().cancelChange(default1.instanceId(), ALL); + tester.deploymentTrigger().cancelChange(default2.instanceId(), ALL); + tester.deploymentTrigger().cancelChange(default3.instanceId(), ALL); default0.abortJob(systemTest).abortJob(stagingTest); default1.abortJob(systemTest).abortJob(stagingTest); default2.abortJob(systemTest).abortJob(stagingTest); @@ -374,7 +374,7 @@ public class UpgraderTest { tester.upgrader().maintain(); tester.triggerJobs(); assertEquals("Upgrade scheduled for remaining apps", 10, tester.jobs().active().size()); - assertEquals("default4 is still upgrading to 6.3", v1, default4.application().change().platform().get()); + assertEquals("default4 is still upgrading to 6.3", v1, default4.instance().change().platform().get()); // 4/5 applications fail (in the last prod zone) and lowers confidence default0.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1).failDeployment(productionUsEast3); @@ -399,7 +399,7 @@ public class UpgraderTest { assertEquals(v2, default0.deployment(ZoneId.from("prod.us-west-1")).version()); assertEquals("Last zone is upgraded to v1", v1, default0.deployment(ZoneId.from("prod.us-east-3")).version()); - assertFalse(default0.application().change().hasTargets()); + assertFalse(default0.instance().change().hasTargets()); } @Test @@ -788,8 +788,8 @@ public class UpgraderTest { // Application change recorded together with ongoing upgrade assertTrue("Change contains both upgrade and application change", - app.application().change().platform().get().equals(version) && - app.application().change().application().get().id().equals(applicationVersion)); + app.instance().change().platform().get().equals(version) && + app.instance().change().application().get().id().equals(applicationVersion)); // Deployment completes app.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1).runJob(productionUsEast3); @@ -879,13 +879,13 @@ public class UpgraderTest { assertEquals(List.of(), tester.jobs().active()); // No jobs left. tester.outstandingChangeDeployer().run(); - assertFalse(app.application().change().hasTargets()); + assertFalse(app.instance().change().hasTargets()); tester.clock().advance(Duration.ofHours(2)); tester.outstandingChangeDeployer().run(); - assertTrue(app.application().change().hasTargets()); + assertTrue(app.instance().change().hasTargets()); app.runJob(productionUsWest1).runJob(productionUsCentral1).runJob(productionUsEast3); - assertFalse(app.application().change().hasTargets()); + assertFalse(app.instance().change().hasTargets()); } @Test @@ -895,50 +895,50 @@ public class UpgraderTest { // Create an application with pinned platform version. var context = tester.newDeploymentContext(); - tester.deploymentTrigger().forceChange(context.application().id(), Change.empty().withPin()); + tester.deploymentTrigger().forceChange(context.instanceId(), Change.empty().withPin()); context.submit().deploy(); - assertFalse(context.application().change().hasTargets()); - assertTrue(context.application().change().isPinned()); + assertFalse(context.instance().change().hasTargets()); + assertTrue(context.instance().change().isPinned()); assertEquals(3, context.instance().deployments().size()); // Application does not upgrade. Version version1 = Version.fromString("6.3"); tester.controllerTester().upgradeSystem(version1); tester.upgrader().maintain(); - assertFalse(context.application().change().hasTargets()); - assertTrue(context.application().change().isPinned()); + assertFalse(context.instance().change().hasTargets()); + assertTrue(context.instance().change().isPinned()); // New application package is deployed. context.submit().deploy(); - assertFalse(context.application().change().hasTargets()); - assertTrue(context.application().change().isPinned()); + assertFalse(context.instance().change().hasTargets()); + assertTrue(context.instance().change().isPinned()); // Application upgrades to new version when pin is removed. - tester.deploymentTrigger().cancelChange(context.application().id(), PIN); + tester.deploymentTrigger().cancelChange(context.instanceId(), PIN); tester.upgrader().maintain(); - assertTrue(context.application().change().hasTargets()); - assertFalse(context.application().change().isPinned()); + assertTrue(context.instance().change().hasTargets()); + assertFalse(context.instance().change().isPinned()); // Application is pinned to new version, and upgrade is therefore not cancelled, even though confidence is broken. - tester.deploymentTrigger().forceChange(context.application().id(), Change.empty().withPin()); + tester.deploymentTrigger().forceChange(context.instanceId(), Change.empty().withPin()); tester.upgrader().maintain(); tester.triggerJobs(); - assertEquals(version1, context.application().change().platform().get()); + assertEquals(version1, context.instance().change().platform().get()); // Application fails upgrade after one zone is complete, and is pinned again to the old version. context.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1) .timeOutUpgrade(productionUsWest1); - tester.deploymentTrigger().cancelChange(context.application().id(), ALL); - tester.deploymentTrigger().forceChange(context.application().id(), Change.of(version0).withPin()); - assertEquals(version0, context.application().change().platform().get()); + tester.deploymentTrigger().cancelChange(context.instanceId(), ALL); + tester.deploymentTrigger().forceChange(context.instanceId(), Change.of(version0).withPin()); + assertEquals(version0, context.instance().change().platform().get()); // Application downgrades to pinned version. tester.abortAll(); context.runJob(stagingTest).runJob(productionUsCentral1); - assertTrue(context.application().change().hasTargets()); + assertTrue(context.instance().change().hasTargets()); context.runJob(productionUsWest1); // us-east-3 never upgraded, so no downgrade is needed. - assertFalse(context.application().change().hasTargets()); + assertFalse(context.instance().change().hasTargets()); } @Test @@ -955,7 +955,8 @@ public class UpgraderTest { // Keep app 1 on current version tester.controller().applications().lockApplicationIfPresent(app1.application().id(), app -> - tester.controller().applications().store(app.withChange(app.get().change().withPin()))); + tester.controller().applications().store(app.with(app1.instance().name(), + instance -> instance.withChange(instance.change().withPin())))); // New version is released Version version1 = Version.fromString("6.2"); @@ -972,14 +973,15 @@ public class UpgraderTest { // App 2 is allowed on new major and upgrades tester.controller().applications().lockApplicationIfPresent(app2.application().id(), app -> tester.applications().store(app.withMajorVersion(7))); tester.upgrader().maintain(); - assertEquals(version2, app2.application().change().platform().orElseThrow()); + assertEquals(version2, app2.instance().change().platform().orElseThrow()); // App 1 is unpinned and upgrades to latest 6 tester.controller().applications().lockApplicationIfPresent(app1.application().id(), app -> - tester.controller().applications().store(app.withChange(app.get().change().withoutPin()))); + tester.controller().applications().store(app.with(app1.instance().name(), + instance -> instance.withChange(instance.change().withoutPin())))); tester.upgrader().maintain(); assertEquals("Application upgrades to latest allowed major", version1, - app1.application().change().platform().orElseThrow()); + app1.instance().change().platform().orElseThrow()); } @Test @@ -998,7 +1000,7 @@ public class UpgraderTest { Version v2 = Version.fromString("6.2"); tester.controllerTester().upgradeSystem(v2); tester.upgrader().maintain(); - assertEquals(Change.of(v2), application.application().change()); + assertEquals(Change.of(v2), application.instance().change()); application.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1); tester.triggerJobs(); @@ -1007,13 +1009,13 @@ public class UpgraderTest { tester.controllerTester().computeVersionStatus(); tester.upgrader().maintain(); application.runJob(productionUsWest1); - assertTrue(application.application().change().isEmpty()); + assertTrue(application.instance().change().isEmpty()); // Next version is released Version v3 = Version.fromString("6.3"); tester.controllerTester().upgradeSystem(v3); tester.upgrader().maintain(); - assertEquals(Change.of(v3), application.application().change()); + assertEquals(Change.of(v3), application.instance().change()); application.runJob(systemTest).runJob(stagingTest); // First deployment starts upgrading @@ -1038,7 +1040,7 @@ public class UpgraderTest { // Upgrade completes application.runJob(productionUsEast3); - assertTrue("Upgrade complete", application.application().change().isEmpty()); + assertTrue("Upgrade complete", application.instance().change().isEmpty()); } @Test diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java index 14002807b96..e485e60815b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java @@ -117,13 +117,12 @@ public class ApplicationSerializerTest { Map.of(), List.of(), RotationStatus.EMPTY, - Change.empty())); + Change.of(Version.fromString("6.7")).withPin())); Application original = new Application(TenantAndApplicationId.from(id1), Instant.now().truncatedTo(ChronoUnit.MILLIS), deploymentSpec, validationOverrides, - Change.of(Version.fromString("6.7")).withPin(), Optional.of(IssueId.from("4321")), Optional.of(IssueId.from("1234")), Optional.of(User.from("by-username")), @@ -168,7 +167,6 @@ public class ApplicationSerializerTest { assertEquals(original.ownershipIssueId(), serialized.ownershipIssueId()); assertEquals(original.owner(), serialized.owner()); assertEquals(original.majorVersion(), serialized.majorVersion()); - assertEquals(original.change(), serialized.change()); assertEquals(original.deployKeys(), serialized.deployKeys()); assertEquals(original.require(id1.instance()).rotations(), serialized.require(id1.instance()).rotations()); 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 cbc7a5477c4..16837fa44f3 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 @@ -340,7 +340,7 @@ public class ApplicationApiTest extends ControllerContainerTest { id2.application()); // Trigger upgrade and then application change - deploymentTester.applications().deploymentTrigger().triggerChange(TenantAndApplicationId.from(id2), Change.of(Version.fromString("7.0"))); + deploymentTester.applications().deploymentTrigger().triggerChange(id2, Change.of(Version.fromString("7.0"))); // POST an application package and a test jar, submitting a new application for production deployment. tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/submit", POST) |