summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Marius Venstad <jonmv@users.noreply.github.com>2019-01-09 09:43:51 +0100
committerGitHub <noreply@github.com>2019-01-09 09:43:51 +0100
commit43abdf70ca8b20f97be487aded69080f73718f94 (patch)
tree14581a69daf7538493d5b35f3e23d7ee9de0cd6a
parentd4da6ddd1884fb81f37484b8eab36a475f3eea32 (diff)
parent3603fe508abce34bf2aae7f7710203e497e29943 (diff)
Merge pull request #8058 from vespa-engine/jvenstad/pin-versions
Jvenstad/pin versions
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java59
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java12
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java82
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java66
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment-cancelled.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-pin-cancelled.json1
15 files changed, 202 insertions, 63 deletions
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 ab97db06484..3185b7d5f45 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
@@ -671,7 +671,7 @@ public class ApplicationController {
private void validateRun(Application application, ZoneId zone, Version platformVersion, ApplicationVersion applicationVersion) {
Deployment deployment = application.deployments().get(zone);
if ( zone.environment().isProduction() && deployment != null
- && ( platformVersion.compareTo(deployment.version()) < 0
+ && ( platformVersion.compareTo(deployment.version()) < 0 && ! application.change().isPinned()
|| applicationVersion.compareTo(deployment.applicationVersion()) < 0))
throw new IllegalArgumentException(String.format("Rejecting deployment of %s to %s, as the requested versions (platform: %s, application: %s)" +
" are older than the currently deployed (platform: %s, application: %s).",
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 9e8b2495616..677b4b4ba07 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
@@ -63,6 +63,11 @@ public class ApplicationList {
return listOf(list.stream().filter(application -> isUpgradingTo(version, application)));
}
+ /** Returns the subset of applications which are not pinned to a certain Vespa version. */
+ public ApplicationList notPinned() {
+ return listOf(list.stream().filter(application -> ! application.change().isPinned()));
+ }
+
/** Returns the subset of applications which are currently not upgrading to the given version */
public ApplicationList notUpgradingTo(Version version) {
return listOf(list.stream().filter(application -> ! isUpgradingTo(version, application)));
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 ab16f84f628..7b77105cc67 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
@@ -20,7 +20,7 @@ import java.util.StringJoiner;
*/
public final class Change {
- private static final Change empty = new Change(Optional.empty(), Optional.empty());
+ private static final Change empty = new Change(Optional.empty(), Optional.empty(), false);
/** The platform version we are upgrading to, or empty if none */
private final Optional<Version> platform;
@@ -28,7 +28,9 @@ public final class Change {
/** The application version we are changing to, or empty if none */
private final Optional<ApplicationVersion> application;
- private Change(Optional<Version> platform, Optional<ApplicationVersion> application) {
+ 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");
if (application.isPresent() && application.get().isUnknown()) {
@@ -36,14 +38,15 @@ public final class Change {
}
this.platform = platform;
this.application = application;
+ this.pinned = pinned;
}
public Change withoutPlatform() {
- return new Change(Optional.empty(), application);
+ return new Change(Optional.empty(), application, pinned);
}
public Change withoutApplication() {
- return new Change(platform, Optional.empty());
+ return new Change(platform, Optional.empty(), pinned);
}
/** Returns whether a change should currently be deployed */
@@ -57,17 +60,32 @@ public final class Change {
/** Returns the application version carried by this. */
public Optional<ApplicationVersion> application() { return application; }
+ public boolean isPinned() { return pinned; }
+
/** Returns an instance representing no change */
public static Change empty() { return empty; }
/** Returns a version of this change which replaces or adds this platform change */
public Change with(Version platformVersion) {
- return new Change(Optional.of(platformVersion), application);
+ if (pinned)
+ throw new IllegalArgumentException("Not allowed to set a platform version when pinned.");
+
+ return new Change(Optional.of(platformVersion), application, pinned);
}
/** Returns a version of this change which replaces or adds this application change */
public Change with(ApplicationVersion applicationVersion) {
- return new Change(platform, Optional.of(applicationVersion));
+ return new Change(platform, Optional.of(applicationVersion), pinned);
+ }
+
+ /** Returns a change with the versions of this, and with the platform version pinned. */
+ public Change withPin() {
+ return new Change(platform, application, true);
+ }
+
+ /** Returns a change with the versions of this, and with the platform version unpinned. */
+ public Change withoutPin() {
+ return new Change(platform, application, false);
}
/** Returns the change obtained when overwriting elements of the given change with any present in this */
@@ -76,37 +94,44 @@ public final class Change {
other = other.with(platform.get());
if (application.isPresent())
other = other.with(application.get());
+ if (pinned)
+ other = other.withPin();
return other;
}
@Override
- public int hashCode() { return Objects.hash(platform, application); }
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Change)) return false;
+ Change change = (Change) o;
+ return pinned == change.pinned &&
+ Objects.equals(platform, change.platform) &&
+ Objects.equals(application, change.application);
+ }
@Override
- public boolean equals(Object other) {
- if (other == this) return true;
- if ( ! (other instanceof Change)) return false;
- Change o = (Change)other;
- if ( ! o.platform.equals(this.platform)) return false;
- if ( ! o.application.equals(this.application)) return false;
- return true;
+ public int hashCode() {
+ return Objects.hash(platform, application, pinned);
}
@Override
public String toString() {
StringJoiner changes = new StringJoiner(" and ");
- platform.ifPresent(version -> changes.add("upgrade to " + version.toString()));
+ if (pinned)
+ changes.add("pin to " + platform.map(Version::toString).orElse("current platform"));
+ else
+ platform.ifPresent(version -> changes.add("upgrade to " + version.toString()));
application.ifPresent(version -> changes.add("application change to " + version.id()));
changes.setEmptyValue("no change");
return changes.toString();
}
public static Change of(ApplicationVersion applicationVersion) {
- return new Change(Optional.empty(), Optional.of(applicationVersion));
+ return new Change(Optional.empty(), Optional.of(applicationVersion), false);
}
public static Change of(Version platformChange) {
- return new Change(Optional.of(platformChange), Optional.empty());
+ return new Change(Optional.of(platformChange), Optional.empty(), false);
}
/** Returns whether this change carries an application downgrade relative to the given version. */
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 3c51640cb01..f6f843cabcb 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
@@ -256,15 +256,17 @@ public class DeploymentTrigger {
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;
default: throw new IllegalArgumentException("Unknown cancellation choice '" + cancellation + "'!");
}
applications().store(application.withChange(change));
});
}
- public enum ChangesToCancel { ALL, PLATFORM, APPLICATION }
+ public enum ChangesToCancel { ALL, PLATFORM, APPLICATION, VERSIONS, PIN }
// ---------- Conveniences ----------
@@ -456,9 +458,17 @@ public class DeploymentTrigger {
* is already deployed in its zone, i.e., no parts of the change are upgrades, and the full current
* change for the application downgrades the deployment, which is an acknowledgement that the deployed
* version is broken somehow, such that the job may be locked in failure until a new version is released.
+ *
+ * Additionally, if the application is pinned to a Vespa version, and the given change has a (this) platform,
+ * the deployment for the job must be on the pinned version.
*/
public boolean isComplete(Change change, Application application, JobType jobType) {
Optional<Deployment> existingDeployment = deploymentFor(application, jobType);
+ if ( change.isPinned()
+ && change.platform().isPresent()
+ && ! existingDeployment.map(Deployment::version).equals(change.platform()))
+ return false;
+
return application.deploymentJobs().statusOf(jobType).flatMap(JobStatus::lastSuccess)
.map(job -> change.platform().map(job.platform()::equals).orElse(true)
&& change.application().map(job.application()::equals).orElse(true))
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 d0cc6ee5a6f..bef61dda875 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
@@ -127,6 +127,9 @@ public class Versions {
private static Version targetPlatform(Application application, Change change, Optional<Deployment> deployment,
Version defaultVersion) {
+ if (change.isPinned() && change.platform().isPresent())
+ return change.platform().get();
+
return max(change.platform(), deployment.map(Deployment::version))
.orElse(application.oldestDeployedPlatform()
.orElse(defaultVersion));
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 c1665dfba42..3aa61fe8370 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
@@ -92,8 +92,10 @@ public class Upgrader extends Maintainer {
return reversed;
}
- /** Returns a list of all applications */
- private ApplicationList applications() { return ApplicationList.from(controller().applications().asList()); }
+ /** Returns a list of all applications, except those which are pinned — these should not be manipulated by the Upgrader */
+ private ApplicationList applications() {
+ return ApplicationList.from(controller().applications().asList()).notPinned();
+ }
private void upgrade(ApplicationList applications, Version version) {
applications = applications.hasProductionDeployment();
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 f4d87d7c967..69ea36c7e3a 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
@@ -60,6 +60,7 @@ public class ApplicationSerializer {
private final String deploymentsField = "deployments";
private final String deploymentJobsField = "deploymentJobs";
private final String deployingField = "deployingField";
+ private final String pinnedField = "pinned";
private final String outstandingChangeField = "outstandingChangeField";
private final String ownershipIssueIdField = "ownershipIssueId";
private final String ownerField = "confirmedOwner";
@@ -275,13 +276,15 @@ public class ApplicationSerializer {
}
private void toSlime(Change deploying, Cursor parentObject, String fieldName) {
- if ( ! deploying.isPresent()) return;
+ if ( ! deploying.isPresent() && ! deploying.isPinned()) return;
Cursor object = parentObject.setObject(fieldName);
if (deploying.platform().isPresent())
object.setString(versionField, deploying.platform().get().toString());
if (deploying.application().isPresent())
toSlime(deploying.application().get(), object);
+ if (deploying.isPinned())
+ object.setBool(pinnedField, true);
}
private void toSlime(Map<HostName, RotationStatus> rotationStatus, Cursor array) {
@@ -435,8 +438,8 @@ public class ApplicationSerializer {
change = Change.of(Version.fromString(versionFieldValue.asString()));
if (object.field(applicationBuildNumberField).valid())
change = change.with(applicationVersionFromSlime(object));
- if ( ! change.isPresent()) // A deploy object with no fields -> unknown application change
- change = Change.empty();
+ if (object.field(pinnedField).asBool())
+ change = change.withPin();
return change;
}
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 afaa0e88d0e..c8104c6e2b0 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
@@ -68,6 +68,7 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.RotationStatus;
import com.yahoo.vespa.hosted.controller.athenz.impl.ZmsClientFacade;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel;
import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse;
import com.yahoo.vespa.hosted.controller.restapi.MessageResponse;
import com.yahoo.vespa.hosted.controller.restapi.ResourceResponse;
@@ -100,7 +101,6 @@ import java.util.Optional;
import java.util.Scanner;
import java.util.logging.Level;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.ALL;
import static java.util.stream.Collectors.joining;
/**
@@ -203,7 +203,9 @@ 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}/application/{application}")) return createApplication(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/promote")) return promoteApplication(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return deploy(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"), readToString(request.getData()), false);
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), readToString(request.getData()), true);
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/application")) return deployApplication(path.get("tenant"), path.get("application"));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/jobreport")) return notifyJobCompletion(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}/job/{jobtype}")) return trigger(appIdFromPath(path), jobTypeFromPath(path), request);
@@ -219,7 +221,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Path path = new Path(request.getUri().getPath());
if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(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"));
+ 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}/submit")) return JobControllerApiHandlerHelper.unregisterResponse(controller.jobController(), path.get("tenant"), path.get("application"));
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}/environment/{environment}/region/{region}/instance/{instance}")) return deactivate(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
@@ -755,48 +758,63 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
/**
* Trigger deployment of the given Vespa version if a valid one is given, e.g., "7.8.9",
- * or the latest known commit of the application if "commit" is given,
- * or an upgrade to the system version if no data is provided.
+ * optionally pinning to that version if.
*/
- private HttpResponse deploy(String tenantName, String applicationName, HttpRequest request) {
+ private HttpResponse deployPlatform(String tenantName, String applicationName, String versionString, boolean pin) {
ApplicationId id = ApplicationId.from(tenantName, applicationName, "default");
- String requestVersion = readToString(request.getData());
StringBuilder response = new StringBuilder();
controller.applications().lockOrThrow(id, application -> {
- Change change;
- if ("commit".equals(requestVersion))
- change = Change.of(application.get().deploymentJobs().statusOf(JobType.component)
- .get().lastSuccess().get().application());
- else {
- Version version = requestVersion == null ? controller.systemVersion() : new Version(requestVersion);
- if ( ! systemHasVersion(version))
- throw new IllegalArgumentException("Cannot trigger deployment of version '" + version + "': " +
- "Version is not active in this system. " +
- "Active versions: " + controller.versionStatus().versions()
- .stream()
- .map(VespaVersion::versionNumber)
- .map(Version::toString)
- .collect(joining(", ")));
- change = Change.of(version);
- }
+ Version version = Version.fromString(versionString);
+ if (version.equals(Version.emptyVersion))
+ version = controller.systemVersion();
+ if ( ! systemHasVersion(version))
+ throw new IllegalArgumentException("Cannot trigger deployment of version '" + version + "': " +
+ "Version is not active in this system. " +
+ "Active versions: " + controller.versionStatus().versions()
+ .stream()
+ .map(VespaVersion::versionNumber)
+ .map(Version::toString)
+ .collect(joining(", ")));
+ Change change = Change.of(version);
+ if (pin)
+ change = change.withPin();
+
+ controller.applications().deploymentTrigger().forceChange(id, change);
+ response.append("Triggered " + change + " for " + id);
+ });
+ return new MessageResponse(response.toString());
+ }
+
+ /** Trigger deployment to the last known application package for the given application. */
+ private HttpResponse deployApplication(String tenantName, String applicationName) {
+ ApplicationId id = ApplicationId.from(tenantName, applicationName, "default");
+ StringBuilder response = new StringBuilder();
+ controller.applications().lockOrThrow(id, application -> {
+ Change change = Change.of(application.get().deploymentJobs().statusOf(JobType.component).get().lastSuccess().get().application());
controller.applications().deploymentTrigger().forceChange(id, change);
response.append("Triggered " + change + " for " + id);
});
return new MessageResponse(response.toString());
}
- /** Cancel any ongoing change for given application */
- private HttpResponse cancelDeploy(String tenantName, String applicationName) {
+ /** Cancel ongoing change for given application, e.g., everything with {"cancel":"all"} */
+ private HttpResponse cancelDeploy(String tenantName, String applicationName, String choice) {
ApplicationId id = ApplicationId.from(tenantName, applicationName, "default");
- Application application = controller.applications().require(id);
- Change change = application.change();
- if ( ! change.isPresent())
- return new MessageResponse("No deployment in progress for " + application + " at this time");
+ StringBuilder response = new StringBuilder();
+ controller.applications().lockOrThrow(id, application -> {
+ Change change = application.get().change();
+ if ( ! change.isPresent()) {
+ response.append("No deployment in progress for " + application + " at this time");
+ return;
+ }
- controller.applications().lockOrThrow(id, lockedApplication ->
- controller.applications().deploymentTrigger().cancelChange(id, ALL));
+ ChangesToCancel cancel = ChangesToCancel.valueOf(choice.toUpperCase());
+ controller.applications().deploymentTrigger().cancelChange(id, cancel);
+ response.append("Changed deployment from '" + change + "' to '" +
+ controller.applications().require(id).change() + "' for " + application);
+ });
- return new MessageResponse("Cancelled " + change + " for " + application);
+ return new MessageResponse(response.toString());
}
/** Schedule restart of deployment, or specific host in a deployment */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
index bc628818fbe..b8e868ed8a4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
@@ -131,7 +131,7 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase {
if (isHostedOperatorOperation(path, method)) return false;
return path.matches("/application/v4/tenant/{tenant}") ||
path.matches("/application/v4/tenant/{tenant}/application/{application}") ||
- path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying") ||
+ path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/{*}") ||
path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{job}/{*}") ||
path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/dev/{*}") ||
path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/perf/{*}") ||
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 b81d9e78339..6755a6f9ad5 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
@@ -10,8 +10,10 @@ import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
+import com.yahoo.vespa.hosted.controller.deployment.BuildJob;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import org.junit.Before;
@@ -29,6 +31,7 @@ import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobTy
import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.stagingTest;
import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.systemTest;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.ALL;
+import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.PIN;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -1132,4 +1135,67 @@ public class UpgraderTest {
assertFalse(tester.application(app.id()).change().isPresent());
}
+ @Test
+ public void testPinning() {
+ Version version0 = Version.fromString("6.2");
+ tester.upgradeSystem(version0);
+
+ // Create an application with pinned platform version.
+ Application application = tester.createApplication("application", "tenant", 2, 3);
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder().environment(Environment.prod)
+ .region("us-east-3")
+ .region("us-west-1")
+ .build();
+ tester.deploymentTrigger().forceChange(application.id(), Change.empty().withPin());
+
+ tester.deployCompletely(application, applicationPackage);
+ assertFalse(tester.application(application.id()).change().isPresent());
+ assertTrue(tester.application(application.id()).change().isPinned());
+ assertEquals(2, tester.application(application.id()).deployments().size());
+
+ // Application does not upgrade.
+ Version version1 = Version.fromString("6.3");
+ tester.upgradeSystem(version1);
+ tester.upgrader().maintain();
+ assertFalse(tester.application(application.id()).change().isPresent());
+ assertTrue(tester.application(application.id()).change().isPinned());
+
+ // New application package is deployed.
+ tester.deployCompletely(application, applicationPackage, BuildJob.defaultBuildNumber + 1);
+ assertFalse(tester.application(application.id()).change().isPresent());
+ assertTrue(tester.application(application.id()).change().isPinned());
+
+ // Application upgrades to new version when pin is removed.
+ tester.deploymentTrigger().cancelChange(application.id(), PIN);
+ tester.upgrader().maintain();
+ assertTrue(tester.application(application.id()).change().isPresent());
+ assertFalse(tester.application(application.id()).change().isPinned());
+
+ // Application is pinned to new version, and upgrade is therefore not cancelled, even though confidence is broken.
+ tester.deploymentTrigger().forceChange(application.id(), Change.empty().withPin());
+ tester.upgrader().maintain();
+ tester.readyJobTrigger().maintain();
+ assertEquals(version1, tester.application(application.id()).change().platform().get());
+
+ // Application fails upgrade after one zone is complete, and is pinned again to the old version.
+ tester.deployAndNotify(application, true, systemTest);
+ tester.deployAndNotify(application, true, stagingTest);
+ tester.deployAndNotify(application, true, productionUsEast3);
+ tester.deploy(productionUsWest1, application, Optional.empty(), false);
+ tester.deployAndNotify(application, false, productionUsWest1);
+ tester.deploymentTrigger().cancelChange(application.id(), ALL);
+ tester.deploymentTrigger().forceChange(application.id(), Change.of(version0).withPin());
+ tester.buildService().clear();
+ assertEquals(version0, tester.application(application.id()).change().platform().get());
+
+ // Application downgrades to pinned version.
+ tester.readyJobTrigger().maintain();
+ tester.deployAndNotify(application, true, systemTest);
+ tester.deployAndNotify(application, true, stagingTest);
+ tester.deployAndNotify(application, true, productionUsEast3);
+ assertTrue(tester.application(application.id()).change().isPresent());
+ tester.deployAndNotify(application, true, productionUsWest1);
+ assertFalse(tester.application(application.id()).change().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 7dff2667b9f..774caea97b0 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
@@ -104,7 +104,7 @@ public class ApplicationSerializerTest {
deploymentSpec,
validationOverrides,
deployments, deploymentJobs,
- Change.of(Version.fromString("6.7")),
+ Change.of(Version.fromString("6.7")).withPin(),
Change.of(ApplicationVersion.from(new SourceRevision("repo", "master", "deadcafe"), 42)),
Optional.of(IssueId.from("1234")),
Optional.of(User.from("by-username")),
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 d9950553e38..d455218f4e9 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
@@ -365,15 +365,21 @@ public class ApplicationApiTest extends ControllerContainerTest {
// DELETE (cancel) again is a no-op
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", DELETE)
- .userIdentity(USER_ID),
+ .userIdentity(USER_ID)
+ .data("{\"cancel\":\"all\"}"),
new File("application-deployment-cancelled-no-op.json"));
- // POST triggering of a full deployment to an application (if version is omitted, current system version is used)
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", POST)
+ // POST pinning to a given version to an application
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying/pin", POST)
.userIdentity(USER_ID)
.data("6.1.0"),
new File("application-deployment.json"));
+ // DELETE only the pin to a given version
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying/pin", DELETE)
+ .userIdentity(USER_ID),
+ new File("application-pin-cancelled.json"));
+
// POST a pause to a production job
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default/job/production-us-west-1/pause", POST)
.userIdentity(USER_ID),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment-cancelled.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment-cancelled.json
index bc09003d86f..efca5831256 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment-cancelled.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment-cancelled.json
@@ -1 +1 @@
-{"message":"Cancelled application change to 1.0.42-commit1 for application 'tenant1.application1'"}
+{"message":"Changed deployment from 'application change to 1.0.42-commit1' to 'no change' for application 'tenant1.application1'"}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment.json
index d2531638a93..fe68f3d94a3 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-deployment.json
@@ -1 +1 @@
-{"message":"Triggered upgrade to 6.1 for tenant1.application1"} \ No newline at end of file
+{"message":"Triggered pin to 6.1 for tenant1.application1"} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-pin-cancelled.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-pin-cancelled.json
new file mode 100644
index 00000000000..62360458ce4
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-pin-cancelled.json
@@ -0,0 +1 @@
+{"message":"Changed deployment from 'pin to 6.1' to 'upgrade to 6.1' for application 'tenant1.application1'"}