diff options
author | Martin Polden <mpolden@mpolden.no> | 2018-02-05 11:18:45 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-05 11:18:45 +0100 |
commit | af8ac7754317bbb9588e077c5271fdac5db3cc23 (patch) | |
tree | 797e308b79b87290e83ad67001b382130b479f9a | |
parent | 6a2dbe14a4dc52b25ca1d100728dd4a0f6c08091 (diff) | |
parent | afa156c8452eb8c3bf5156ad423e0a8216ed369b (diff) |
Merge pull request #4911 from vespa-engine/mpolden/application-version
Migrate to new application version
19 files changed, 443 insertions, 292 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 d5ce613b98d..5105a016934 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 @@ -7,9 +7,9 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; -import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationRotation; import com.yahoo.vespa.hosted.controller.application.ApplicationVersion; import com.yahoo.vespa.hosted.controller.application.Change; @@ -42,7 +42,7 @@ public class Application { private final Map<ZoneId, Deployment> deployments; private final DeploymentJobs deploymentJobs; private final Change change; - private final boolean outstandingChange; + private final Change outstandingChange; private final Optional<IssueId> ownershipIssueId; private final ApplicationMetrics metrics; private final Optional<RotationId> rotation; @@ -51,14 +51,14 @@ public class Application { public Application(ApplicationId id) { this(id, DeploymentSpec.empty, ValidationOverrides.empty, Collections.emptyMap(), new DeploymentJobs(Optional.empty(), Collections.emptyList(), Optional.empty()), - Change.empty(), false, Optional.empty(), new ApplicationMetrics(0, 0), + Change.empty(), Change.empty(), Optional.empty(), new ApplicationMetrics(0, 0), Optional.empty()); } /** Used from persistence layer: Do not use */ public Application(ApplicationId id, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, List<Deployment> deployments, DeploymentJobs deploymentJobs, Change change, - boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics, + Change outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics, Optional<RotationId> rotation) { this(id, deploymentSpec, validationOverrides, deployments.stream().collect(Collectors.toMap(Deployment::zone, d -> d)), @@ -67,7 +67,7 @@ public class Application { Application(ApplicationId id, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change, - boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics, + Change outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics, Optional<RotationId> rotation) { Objects.requireNonNull(id, "id cannot be null"); Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null"); @@ -129,7 +129,7 @@ public class Application { * Returns whether this has an outstanding change (in the source repository), which * has currently not started deploying (because a deployment is (or was) already in progress */ - public boolean hasOutstandingChange() { return outstandingChange; } + public Change outstandingChange() { return outstandingChange; } public Optional<IssueId> ownershipIssueId() { return ownershipIssueId; @@ -157,25 +157,31 @@ public class Application { /** Returns the current version this application has, or if none; should use, in the given zone */ public Version versionIn(ZoneId zone, Controller controller) { return Optional.ofNullable(deployments().get(zone)).map(Deployment::version) // Already deployed in this zone: Use that version - .orElse(oldestDeployedVersion().orElse(controller.systemVersion())); - } - - /** Returns the application version a deployment to this zone should use, or empty if we don't know */ - public Optional<ApplicationVersion> deployApplicationVersionIn(ZoneId zone) { - if (change().application().isPresent()) { - ApplicationVersion version = change().application().get(); - if (version == ApplicationVersion.unknown) - return Optional.empty(); - else - return Optional.of(version); + .orElse(oldestDeployedVersion().orElse(controller.systemVersion())); + } + + /** Returns the Vespa version to use for the given job */ + public Version deployVersionFor(DeploymentJobs.JobType jobType, Controller controller) { + return jobType == DeploymentJobs.JobType.component + ? controller.systemVersion() + : deployVersionIn(jobType.zone(controller.system()).get(), controller); + } + + /** Returns the application version to use for the given job */ + public Optional<ApplicationVersion> deployApplicationVersionFor(DeploymentJobs.JobType jobType, + Controller controller, + boolean currentVersion) { + // Use last successful version if currentVersion is requested (staging deployment) or when we're not deploying + // a new application version + if (currentVersion || !change().application().isPresent()) { + Optional<ApplicationVersion> version = deploymentJobs().lastSuccessfulApplicationVersionFor(jobType); + if (version.isPresent()) { + return version; + } } - - return applicationVersionIn(zone); - } - - /** Returns the application version that is or should be deployed with in the given zone, or empty if unknown. */ - public Optional<ApplicationVersion> applicationVersionIn(ZoneId zone) { - return Optional.ofNullable(deployments().get(zone)).map(Deployment::applicationVersion); + return jobType == DeploymentJobs.JobType.component + ? Optional.empty() + : deployApplicationVersionIn(jobType.zone(controller.system()).get()); } /** Returns the global rotation of this, if present */ @@ -203,8 +209,22 @@ public class Application { return "application '" + id + "'"; } + /** Returns whether changes to this are blocked in the given instant */ public boolean isBlocked(Instant instant) { return ! deploymentSpec().canUpgradeAt(instant) || ! deploymentSpec().canChangeRevisionAt(instant); } + /** Returns the application version a deployment to this zone should use, or empty if we don't know */ + private Optional<ApplicationVersion> deployApplicationVersionIn(ZoneId zone) { + if (change().application().isPresent()) { + return Optional.of(change().application().get()); + } + return applicationVersionIn(zone); + } + + /** Returns the application version that is or should be deployed with in the given zone, or empty if unknown. */ + private Optional<ApplicationVersion> applicationVersionIn(ZoneId zone) { + return Optional.ofNullable(deployments().get(zone)).map(Deployment::applicationVersion); + } + } 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 5d75ae4a340..090f25171b1 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 @@ -23,7 +23,6 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.RevisionId; import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory; import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerClient; import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log; import com.yahoo.vespa.hosted.controller.api.integration.configserver.NoInstanceException; @@ -287,41 +286,51 @@ public class ApplicationController { version = application.deployVersionIn(zone, controller); } - Optional<DeploymentJobs.JobType> jobType = DeploymentJobs.JobType.from(controller.system(), zone); - if (!jobType.isPresent() && !applicationPackageFromDeployer.isPresent()) { - throw new IllegalArgumentException("Unable to determine job type from zone '" + zone + - "' and no application package was given"); - } - - // Determine which application package to use - ApplicationPackage applicationPackage; + // Determine application package to use ApplicationVersion applicationVersion; - if (applicationPackageFromDeployer.isPresent()) { - applicationVersion = toApplicationPackageRevision(applicationPackageFromDeployer.get(), - options.screwdriverBuildJob); - applicationPackage = applicationPackageFromDeployer.get(); - } else { - applicationVersion = application.deployApplicationVersion(jobType.get(), controller) - .orElseThrow(() -> new IllegalArgumentException("Cannot determine application version to use in " + zone)); - applicationPackage = new ApplicationPackage(artifactRepository.getApplicationPackage( - applicationId, applicationVersion.id()) + ApplicationPackage applicationPackage; + Optional<DeploymentJobs.JobType> job = DeploymentJobs.JobType.from(controller.system(), zone); + + // TODO: Simplify after new application version is always available + if (canDownloadReportedApplicationVersion(application) && !canDeployDirectlyTo(zone, options)) { + if (!job.isPresent()) { + throw new IllegalArgumentException("Cannot determine job for zone " + zone); + } + applicationVersion = application.deployApplicationVersionFor(job.get(), controller, + options.deployCurrentVersion) + .orElseThrow(() -> new IllegalArgumentException("Cannot determine application version for " + applicationId + " in " + job.get())); + if (canDownloadArtifact(applicationVersion)) { + applicationPackage = new ApplicationPackage( + artifactRepository.getApplicationPackage(applicationId, applicationVersion.id()) + ); + } else { + applicationPackage = applicationPackageFromDeployer.orElseThrow( + () -> new IllegalArgumentException("Application package with version " + + applicationVersion.id() + " cannot be downloaded, and " + + "no package was given by deployer")); + } + } else { // ..otherwise we use the package sent by the deployer and deduce version from the package + // TODO: Only allow this for environments that are allowed to deploy directly + applicationPackage = applicationPackageFromDeployer.orElseThrow( + () -> new IllegalArgumentException("Application package must be given as new application " + + "version is not known for " + applicationId) ); + applicationVersion = toApplicationPackageRevision(applicationPackage, options.screwdriverBuildJob); } - validate(applicationPackage.deploymentSpec()); - // TODO: Remove after introducing new application version number - if ( ! options.deployCurrentVersion && applicationPackageFromDeployer.isPresent()) { + // TODO: Remove after introducing new application version + if (!options.deployCurrentVersion && !canDownloadReportedApplicationVersion(application)) { if (application.change().application().isPresent()) { - application = application.withDeploying(application.change().with(applicationVersion)); + application = application.withChange(application.change().with(applicationVersion)); } - if (!canDeployDirectlyTo(zone, options) && jobType.isPresent()) { + if (!canDeployDirectlyTo(zone, options) && job.isPresent()) { // Update with (potentially) missing information about what we triggered: // * When someone else triggered the job, we need to store a stand-in triggering event. // * When this is the system test job, we need to record the new application version, // for future use. - JobStatus.JobRun triggering = getOrCreateTriggering(application, version, jobType.get()); - application = application.withJobTriggering(jobType.get(), + JobStatus.JobRun triggering = getOrCreateTriggering(application, version, job.get()); + application = application.withJobTriggering(job.get(), application.change(), triggering.at(), version, @@ -406,7 +415,8 @@ public class ApplicationController { Log logEntry = new Log(); logEntry.level = "WARNING"; logEntry.time = clock.instant().toEpochMilli(); - logEntry.message = "Ignoring deployment of " + get(applicationId) + " to " + zone + " as a deployment is not currently expected"; + logEntry.message = "Ignoring deployment of " + require(applicationId) + " to " + zone + + " as a deployment is not currently expected"; PrepareResponse prepareResponse = new PrepareResponse(); prepareResponse.log = Collections.singletonList(logEntry); prepareResponse.configChangeActions = new ConfigChangeActions(Collections.emptyList(), Collections.emptyList()); @@ -682,6 +692,18 @@ public class ApplicationController { zone.environment().isManuallyDeployed(); } + /** Returns whether artifact for given version number is available in artifact repository */ + private static boolean canDownloadArtifact(ApplicationVersion applicationVersion) { + return applicationVersion.buildNumber().isPresent() && applicationVersion.source().isPresent(); + } + + /** Returns whether component has reported a version number that is availabe in artifact repository */ + private static boolean canDownloadReportedApplicationVersion(Application application) { + return application.deploymentJobs().lastSuccessfulApplicationVersionFor(DeploymentJobs.JobType.component) + .filter(ApplicationController::canDownloadArtifact) + .isPresent(); + } + /** Verify that each of the production zones listed in the deployment spec exist in this system. */ private void validate(DeploymentSpec deploymentSpec) { deploymentSpec.zones().stream() 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 e744df0da68..950790e26b6 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java @@ -6,11 +6,11 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; -import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.integration.MetricsService; import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationRotation; import com.yahoo.vespa.hosted.controller.application.ApplicationVersion; import com.yahoo.vespa.hosted.controller.application.Change; @@ -49,7 +49,7 @@ public class LockedApplication extends Application { private LockedApplication(Builder builder) { super(builder.applicationId, builder.deploymentSpec, builder.validationOverrides, builder.deployments, builder.deploymentJobs, builder.deploying, - builder.hasOutstandingChange, builder.ownershipIssueId, builder.metrics, builder.rotation); + builder.outstandingChange, builder.ownershipIssueId, builder.metrics, builder.rotation); } public LockedApplication withProjectId(long projectId) { @@ -60,8 +60,11 @@ public class LockedApplication extends Application { return new LockedApplication(new Builder(this).with(deploymentJobs().with(issueId))); } - public LockedApplication withJobCompletion(DeploymentJobs.JobReport report, Instant notificationTime, Controller controller) { - return new LockedApplication(new Builder(this).with(deploymentJobs().withCompletion(report, notificationTime, controller))); + public LockedApplication withJobCompletion(DeploymentJobs.JobReport report, ApplicationVersion applicationVersion, + Instant notificationTime, Controller controller) { + return new LockedApplication(new Builder(this).with(deploymentJobs().withCompletion( + report, applicationVersion, notificationTime, controller)) + ); } public LockedApplication withJobTriggering(JobType type, Change change, Instant triggerTime, @@ -119,12 +122,12 @@ public class LockedApplication extends Application { return new LockedApplication(new Builder(this).with(validationOverrides)); } - public LockedApplication withDeploying(Change deploying) { - return new LockedApplication(new Builder(this).withDeploying(deploying)); + public LockedApplication withChange(Change change) { + return new LockedApplication(new Builder(this).withChange(change)); } - public LockedApplication withOutstandingChange(boolean outstandingChange) { - return new LockedApplication(new Builder(this).with(outstandingChange)); + public LockedApplication withOutstandingChange(Change outstandingChange) { + return new LockedApplication(new Builder(this).withOutstandingChange(outstandingChange)); } public LockedApplication withOwnershipIssueId(IssueId issueId) { @@ -139,18 +142,6 @@ public class LockedApplication extends Application { return new LockedApplication(new Builder(this).with(rotation)); } - public Version deployVersionFor(DeploymentJobs.JobType jobType, Controller controller) { - return jobType == JobType.component - ? controller.systemVersion() - : deployVersionIn(jobType.zone(controller.system()).get(), controller); - } - - public Optional<ApplicationVersion> deployApplicationVersion(DeploymentJobs.JobType jobType, Controller controller) { - return jobType == JobType.component - ? Optional.empty() - : deployApplicationVersionIn(jobType.zone(controller.system()).get()); - } - /** Don't expose non-leaf sub-objects. */ private LockedApplication with(Deployment deployment) { Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(deployments()); @@ -158,7 +149,6 @@ public class LockedApplication extends Application { return new LockedApplication(new Builder(this).with(deployments)); } - private static class Builder { private final ApplicationId applicationId; @@ -167,7 +157,7 @@ public class LockedApplication extends Application { private Map<ZoneId, Deployment> deployments; private DeploymentJobs deploymentJobs; private Change deploying; - private boolean hasOutstandingChange; + private Change outstandingChange; private Optional<IssueId> ownershipIssueId; private ApplicationMetrics metrics; private Optional<RotationId> rotation; @@ -179,7 +169,7 @@ public class LockedApplication extends Application { this.deployments = application.deployments(); this.deploymentJobs = application.deploymentJobs(); this.deploying = application.change(); - this.hasOutstandingChange = application.hasOutstandingChange(); + this.outstandingChange = application.outstandingChange(); this.ownershipIssueId = application.ownershipIssueId(); this.metrics = application.metrics(); this.rotation = application.rotation().map(ApplicationRotation::id); @@ -205,13 +195,13 @@ public class LockedApplication extends Application { return this; } - private Builder withDeploying(Change deploying) { + private Builder withChange(Change deploying) { this.deploying = deploying; return this; } - private Builder with(boolean hasOutstandingChange) { - this.hasOutstandingChange = hasOutstandingChange; + private Builder withOutstandingChange(Change outstandingChange) { + this.outstandingChange = outstandingChange; return this; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationVersion.java index 304d82b2bec..31ee5fa6d44 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationVersion.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationVersion.java @@ -11,18 +11,19 @@ import java.util.Optional; * @author bratseth * @author mpolden */ -public class ApplicationVersion { +public class ApplicationVersion implements Comparable<ApplicationVersion> { - // TODO: Remove the need for this + /** + * Used in cases where application version cannot be determined, such as manual deployments (e.g. in dev + * environment) + */ public static final ApplicationVersion unknown = new ApplicationVersion(); - // Never changes. Only used to create a valid version number for the bundle + // This never changes and is only used to create a valid semantic version number, as required by application bundles private static final String majorVersion = "1.0"; - // TODO: Remove after introducing new application version + // TODO: Remove after 2018-03-01 private final Optional<String> applicationPackageHash; - - // TODO: Make mandatory private final Optional<SourceRevision> source; private final Optional<Long> buildNumber; @@ -37,11 +38,11 @@ public class ApplicationVersion { Objects.requireNonNull(applicationPackageHash, "applicationPackageHash cannot be null"); Objects.requireNonNull(source, "source cannot be null"); Objects.requireNonNull(buildNumber, "buildNumber cannot be null"); - if (buildNumber.isPresent() && !source.isPresent()) { - throw new IllegalArgumentException("both buildNumber and source must be set if buildNumber is set"); + if (!applicationPackageHash.isPresent() && source.isPresent() != buildNumber.isPresent()) { + throw new IllegalArgumentException("both buildNumber and source must be set together"); } - if ( ! buildNumber.isPresent() && ! applicationPackageHash.isPresent()) { - throw new IllegalArgumentException("applicationPackageHash must be given if buildNumber is unset"); + if (buildNumber.isPresent() && buildNumber.get() <= 0) { + throw new IllegalArgumentException("buildNumber must be > 0"); } this.applicationPackageHash = applicationPackageHash; this.source = source; @@ -68,9 +69,14 @@ public class ApplicationVersion { if (applicationPackageHash.isPresent()) { return applicationPackageHash.get(); } - return String.format("%s.%d-%s", majorVersion, buildNumber.get(), abbreviateCommit(source.get().commit())); + return source.map(sourceRevision -> String.format("%s.%d-%s", majorVersion, buildNumber.get(), + abbreviateCommit(source.get().commit()))) + .orElse("unknown"); } + /** Returns the application package hash, if known */ + public Optional<String> applicationPackageHash() { return applicationPackageHash; } + /** * Returns information about the source of this revision, or empty if the source is not know/defined * (which is the case for command-line deployment from developers, but never for deployment jobs) @@ -80,23 +86,29 @@ public class ApplicationVersion { /** Returns the build number that built this version */ public Optional<Long> buildNumber() { return buildNumber; } + /** Returns whether this is unknown */ + public boolean isUnknown() { + return this.equals(unknown); + } + @Override - public int hashCode() { return applicationPackageHash.hashCode(); } + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ApplicationVersion that = (ApplicationVersion) o; + return Objects.equals(applicationPackageHash, that.applicationPackageHash) && + Objects.equals(source, that.source) && + Objects.equals(buildNumber, that.buildNumber); + } @Override - public boolean equals(Object other) { - if (this == other) return true; - if ( ! (other instanceof ApplicationVersion)) return false; - return this.applicationPackageHash.equals(((ApplicationVersion)other).applicationPackageHash); + public int hashCode() { + return Objects.hash(applicationPackageHash, source, buildNumber); } @Override public String toString() { - if (buildNumber.isPresent()) { - return "Application package version: " + abbreviateCommit(source.get().commit()) + "-" + buildNumber.get(); - } - return "Application package revision '" + applicationPackageHash + "'" + - (source.isPresent() ? " with " + source.get() : ""); + return "Application package version: " + id() + source.map(s -> ", " + s.toString()).orElse(""); } /** Abbreviate given commit hash to 9 characters */ @@ -104,4 +116,11 @@ public class ApplicationVersion { return hash.length() <= 9 ? hash : hash.substring(0, 9); } + @Override + public int compareTo(ApplicationVersion o) { + if (!buildNumber().isPresent() || !o.buildNumber().isPresent()) { + return 0; // No sorting for application package hash + } + return buildNumber().get().compareTo(o.buildNumber().get()); + } } 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 13d66c8d083..216025215d3 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 @@ -31,6 +31,10 @@ public final class Change { private Change(Optional<Version> platform, Optional<ApplicationVersion> application) { Objects.requireNonNull(platform, "platform cannot be null"); Objects.requireNonNull(application, "application cannot be null"); + if (application.isPresent() && application.get().isUnknown()) { + // TODO: Require version to be known for application change + //throw new IllegalArgumentException("Application version to deploy must be a known version"); + } this.platform = platform; this.application = application; } @@ -42,7 +46,7 @@ public final class Change { return false; } - /** Returns whether a change shoudl currently be deployed */ + /** Returns whether a change should currently be deployed */ public boolean isPresent() { return platform.isPresent() || application.isPresent(); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java index bb7b39eed0f..7e1dcfe2bc6 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java @@ -55,11 +55,13 @@ public class DeploymentJobs { } /** Return a new instance with the given completion */ - public DeploymentJobs withCompletion(JobReport report, Instant notificationTime, Controller controller) { + public DeploymentJobs withCompletion(JobReport report, ApplicationVersion applicationVersion, + Instant notificationTime, Controller controller) { Map<JobType, JobStatus> status = new LinkedHashMap<>(this.status); status.compute(report.jobType(), (type, job) -> { if (job == null) job = JobStatus.initial(report.jobType()); - return job.withCompletion(report.buildNumber(), report.jobError(), notificationTime, controller); + return job.withCompletion(report.buildNumber(), applicationVersion, report.jobError(), notificationTime, + controller); }); return new DeploymentJobs(Optional.of(report.projectId()), status, issueId); } @@ -129,12 +131,11 @@ public class DeploymentJobs { return true; // other environments do not have any preconditions } - /** Returns whether the job of the given type has completed successfully for the given change */ - public boolean isSuccessful(Change change, JobType jobType) { + /** Returns the last successful application version for the given job */ + public Optional<ApplicationVersion> lastSuccessfulApplicationVersionFor(JobType jobType) { return Optional.ofNullable(jobStatus().get(jobType)) - .flatMap(JobStatus::lastSuccess) - .filter(status -> status.lastCompletedWas(change)) - .isPresent(); + .flatMap(JobStatus::lastSuccess) + .map(JobStatus.JobRun::applicationVersion); } /** @@ -146,6 +147,14 @@ public class DeploymentJobs { public Optional<IssueId> issueId() { return issueId; } + /** Returns whether the job of the given type has completed successfully for the given change */ + private boolean isSuccessful(Change change, JobType jobType) { + return Optional.ofNullable(jobStatus().get(jobType)) + .flatMap(JobStatus::lastSuccess) + .filter(status -> status.lastCompletedWas(change)) + .isPresent(); + } + private static Optional<Long> requireId(Optional<Long> id, String message) { Objects.requireNonNull(id, message); if ( ! id.isPresent()) { @@ -254,11 +263,16 @@ public class DeploymentJobs { Objects.requireNonNull(sourceRevision, "sourceRevision cannot be null"); Objects.requireNonNull(jobError, "jobError cannot be null"); + if (jobType == JobType.component && !sourceRevision.isPresent()) { + // TODO: Throw after 2018-03-01 + //throw new IllegalArgumentException("sourceRevision is required for job " + jobType); + } + this.applicationId = applicationId; this.projectId = projectId; this.buildNumber = buildNumber; this.jobType = jobType; - this.sourceRevision = sourceRevision; // TODO: Require non-empty source revision if jobType == component + this.sourceRevision = sourceRevision; this.jobError = jobError; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java index 41060a7af4c..fb9ab8735d8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java @@ -172,7 +172,7 @@ public class JobList { private static boolean failingApplicationChange(JobStatus job) { if ( job.isSuccess()) return false; if ( ! job.lastSuccess().isPresent()) return true; // An application which never succeeded is surely bad. - if ( job.lastSuccess().get().applicationVersion() == ApplicationVersion.unknown) return true; // Indicates the component job, which is always an application change. + if ( job.lastSuccess().get().applicationVersion().isUnknown()) return true; // Indicates the component job, which is always an application change. if ( ! job.firstFailing().get().version().equals(job.lastSuccess().get().version())) return false; // Version change may be to blame. return ! job.firstFailing().get().applicationVersion().equals(job.lastSuccess().get().applicationVersion()); // Return whether there is an application change. } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java index e165d3c9fe5..e963fde8f94 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java @@ -57,27 +57,31 @@ public class JobStatus { public JobStatus withTriggering(Version version, ApplicationVersion applicationVersion, boolean upgrade, String reason, Instant triggerTime) { - return new JobStatus(type, jobError, Optional.of(new JobRun(-1, version, applicationVersion, upgrade, reason, triggerTime)), + return new JobStatus(type, jobError, Optional.of(new JobRun(-1, version, applicationVersion, upgrade, + reason, triggerTime)), lastCompleted, firstFailing, lastSuccess); } - public JobStatus withCompletion(long runId, Optional<DeploymentJobs.JobError> jobError, Instant completionTime, Controller controller) { + public JobStatus withCompletion(long runId, Optional<DeploymentJobs.JobError> jobError, Instant completionTime, + Controller controller) { + return withCompletion(runId, ApplicationVersion.unknown, jobError, completionTime, controller); + } + + public JobStatus withCompletion(long runId, ApplicationVersion applicationVersion, + Optional<DeploymentJobs.JobError> jobError, Instant completionTime, + Controller controller) { Version version; - ApplicationVersion applicationVersion; boolean upgrade; String reason; if (type == DeploymentJobs.JobType.component) { // not triggered by us version = controller.systemVersion(); - applicationVersion = ApplicationVersion.unknown; upgrade = false; reason = "Application commit"; - } - else if (! lastTriggered.isPresent()) { + } else if (! lastTriggered.isPresent()) { throw new IllegalStateException("Got notified about completion of " + this + ", but that has neither been triggered nor deployed"); - } - else { + } else { version = lastTriggered.get().version(); applicationVersion = lastTriggered.get().applicationVersion(); upgrade = lastTriggered.get().upgrade(); @@ -197,7 +201,7 @@ public class JobStatus { /** Returns the Vespa version used on this run */ public Version version() { return version; } - /** Returns the application version used for this run, or empty when not known */ + /** Returns the application version used in this run */ public ApplicationVersion applicationVersion() { return applicationVersion; } /** Returns a human-readable reason for this particular job run */ 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 1beab1307c1..067d8d41f53 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 @@ -78,7 +78,8 @@ public class DeploymentTrigger { */ public void triggerFromCompletion(JobReport report) { applications().lockOrThrow(report.applicationId(), application -> { - application = application.withJobCompletion(report, clock.instant(), controller); + ApplicationVersion applicationVersion = applicationVersionFrom(report); + application = application.withJobCompletion(report, applicationVersion, clock.instant(), controller); application = application.withProjectId(report.projectId()); // Handle successful starting and ending @@ -87,20 +88,17 @@ public class DeploymentTrigger { if (acceptNewApplicationVersionNow(application)) { // Set this as the change we are doing, unless we are already pushing a platform change if ( ! ( application.change().platform().isPresent())) { - ApplicationVersion applicationVersion = ApplicationVersion.unknown; - if (report.sourceRevision().isPresent()) - applicationVersion = ApplicationVersion.from(report.sourceRevision().get(), report.buildNumber()); - application = application.withDeploying(Change.of(applicationVersion)); + application = application.withChange(Change.of(applicationVersion)); } } else { // postpone - applications().store(application.withOutstandingChange(true)); + applications().store(application.withOutstandingChange(Change.of(applicationVersion))); return; } } else if (deploymentComplete(application)) { // change completed - application = application.withDeploying(Change.empty()); + application = application.withChange(Change.empty()); } } @@ -139,7 +137,7 @@ public class DeploymentTrigger { } if (change.application().isPresent()) { // If we don't yet know the application version we are deploying, then we are not complete - if (change.application().get() == ApplicationVersion.unknown) return false; + if (change.application().get().isUnknown()) return false; if ( ! change.application().get().equals(deployment.applicationVersion())) return false; } } @@ -260,9 +258,9 @@ public class DeploymentTrigger { if (application.change().isPresent() && ! application.deploymentJobs().hasFailures()) throw new IllegalArgumentException("Could not start " + change + " on " + application + ": " + application.change() + " is already in progress"); - application = application.withDeploying(change); + application = application.withChange(change); if (change.application().isPresent()) - application = application.withOutstandingChange(false); + application = application.withOutstandingChange(Change.empty()); application = trigger(JobType.systemTest, application, false, change.toString()); applications().store(application); }); @@ -276,7 +274,7 @@ public class DeploymentTrigger { public void cancelChange(ApplicationId applicationId) { applications().lockOrThrow(applicationId, application -> { buildSystem.removeJobs(application.id()); - applications().store(application.withDeploying(Change.empty())); + applications().store(application.withChange(Change.empty())); }); } @@ -355,10 +353,17 @@ public class DeploymentTrigger { application.change(), clock.instant(), application.deployVersionFor(jobType, controller), - application.deployApplicationVersion(jobType, controller).orElse(ApplicationVersion.unknown), + application.deployApplicationVersionFor(jobType, controller, false) + .orElse(ApplicationVersion.unknown), reason); } + /** Create application version from job report */ + private ApplicationVersion applicationVersionFrom(JobReport report) { + return report.sourceRevision().map(sr -> ApplicationVersion.from(sr, report.buildNumber())) + .orElse(ApplicationVersion.unknown); + } + /** Returns true if the given proposed job triggering should be effected */ private boolean allowedTriggering(JobType jobType, LockedApplication application) { // Note: We could make a more fine-grained and more correct determination about whether to block 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 3dd63a511e1..9f1738f9560 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 @@ -4,8 +4,6 @@ package com.yahoo.vespa.hosted.controller.maintenance; 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.ApplicationVersion; -import com.yahoo.vespa.hosted.controller.application.Change; import java.time.Duration; @@ -24,9 +22,9 @@ public class OutstandingChangeDeployer extends Maintainer { protected void maintain() { ApplicationList applications = ApplicationList.from(controller().applications().asList()).notPullRequest(); for (Application application : applications.asList()) { - if (application.hasOutstandingChange() && ! application.change().isPresent()) + if (!application.change().isPresent() && application.outstandingChange().isPresent()) controller().applications().deploymentTrigger().triggerChange(application.id(), - Change.of(ApplicationVersion.unknown)); + application.outstandingChange()); } } 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 652f95a2d13..670338d10e5 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java @@ -6,15 +6,16 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; -import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; +import com.yahoo.slime.Type; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationVersion; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.ClusterInfo; @@ -61,6 +62,7 @@ public class ApplicationSerializer { private final String environmentField = "environment"; private final String regionField = "region"; private final String deployTimeField = "deployTime"; + private final String applicationBuildNumberField = "applicationBuildNumber"; private final String applicationPackageRevisionField = "applicationPackageRevision"; private final String applicationPackageHashField = "applicationPackageHash"; private final String sourceRevisionField = "sourceRevision"; @@ -125,8 +127,8 @@ public class ApplicationSerializer { root.setString(validationOverridesField, application.validationOverrides().xmlForm()); deploymentsToSlime(application.deployments().values(), root.setArray(deploymentsField)); toSlime(application.deploymentJobs(), root.setObject(deploymentJobsField)); - toSlime(application.change(), root); - root.setBool(outstandingChangeField, application.hasOutstandingChange()); + toSlime(application.change(), root, deployingField); + toSlime(application.outstandingChange(), root, outstandingChangeField); application.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value())); root.setDouble(queryQualityField, application.metrics().queryServiceQuality()); root.setDouble(writeQualityField, application.metrics().writeServiceQuality()); @@ -198,9 +200,15 @@ public class ApplicationSerializer { } private void toSlime(ApplicationVersion applicationVersion, Cursor object) { - object.setString(applicationPackageHashField, applicationVersion.id()); - if (applicationVersion.source().isPresent()) + if (applicationVersion.buildNumber().isPresent() && applicationVersion.source().isPresent()) { + object.setLong(applicationBuildNumberField, applicationVersion.buildNumber().get()); toSlime(applicationVersion.source().get(), object.setObject(sourceRevisionField)); + } else if (applicationVersion.applicationPackageHash().isPresent()) { // TODO: Remove after 2018-03-01 + object.setString(applicationPackageHashField, applicationVersion.applicationPackageHash().get()); + if (applicationVersion.source().isPresent()){ + toSlime(applicationVersion.source().get(), object.setObject(sourceRevisionField)); + } + } } private void toSlime(SourceRevision sourceRevision, Cursor object) { @@ -236,20 +244,19 @@ public class ApplicationSerializer { Cursor object = parent.setObject(jobRunObjectName); object.setLong(jobRunIdField, jobRun.get().id()); object.setString(versionField, jobRun.get().version().toString()); - if ( jobRun.get().applicationVersion() != ApplicationVersion.unknown) - toSlime(jobRun.get().applicationVersion(), object.setObject(revisionField)); + toSlime(jobRun.get().applicationVersion(), object.setObject(revisionField)); object.setBool(upgradeField, jobRun.get().upgrade()); object.setString(reasonField, jobRun.get().reason()); object.setLong(atField, jobRun.get().at().toEpochMilli()); } - private void toSlime(Change deploying, Cursor parentObject) { + private void toSlime(Change deploying, Cursor parentObject, String fieldName) { if ( ! deploying.isPresent()) return; - Cursor object = parentObject.setObject(deployingField); + Cursor object = parentObject.setObject(fieldName); if (deploying.platform().isPresent()) object.setString(versionField, deploying.platform().get().toString()); - if (deploying.application().isPresent() && deploying.application().get() != ApplicationVersion.unknown) + if (deploying.application().isPresent()) toSlime(deploying.application().get(), object); } @@ -264,7 +271,7 @@ public class ApplicationSerializer { List<Deployment> deployments = deploymentsFromSlime(root.field(deploymentsField)); DeploymentJobs deploymentJobs = deploymentJobsFromSlime(root.field(deploymentJobsField)); Change deploying = changeFromSlime(root.field(deployingField)); - boolean outstandingChange = root.field(outstandingChangeField).asBool(); + Change outstandingChange = outstandingChangeFromSlime(root.field(outstandingChangeField)); Optional<IssueId> ownershipIssueId = optionalString(root.field(ownershipIssueIdField)).map(IssueId::from); ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(), root.field(writeQualityField).asDouble()); @@ -342,10 +349,17 @@ public class ApplicationSerializer { private ApplicationVersion applicationVersionFromSlime(Inspector object) { if ( ! object.valid()) return ApplicationVersion.unknown; - String applicationPackageHash = object.field(applicationPackageHashField).asString(); + Optional<String> applicationPackageHash = optionalString(object.field(applicationPackageHashField)); + Optional<Long> applicationBuildNumber = optionalLong(object.field(applicationBuildNumberField)); Optional<SourceRevision> sourceRevision = sourceRevisionFromSlime(object.field(sourceRevisionField)); - return sourceRevision.isPresent() ? ApplicationVersion.from(applicationPackageHash, sourceRevision.get()) - : ApplicationVersion.from(applicationPackageHash); + if (applicationPackageHash.isPresent()) { // TODO: Remove after 2018-03-01 + return sourceRevision.map(sr -> ApplicationVersion.from(applicationPackageHash.get(), sr)) + .orElseGet(() -> ApplicationVersion.from(applicationPackageHash.get())); + } + if (!sourceRevision.isPresent() || !applicationBuildNumber.isPresent()) { + return ApplicationVersion.unknown; + } + return ApplicationVersion.from(sourceRevision.get(), applicationBuildNumber.get()); } private Optional<SourceRevision> sourceRevisionFromSlime(Inspector object) { @@ -369,13 +383,23 @@ public class ApplicationSerializer { Change change = Change.empty(); if (versionFieldValue.valid()) change = Change.of(Version.fromString(versionFieldValue.asString())); - if (object.field(applicationPackageHashField).valid()) + if (object.field(applicationBuildNumberField).valid() || + object.field(applicationPackageHashField).valid()) // TODO: Remove after 2018-03-01 change = change.with(applicationVersionFromSlime(object)); if ( ! change.isPresent()) // A deploy object with no fields -> unknown application change change = Change.of(ApplicationVersion.unknown); return change; } + // TODO: Remove and inline after 2018-03-01 + private Change outstandingChangeFromSlime(Inspector object) { + if (object.type() == Type.BOOL) { + boolean outstandingChange = object.asBool(); + return outstandingChange ? Change.of(ApplicationVersion.unknown) : Change.empty(); + } + return changeFromSlime(object); + } + private List<JobStatus> jobStatusListFromSlime(Inspector array) { List<JobStatus> jobStatusList = new ArrayList<>(); array.traverse((ArrayTraverser) (int i, Inspector item) -> jobStatusList.add(jobStatusFromSlime(item))); 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 9019e843c2c..2105ea1d3d9 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 @@ -475,9 +475,10 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private void toSlime(ApplicationVersion applicationVersion, Cursor object) { - object.setString("hash", applicationVersion.id()); - if (applicationVersion.source().isPresent()) + if (!applicationVersion.isUnknown()) { + object.setString("hash", applicationVersion.id()); sourceRevisionToSlime(applicationVersion.source(), object.setObject("source")); + } } private void sourceRevisionToSlime(Optional<SourceRevision> revision, Cursor object) { @@ -967,7 +968,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private void toSlime(JobStatus.JobRun jobRun, Cursor object) { object.setLong("id", jobRun.id()); object.setString("version", jobRun.version().toFullString()); - if (jobRun.applicationVersion() != ApplicationVersion.unknown) + if (!jobRun.applicationVersion().isUnknown()) toSlime(jobRun.applicationVersion(), object.setObject("revision")); object.setString("reason", jobRun.reason()); object.setLong("at", jobRun.at().toEpochMilli()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ArtifactRepositoryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ArtifactRepositoryMock.java index efbc10e8deb..cfe60cd3e96 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ArtifactRepositoryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ArtifactRepositoryMock.java @@ -14,11 +14,22 @@ import java.util.Objects; */ public class ArtifactRepositoryMock implements ArtifactRepository { - private final Map<Integer, byte[]> repository = new HashMap<>(); + private final Map<Integer, Artifact> repository = new HashMap<>(); public ArtifactRepositoryMock put(ApplicationId applicationId, ApplicationPackage applicationPackage, String applicationVersion) { - repository.put(artifactHash(applicationId, applicationVersion), applicationPackage.zippedContent()); + repository.put(artifactHash(applicationId, applicationVersion), + new Artifact(applicationPackage.zippedContent())); + return this; + } + + public int hits(ApplicationId applicationId, String applicationVersion) { + Artifact artifact = repository.get(artifactHash(applicationId, applicationVersion)); + return artifact == null ? 0 : artifact.hits; + } + + public ArtifactRepository resetHits() { + repository.values().forEach(Artifact::resetHits); return this; } @@ -29,11 +40,32 @@ public class ArtifactRepositoryMock implements ArtifactRepository { throw new IllegalArgumentException("No application package found for " + applicationId + " with version " + applicationVersion); } - return repository.get(artifactHash); + Artifact artifact = repository.get(artifactHash); + artifact.recordHit(); + return artifact.data; } private static int artifactHash(ApplicationId applicationId, String applicationVersion) { return Objects.hash(applicationId, applicationVersion); } + private class Artifact { + + private final byte[] data; + private int hits = 0; + + private Artifact(byte[] data) { + this.data = data; + } + + private void recordHit() { + hits++; + } + + private void resetHits() { + hits = 0; + } + + } + } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerClientMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerClientMock.java index bd5aef1ec3a..08f96195d2a 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerClientMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerClientMock.java @@ -42,8 +42,8 @@ public class ConfigServerClientMock extends AbstractComponent implements ConfigS private final Map<URI, Version> versions = new HashMap<>(); private Version defaultVersion = new Version(6, 1, 0); - private RuntimeException prepareException = null; private Version lastPrepareVersion = null; + private RuntimeException prepareException = null; /** The version given in the previous prepare call, or empty if no call has been made */ public Optional<Version> lastPrepareVersion() { @@ -72,7 +72,11 @@ public class ConfigServerClientMock extends AbstractComponent implements ConfigS public void setDefaultVersion(Version version) { this.defaultVersion = version; } - + + public Version getDefaultVersion() { + return defaultVersion; + } + @Override public PreparedApplication prepare(DeploymentId deployment, DeployOptions deployOptions, Set<String> rotationCnames, Set<String> rotationNames, byte[] content) { 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 916494a0636..1f2a9c4e910 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 @@ -102,7 +102,7 @@ public class ControllerTest { .build(); // staging job - succeeding - Version version1 = Version.fromString("6.1"); // Set in config server mock + Version version1 = tester.defaultVespaVersion(); Application app1 = tester.createApplication("app1", "tenant1", 1, 11L); tester.notifyJobCompletion(component, app1, true); assertEquals("Application version is currently not known", @@ -205,129 +205,64 @@ public class ControllerTest { assertNull("Deployment job was removed", applications.require(app1.id()).deploymentJobs().jobStatus().get(productionCorpUsEast1)); } - // TODO: Replace above test with this one after introducing new application version number @Test - public void testDeploymentWithApplicationVersion() { - // Setup system + public void testDeploymentApplicationVersion() { DeploymentTester tester = new DeploymentTester(); - ApplicationController applications = tester.controller().applications(); - Version version1 = Version.fromString("6.1"); // Set in config server mock - Application app1 = tester.createApplication("app1", "tenant1", 1, 11L); - - // Component runs, uploads artifact and notifies completion + Application app = tester.createApplication("app1", "tenant1", 1, 11L); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("corp-us-east-1") .region("us-east-3") .build(); - SourceRevision source = new SourceRevision("repo", "branch", "deadbeef"); - String expectedVersionString = "1.0.37-deadbeef"; - tester.artifactRepository().put(app1.id(), applicationPackage, expectedVersionString); - tester.notifyJobCompletion(component, app1, Optional.empty(), Optional.of(source), 37); - ApplicationVersion expectedVersion = ApplicationVersion.from(source, 37); - assertEquals(expectedVersionString, tester.controller().applications() - .require(app1.id()) - .change().application().get().id()); - - // Deploy without application package - tester.deployAndNotify(app1, true, systemTest); - tester.deployAndNotify(app1, true, stagingTest); - assertEquals(4, applications.require(app1.id()).deploymentJobs().jobStatus().size()); - assertStatus(JobStatus.initial(stagingTest) - .withTriggering(version1, expectedVersion, false, "", tester.clock().instant().minus(Duration.ofMillis(1))) - .withCompletion(42, Optional.empty(), tester.clock().instant(), tester.controller()), app1.id(), tester.controller()); + SourceRevision source = new SourceRevision("repo", "master", "commit1"); - // Causes first deployment job to be triggered - assertStatus(JobStatus.initial(productionCorpUsEast1) - .withTriggering(version1, expectedVersion, false, "", tester.clock().instant()), app1.id(), tester.controller()); - tester.clock().advance(Duration.ofSeconds(1)); + ApplicationVersion applicationVersion = ApplicationVersion.from(source, 101); + tester.artifactRepository().put(app.id(), applicationPackage, applicationVersion.id()); + runDeployment(tester, app.id(), applicationVersion, Optional.empty(), Optional.of(source), 101); + assertEquals("Artifact is downloaded twice in staging and once for other zones", 5, + tester.artifactRepository().hits(app.id(), applicationVersion.id())); - // production job (failing) - tester.deployAndNotify(app1, false, productionCorpUsEast1); - assertEquals(4, applications.require(app1.id()).deploymentJobs().jobStatus().size()); - - JobStatus expectedJobStatus = JobStatus.initial(productionCorpUsEast1) - .withTriggering(version1, expectedVersion, false, "", tester.clock().instant()) - .withCompletion(42, Optional.of(JobError.unknown), tester.clock().instant(), tester.controller()); - - assertStatus(expectedJobStatus, app1.id(), tester.controller()); - - // Simulate restart - tester.restartController(); - applications = tester.controller().applications(); - - assertNotNull(tester.controller().tenants().tenant(new TenantId("tenant1"))); - assertNotNull(applications.get(ApplicationId.from(TenantName.from("tenant1"), - ApplicationName.from("application1"), - InstanceName.from("default")))); - assertEquals(4, applications.require(app1.id()).deploymentJobs().jobStatus().size()); - - - tester.clock().advance(Duration.ofHours(1)); - - tester.notifyJobCompletion(productionCorpUsEast1, app1, false); // Need to complete the job, or new jobs won't start. - - // Component is triggered again - tester.artifactRepository().put(app1.id(), applicationPackage, "1.0.38-deadbeef"); - tester.notifyJobCompletion(component, app1, Optional.empty(), Optional.of(source), 38); - tester.deployAndNotify(app1, Optional.empty(), true, false, systemTest); - expectedVersion = ApplicationVersion.from(source, 38); - assertStatus(JobStatus.initial(systemTest) - .withTriggering(version1, expectedVersion, false, "", tester.clock().instant().minus(Duration.ofMillis(1))) - .withCompletion(42, Optional.empty(), tester.clock().instant(), tester.controller()), app1.id(), tester.controller()); - tester.deployAndNotify(app1, Optional.empty(), true, true, stagingTest); - - // production job succeeding now - tester.deployAndNotify(app1, Optional.empty(), true, true, productionCorpUsEast1); - expectedJobStatus = expectedJobStatus - .withTriggering(version1, expectedVersion, false, "", tester.clock().instant().minus(Duration.ofMillis(1))) - .withCompletion(42, Optional.empty(), tester.clock().instant(), tester.controller()); - assertStatus(expectedJobStatus, app1.id(), tester.controller()); - - // causes triggering of next production job - assertStatus(JobStatus.initial(productionUsEast3) - .withTriggering(version1, expectedVersion, false, "", tester.clock().instant()), - app1.id(), tester.controller()); - tester.deployAndNotify(app1, Optional.empty(), true, true, productionUsEast3); - - assertEquals(5, applications.get(app1.id()).get().deploymentJobs().jobStatus().size()); - - // prod zone removal is not allowed - applicationPackage = new ApplicationPackageBuilder() - .environment(Environment.prod) - .region("us-east-3") - .build(); - tester.artifactRepository().put(app1.id(), applicationPackage, "1.0.56-cafed00d"); - source = new SourceRevision("repo", "branch", "cafed00d"); - tester.notifyJobCompletion(component, app1, Optional.empty(), Optional.of(source), 56); - try { - tester.deploy(systemTest, app1, Optional.empty(), false); - fail("Expected exception due to unallowed production deployment removal"); - } - catch (IllegalArgumentException e) { - assertEquals("deployment-removal: application 'tenant1.app1' is deployed in corp-us-east-1, but does not include this zone in deployment.xml", e.getMessage()); - } - assertNotNull("Zone was not removed", - applications.require(app1.id()).deployments().get(productionCorpUsEast1.zone(SystemName.main).get())); - JobStatus jobStatus = applications.require(app1.id()).deploymentJobs().jobStatus().get(productionCorpUsEast1); - assertNotNull("Deployment job was not removed", jobStatus); - assertEquals(42, jobStatus.lastCompleted().get().id()); - assertEquals("staging-test completed", jobStatus.lastCompleted().get().reason()); + // Application is upgraded. This makes deployment orchestration pick the last successful application version in + // zones which do not have permanent deployments, e.g. test and staging + runUpgrade(tester, app.id(), applicationVersion); + } - // prod zone removal is allowed with override - applicationPackage = new ApplicationPackageBuilder() - .allow(ValidationId.deploymentRemoval) - .upgradePolicy("default") + @Test + public void testDeploymentApplicationVersionMigration() { + DeploymentTester tester = new DeploymentTester(); + Application app = tester.createApplication("app1", "tenant1", 1, 11L); + ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) + .region("corp-us-east-1") .region("us-east-3") .build(); - tester.artifactRepository().put(app1.id(), applicationPackage, "1.0.103-c00ffefe"); - source = new SourceRevision("repo", "branch", "c00ffefe"); - tester.notifyJobCompletion(component, app1, Optional.empty(), Optional.of(source), 103); - tester.deployAndNotify(app1, Optional.empty(), true, true, systemTest); - assertNull("Zone was removed", - applications.require(app1.id()).deployments().get(productionCorpUsEast1.zone(SystemName.main).get())); - assertNull("Deployment job was removed", applications.require(app1.id()).deploymentJobs().jobStatus().get(productionCorpUsEast1)); + SourceRevision source = new SourceRevision("repo", "master", "commit1"); + + // Scenario 1: Old fashioned deployment. Simulates existing production deployments + ApplicationVersion v0 = ApplicationVersion.from(applicationPackage.hash(), source); + runDeployment(tester, app.id(), v0, Optional.of(applicationPackage), Optional.empty(), 100); + assertEquals("Nothing downloaded from repository", 0, + tester.artifactRepository().hits(app.id(), v0.id())); + + // Scenario 2: New application version number is reported and package is downloaded by controller. In staging, + // the application package from the deployer is used as v0 cannot be downloaded from repository. + ApplicationVersion v1 = ApplicationVersion.from(source, 101); + tester.artifactRepository().put(app.id(), applicationPackage, v1.id()); + runDeployment(tester, app.id(), v1, Optional.of(applicationPackage), Optional.of(source), 101); + assertEquals("Artifact is downloaded once per zone", 4, + tester.artifactRepository().hits(app.id(), v1.id())); + assertEquals("v0 is never downloaded", 0, + tester.artifactRepository().hits(app.id(), v0.id())); + tester.artifactRepository().resetHits(); + + // Scenario 3: Both application versions are available in repository + ApplicationVersion v2 = ApplicationVersion.from(source, 102); + tester.artifactRepository().put(app.id(), applicationPackage, v2.id()); + runDeployment(tester, app.id(), v2, Optional.empty(), Optional.of(source), 102); + assertEquals("Previous artifact is downloaded once", 1, + tester.artifactRepository().hits(app.id(), v1.id())); + assertEquals("Artifact is downloaded once per zone", 4, + tester.artifactRepository().hits(app.id(), v2.id())); } @Test @@ -666,19 +601,20 @@ public class ControllerTest { @Test public void testDeployUntestedChangeFails() { - ControllerTester tester = new ControllerTester(); + DeploymentTester tester = new DeploymentTester(); ApplicationController applications = tester.controller().applications(); - TenantId tenant = tester.createTenant("tenant1", "domain1", 11L); - Application app = tester.createApplication(tenant, "app1", "default", 1); + TenantId tenant = tester.controllerTester().createTenant("tenant1", "domain1", 11L); + Application app = tester.controllerTester().createApplication(tenant, "app1", "default", 1); + tester.deployCompletely(app, applicationPackage); tester.controller().applications().lockOrThrow(app.id(), application -> { - application = application.withDeploying(Change.of(Version.fromString("6.3"))); + application = application.withChange(Change.of(Version.fromString("6.3"))); applications.store(application); try { - tester.deploy(app, ZoneId.from("prod", "us-east-3")); + tester.controllerTester().deploy(app, ZoneId.from("prod", "corp-us-east-1"), applicationPackage); fail("Expected exception"); } catch (IllegalArgumentException e) { - assertEquals("Rejecting deployment of application 'tenant1.app1' to zone prod.us-east-3 as upgrade to 6.3 is not tested", e.getMessage()); + assertEquals("Rejecting deployment of application 'tenant1.app1' to zone prod.corp-us-east-1 as upgrade to 6.3 is not tested", e.getMessage()); } }); } @@ -919,6 +855,60 @@ public class ControllerTest { } + private void runUpgrade(DeploymentTester tester, ApplicationId application, ApplicationVersion version) { + Version next = Version.fromString("6.2"); + tester.upgradeSystem(next); + runDeployment(tester, tester.applications().require(application), version, Optional.of(next), Optional.empty()); + } + + private void runDeployment(DeploymentTester tester, ApplicationId application, ApplicationVersion version, + Optional<ApplicationPackage> applicationPackage, Optional<SourceRevision> sourceRevision, + long initialBuildNumber) { + Application app = tester.applications().require(application); + tester.notifyJobCompletion(component, app, Optional.empty(), sourceRevision, initialBuildNumber); + ApplicationVersion change = sourceRevision.map(sr -> ApplicationVersion.from(sr, initialBuildNumber)) + .orElse(ApplicationVersion.unknown); + assertEquals(change.id(), tester.controller().applications() + .require(application) + .change().application().get().id()); + runDeployment(tester, app, version, Optional.empty(), applicationPackage); + } + + private void runDeployment(DeploymentTester tester, Application app, ApplicationVersion version, + Optional<Version> upgrade, Optional<ApplicationPackage> applicationPackage) { + Version vespaVersion = upgrade.orElseGet(tester::defaultVespaVersion); + + // Deploy in test + tester.deployAndNotify(app, applicationPackage, true, true, systemTest); + tester.deployAndNotify(app, applicationPackage, true, true, stagingTest); + assertStatus(JobStatus.initial(stagingTest) + .withTriggering(vespaVersion, version, upgrade.isPresent(), "", + tester.clock().instant().minus(Duration.ofMillis(1))) + .withCompletion(42, Optional.empty(), tester.clock().instant(), + tester.controller()), app.id(), tester.controller()); + + // Deploy in production + tester.deployAndNotify(app, applicationPackage, true, true, productionCorpUsEast1); + assertStatus(JobStatus.initial(productionCorpUsEast1) + .withTriggering(vespaVersion, version, upgrade.isPresent(), "", + tester.clock().instant().minus(Duration.ofMillis(1))) + .withCompletion(42, Optional.empty(), tester.clock().instant(), + tester.controller()), app.id(), tester.controller()); + tester.deployAndNotify(app, applicationPackage, true, true, productionUsEast3); + assertStatus(JobStatus.initial(productionUsEast3) + .withTriggering(vespaVersion, version, upgrade.isPresent(), "", + tester.clock().instant().minus(Duration.ofMillis(1))) + .withCompletion(42, Optional.empty(), tester.clock().instant(), + tester.controller()), app.id(), tester.controller()); + + // Verify deployed version + app = tester.controller().applications().require(app.id()); + for (Deployment deployment : app.productionDeployments().values()) { + assertEquals(version, deployment.applicationVersion()); + upgrade.ifPresent(v -> assertEquals(v, deployment.version())); + } + } + @Test public void testDeploymentOfNewInstanceWithIllegalApplicationName() { ControllerTester tester = new ControllerTester(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java index d0dfe825558..c923542b8c9 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java @@ -104,11 +104,15 @@ public class DeploymentTester { } public void upgradeSystem(Version version) { - controllerTester().configServer().setDefaultVersion(version); + configServer().setDefaultVersion(version); updateVersionStatus(version); upgrader().maintain(); } + public Version defaultVespaVersion() { + return configServer().getDefaultVersion(); + } + public Application createApplication(String applicationName, String tenantName, long projectId, Long propertyId) { TenantId tenant = tester.createTenant(tenantName, UUID.randomUUID().toString(), propertyId); return tester.createApplication(tenant, applicationName, "default", projectId); @@ -253,10 +257,6 @@ public class DeploymentTester { deployCurrentVersion)); } - public void deployAndNotify(Application application, boolean success, JobType... job) { - deployAndNotify(application, Optional.empty(), success, true, job); - } - public void deployAndNotify(Application application, String upgradePolicy, boolean success, JobType... jobs) { deployAndNotify(application, applicationPackage(upgradePolicy), success, true, jobs); } @@ -276,9 +276,17 @@ public class DeploymentTester { consumeJobs(application, expectOnlyTheseJobs, jobs); for (JobType job : jobs) { if (success) { + // Staging deploys twice, once with current version and once with new version + if (job == JobType.stagingTest) { + deploy(job, application, applicationPackage, true); + } deploy(job, application, applicationPackage, false); } notifyJobCompletion(job, application, success); + // Deactivate test deployments after deploy. This replicates the behaviour of the tenant pipeline + if (job.isTest()) { + controller().applications().deactivate(application, job.zone(controller().system()).get()); + } } } 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 5c61e43f9cf..029eb335d82 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 @@ -320,7 +320,7 @@ public class DeploymentTriggerTest { new JobControl(tester.controllerTester().curator())); LockedApplication app = (LockedApplication)tester.createAndDeploy("default0", 3, "default"); // Store that we are upgrading but don't start the system-tests job - tester.controller().applications().store(app.withDeploying(Change.of(Version.fromString("6.2")))); + tester.controller().applications().store(app.withChange(Change.of(Version.fromString("6.2")))); assertEquals(0, tester.buildSystem().jobs().size()); readyJobsTrigger.run(); assertEquals(1, tester.buildSystem().jobs().size()); 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 3d34e78c759..4bed276fcac 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 @@ -2,15 +2,18 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.component.Version; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.api.integration.BuildService; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; +import com.yahoo.vespa.hosted.controller.application.SourceRevision; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; import org.junit.Test; import java.time.Duration; import java.util.List; +import java.util.Optional; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -34,9 +37,15 @@ public class OutstandingChangeDeployerTest { tester.deploymentTrigger().triggerChange(tester.application("app1").id(), Change.of(version)); assertEquals(Change.of(version), tester.application("app1").change()); - assertFalse(tester.application("app1").hasOutstandingChange()); - tester.notifyJobCompletion(DeploymentJobs.JobType.component, tester.application("app1"), true); - assertTrue(tester.application("app1").hasOutstandingChange()); + assertFalse(tester.application("app1").outstandingChange().isPresent()); + tester.notifyJobCompletion(DeploymentJobs.JobType.component, tester.application("app1"), + Optional.empty(), Optional.of(new SourceRevision("repo", "master", + "cafed00d")), + 42); + + Application app = tester.application("app1"); + assertTrue(app.outstandingChange().isPresent()); + assertEquals("1.0.42-cafed00d", app.outstandingChange().application().get().id()); assertEquals(1, tester.buildSystem().jobs().size()); deployer.maintain(); @@ -50,7 +59,7 @@ public class OutstandingChangeDeployerTest { assertEquals(1, jobs.size()); assertEquals(11, jobs.get(0).projectId()); assertEquals(DeploymentJobs.JobType.systemTest.jobName(), jobs.get(0).jobName()); - assertFalse(tester.application("app1").hasOutstandingChange()); + assertFalse(tester.application("app1").outstandingChange().isPresent()); } } 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 9a945281789..9c19cdb66ac 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 @@ -62,9 +62,9 @@ public class ApplicationSerializerTest { "</validation-overrides>"); List<Deployment> deployments = new ArrayList<>(); - ApplicationVersion applicationVersion1 = ApplicationVersion.from("appHash1"); + ApplicationVersion applicationVersion1 = ApplicationVersion.from(new SourceRevision("repo1", "branch1", "commit1"), 31); ApplicationVersion applicationVersion2 = ApplicationVersion - .from("appHash2", new SourceRevision("repo1", "branch1", "commit1")); + .from(new SourceRevision("repo1", "branch1", "commit1"), 32); deployments.add(new Deployment(zone1, applicationVersion1, Version.fromString("1.2.3"), Instant.ofEpochMilli(3))); // One deployment without cluster info and utils deployments.add(new Deployment(zone2, applicationVersion2, Version.fromString("1.2.3"), Instant.ofEpochMilli(5), createClusterUtils(3, 0.2), createClusterInfo(3, 4),new DeploymentMetrics(2,3,4,5,6))); @@ -86,7 +86,7 @@ public class ApplicationSerializerTest { validationOverrides, deployments, deploymentJobs, Change.of(Version.fromString("6.7")), - true, + Change.of(ApplicationVersion.from(new SourceRevision("repo", "master", "deadcafe"), 42)), Optional.of(IssueId.from("1234")), new MetricsService.ApplicationMetrics(0.5, 0.9), Optional.of(new RotationId("my-rotation"))); @@ -113,7 +113,7 @@ public class ApplicationSerializerTest { assertEquals( original.deploymentJobs().jobStatus().get(DeploymentJobs.JobType.stagingTest), serialized.deploymentJobs().jobStatus().get(DeploymentJobs.JobType.stagingTest)); - assertEquals(original.hasOutstandingChange(), serialized.hasOutstandingChange()); + assertEquals(original.outstandingChange(), serialized.outstandingChange()); assertEquals(original.ownershipIssueId(), serialized.ownershipIssueId()); @@ -148,22 +148,29 @@ public class ApplicationSerializerTest { assertEquals(6, serialized.deployments().get(zone2).metrics().writeLatencyMillis(), Double.MIN_VALUE); { // test more deployment serialization cases - Application original2 = writable(original).withDeploying(Change.of(ApplicationVersion.from("hash1"))); + Application original2 = writable(original).withChange(Change.of(ApplicationVersion.from("hash1"))); Application serialized2 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original2)); assertEquals(original2.change(), serialized2.change()); assertEquals(serialized2.change().application().get().source(), original2.change().application().get().source()); - Application original3 = writable(original).withDeploying(Change.of(ApplicationVersion.from("hash1", - new SourceRevision("a", "b", "c")))); + Application original3 = writable(original).withChange(Change.of(ApplicationVersion.from("hash1", + new SourceRevision("a", "b", "c")))); Application serialized3 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original3)); - assertEquals(original3.change(), serialized2.change()); + assertEquals(original3.change(), serialized3.change()); assertEquals(serialized3.change().application().get().source(), original3.change().application().get().source()); - - Application original4 = writable(original).withDeploying(Change.empty()); + Application original4 = writable(original).withChange(Change.empty()); Application serialized4 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original4)); assertEquals(original4.change(), serialized4.change()); + + Application original5 = writable(original).withChange(Change.of(ApplicationVersion.unknown)); + Application serialized5 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original5)); + assertEquals(original5.change(), serialized5.change()); + + Application original6 = writable(original).withOutstandingChange(Change.of(ApplicationVersion.unknown)); + Application serialized6 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original6)); + assertEquals(original6.outstandingChange(), serialized6.outstandingChange()); } } |