summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@oath.com>2019-01-14 13:16:06 +0100
committerJon Bratseth <bratseth@oath.com>2019-01-14 13:16:06 +0100
commitebfc65452ac5f5a9f0aa100763dc22a80cfd7167 (patch)
tree94b6e12ceb2c4d164baab0dbe63f4d2256fcd402 /controller-server
parent8b57fffd5dd15bf5da117191cf40d443a967b5c0 (diff)
Add application level majorVersion override in application/v4
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java21
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java52
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java4
-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.java36
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java38
-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.java117
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-majorVersion.json87
9 files changed, 281 insertions, 85 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 1baff026385..82355144b20 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
@@ -52,6 +52,7 @@ public class Application {
private final Change outstandingChange;
private final Optional<IssueId> ownershipIssueId;
private final Optional<User> owner;
+ private final Optional<Integer> majorVersion;
private final ApplicationMetrics metrics;
private final Optional<RotationId> rotation;
private final Map<HostName, RotationStatus> rotationStatus;
@@ -60,23 +61,27 @@ public class Application {
public Application(ApplicationId id, Instant now) {
this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Collections.emptyMap(),
new DeploymentJobs(OptionalLong.empty(), Collections.emptyList(), Optional.empty(), false),
- Change.empty(), Change.empty(), Optional.empty(), Optional.empty(), new ApplicationMetrics(0, 0),
+ Change.empty(), Change.empty(), Optional.empty(), Optional.empty(), Optional.empty(),
+ new ApplicationMetrics(0, 0),
Optional.empty(), Collections.emptyMap());
}
/** Used from persistence layer: Do not use */
public Application(ApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
List<Deployment> deployments, DeploymentJobs deploymentJobs, Change change,
- Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, ApplicationMetrics metrics,
+ Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner,
+ Optional<Integer> majorVersion, ApplicationMetrics metrics,
Optional<RotationId> rotation, Map<HostName, RotationStatus> rotationStatus) {
this(id, createdAt, deploymentSpec, validationOverrides,
deployments.stream().collect(Collectors.toMap(Deployment::zone, d -> d)),
- deploymentJobs, change, outstandingChange, ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion,
+ metrics, rotation, rotationStatus);
}
Application(ApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change,
- Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, ApplicationMetrics metrics,
+ Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner,
+ Optional<Integer> majorVersion, ApplicationMetrics metrics,
Optional<RotationId> rotation, Map<HostName, RotationStatus> rotationStatus) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null");
@@ -88,6 +93,7 @@ public class Application {
this.outstandingChange = Objects.requireNonNull(outstandingChange, "outstandingChange cannot be null");
this.ownershipIssueId = Objects.requireNonNull(ownershipIssueId, "ownershipIssueId cannot be null");
this.owner = Objects.requireNonNull(owner, "owner cannot be null");
+ this.majorVersion = Objects.requireNonNull(majorVersion, "majorVersion cannot be null");
this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null");
this.rotation = Objects.requireNonNull(rotation, "rotation cannot be null");
this.rotationStatus = ImmutableMap.copyOf(Objects.requireNonNull(rotationStatus, "rotationStatus cannot be null"));
@@ -146,6 +152,13 @@ public class Application {
return owner;
}
+ /**
+ * Overrides the preferred major version for this application.
+ * This overrides the major version set in the deployment spec (if any) and the major version the system
+ * wants to use.
+ */
+ public Optional<Integer> majorVersion() { return majorVersion; }
+
/** Returns metrics for this */
public ApplicationMetrics metrics() {
return metrics;
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 1e138dd5a4d..6eb321918e6 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
@@ -50,7 +50,8 @@ public class LockedApplication {
private final Change change;
private final Change outstandingChange;
private final Optional<IssueId> ownershipIssueId;
- private Optional<User> owner;
+ private final Optional<User> owner;
+ private final Optional<Integer> majorVersion;
private final ApplicationMetrics metrics;
private final Optional<RotationId> rotation;
private final Map<HostName, RotationStatus> rotationStatus;
@@ -66,7 +67,7 @@ public class LockedApplication {
application.deploymentSpec(), application.validationOverrides(),
application.deployments(),
application.deploymentJobs(), application.change(), application.outstandingChange(),
- application.ownershipIssueId(), application.owner(), application.metrics(),
+ application.ownershipIssueId(), application.owner(), application.majorVersion(), application.metrics(),
application.rotation(),
application.rotationStatus());
}
@@ -74,7 +75,8 @@ public class LockedApplication {
private LockedApplication(Lock lock, ApplicationId id, Instant createdAt,
DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change,
- Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, ApplicationMetrics metrics,
+ Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner,
+ Optional<Integer> majorVersion, ApplicationMetrics metrics,
Optional<RotationId> rotation, Map<HostName, RotationStatus> rotationStatus) {
this.lock = lock;
this.id = id;
@@ -87,6 +89,7 @@ public class LockedApplication {
this.outstandingChange = outstandingChange;
this.ownershipIssueId = ownershipIssueId;
this.owner = owner;
+ this.majorVersion = majorVersion;
this.metrics = metrics;
this.rotation = rotation;
this.rotationStatus = rotationStatus;
@@ -95,44 +98,44 @@ public class LockedApplication {
/** Returns a read-only copy of this */
public Application get() {
return new Application(id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change,
- outstandingChange, ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ outstandingChange, ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withBuiltInternally(boolean builtInternally) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs.withBuiltInternally(builtInternally), change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withProjectId(OptionalLong projectId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs.withProjectId(projectId), change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withDeploymentIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs.with(issueId), change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withJobPause(JobType jobType, OptionalLong pausedUntil) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs.withPause(jobType, pausedUntil), change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withJobCompletion(long projectId, JobType jobType, JobStatus.JobRun completion,
Optional<DeploymentJobs.JobError> jobError) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs.withCompletion(projectId, jobType, completion, jobError),
- change, outstandingChange, ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withJobTriggering(JobType jobType, JobStatus.JobRun job) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs.withTriggering(jobType, job), change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withNewDeployment(ZoneId zone, ApplicationVersion applicationVersion, Version version,
@@ -182,60 +185,67 @@ public class LockedApplication {
public LockedApplication withoutDeploymentJob(JobType jobType) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs.without(jobType), change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication with(DeploymentSpec deploymentSpec) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs, change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication with(ValidationOverrides validationOverrides) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs, change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withChange(Change change) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs, change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withOutstandingChange(Change outstandingChange) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs, change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withOwnershipIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs, change, outstandingChange,
- Optional.ofNullable(issueId), owner, metrics, rotation, rotationStatus);
+ Optional.ofNullable(issueId), owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication withOwner(User owner) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs, change, outstandingChange,
- ownershipIssueId, Optional.ofNullable(owner), metrics, rotation, rotationStatus);
+ ownershipIssueId, Optional.ofNullable(owner), majorVersion, metrics, rotation, rotationStatus);
+ }
+
+ /** Set a major vewrsion for this, or set to null to remove any major version override */
+ public LockedApplication withMajorVersion(Integer majorVersion) {
+ return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
+ deploymentJobs, change, outstandingChange,
+ ownershipIssueId, owner, Optional.ofNullable(majorVersion), metrics, rotation, rotationStatus);
}
public LockedApplication with(MetricsService.ApplicationMetrics metrics) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs, change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
public LockedApplication with(RotationId rotation) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs, change, outstandingChange,
- ownershipIssueId, owner, metrics, Optional.of(rotation), rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, Optional.of(rotation), rotationStatus);
}
public LockedApplication withRotationStatus(Map<HostName, RotationStatus> rotationStatus) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change,
- outstandingChange, ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ outstandingChange, ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
/** Don't expose non-leaf sub-objects. */
@@ -248,7 +258,7 @@ public class LockedApplication {
private LockedApplication with(Map<ZoneId, Deployment> deployments) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments,
deploymentJobs, change, outstandingChange,
- ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
@Override
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 677b4b4ba07..d279e899df0 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
@@ -160,13 +160,13 @@ public class ApplicationList {
}
/**
- * Returns the subset of applications that hasn't pinned to an an earlier major version than the given one.
+ * Returns the subset of applications that hasn't pinned to another major version than the given one.
*
* @param targetMajorVersion the target major version which applications returned allows upgrading to
* @param defaultMajorVersion the default major version to assume for applications not specifying one
*/
public ApplicationList allowMajorVersion(int targetMajorVersion, int defaultMajorVersion) {
- return listOf(list.stream().filter(a -> a.deploymentSpec().majorVersion().orElse(defaultMajorVersion)
+ return listOf(list.stream().filter(a -> a.majorVersion().orElse(a.deploymentSpec().majorVersion().orElse(defaultMajorVersion))
>= targetMajorVersion));
}
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 147b2edee3e..ffb900f0fac 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
@@ -64,6 +64,7 @@ public class ApplicationSerializer {
private final String outstandingChangeField = "outstandingChangeField";
private final String ownershipIssueIdField = "ownershipIssueId";
private final String ownerField = "confirmedOwner";
+ private final String majorVersionField = "majorVersion";
private final String writeQualityField = "writeQuality";
private final String queryQualityField = "queryQuality";
private final String rotationField = "rotation";
@@ -153,6 +154,7 @@ public class ApplicationSerializer {
toSlime(application.outstandingChange(), root, outstandingChangeField);
application.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value()));
application.owner().ifPresent(owner -> root.setString(ownerField, owner.username()));
+ application.majorVersion().ifPresent(majorVersion -> root.setLong(majorVersionField, majorVersion));
root.setDouble(queryQualityField, application.metrics().queryServiceQuality());
root.setDouble(writeQualityField, application.metrics().writeServiceQuality());
application.rotation().ifPresent(rotation -> root.setString(rotationField, rotation.asString()));
@@ -314,13 +316,14 @@ public class ApplicationSerializer {
Change outstandingChange = changeFromSlime(root.field(outstandingChangeField));
Optional<IssueId> ownershipIssueId = optionalString(root.field(ownershipIssueIdField)).map(IssueId::from);
Optional<User> owner = optionalString(root.field(ownerField)).map(User::from);
+ Optional<Integer> majorVersion = optionalInteger(root.field(majorVersionField));
ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(),
root.field(writeQualityField).asDouble());
Optional<RotationId> rotation = rotationFromSlime(root.field(rotationField));
Map<HostName, RotationStatus> rotationStatus = rotationStatusFromSlime(root.field(rotationStatusField));
return new Application(id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, deploying,
- outstandingChange, ownershipIssueId, owner, metrics, rotation, rotationStatus);
+ outstandingChange, ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
private List<Deployment> deploymentsFromSlime(Inspector array) {
@@ -500,6 +503,10 @@ public class ApplicationSerializer {
return field.valid() ? OptionalLong.of(field.asLong()) : OptionalLong.empty();
}
+ private Optional<Integer> optionalInteger(Inspector field) {
+ return field.valid() ? Optional.of((int)field.asLong()) : Optional.empty();
+ }
+
private OptionalDouble optionalDouble(Inspector field) {
return field.valid() ? OptionalDouble.of(field.asDouble()) : OptionalDouble.empty();
}
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 56e1e746d04..fcf82a79850 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
@@ -137,6 +137,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
case GET: return handleGET(request);
case PUT: return handlePUT(request);
case POST: return handlePOST(request);
+ case PATCH: return handlePATCH(request);
case DELETE: return handleDELETE(request);
case OPTIONS: return handleOPTIONS();
default: return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
@@ -217,6 +218,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return ErrorResponse.notFoundError("Nothing at " + path);
}
+ private HttpResponse handlePATCH(HttpRequest request) {
+ Path path = new Path(request.getUri().getPath());
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}"))
+ return setMajorVersion(path.get("tenant"), path.get("application"), request);
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
private HttpResponse handleDELETE(HttpRequest request) {
Path path = new Path(request.getUri().getPath());
if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request);
@@ -235,7 +243,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
// We implement this to avoid redirect loops on OPTIONS requests from browsers, but do not really bother
// spelling out the methods supported at each path, which we should
EmptyJsonResponse response = new EmptyJsonResponse();
- response.headers().put("Allow", "GET,PUT,POST,DELETE,OPTIONS");
+ response.headers().put("Allow", "GET,PUT,POST,PATCH,DELETE,OPTIONS");
return response;
}
@@ -344,16 +352,28 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse application(String tenantName, String applicationName, HttpRequest request) {
- ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, "default");
- Application application =
- controller.applications().get(applicationId)
- .orElseThrow(() -> new NotExistsException(applicationId + " not found"));
-
Slime slime = new Slime();
- toSlime(slime.setObject(), application, request);
+ toSlime(slime.setObject(), getApplication(tenantName, applicationName, request), request);
return new SlimeJsonResponse(slime);
}
+ private HttpResponse setMajorVersion(String tenantName, String applicationName, HttpRequest request) {
+ Application application = getApplication(tenantName, applicationName, request);
+ Inspector majorVersionField = toSlime(request.getData()).get().field("majorVersion");
+ if ( ! majorVersionField.valid())
+ throw new IllegalArgumentException("Request body must contain a majorVersion field");
+ Integer majorVersion = majorVersionField.asLong() == 0 ? null : (int)majorVersionField.asLong();
+ controller.applications().lockIfPresent(application.id(),
+ a -> controller.applications().store(a.withMajorVersion(majorVersion)));
+ return new MessageResponse("Set major version to " + ( majorVersion == null ? "empty" : majorVersion));
+ }
+
+ private Application getApplication(String tenantName, String applicationName, HttpRequest request) {
+ ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, "default");
+ return controller.applications().get(applicationId)
+ .orElseThrow(() -> new NotExistsException(applicationId + " not found"));
+ }
+
private HttpResponse logs(String tenantName, String applicationName, String instanceName, String environment, String region, String query) {
ApplicationId application = ApplicationId.from(tenantName, applicationName, instanceName);
ZoneId zone = ZoneId.from(environment, region);
@@ -452,6 +472,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
// Compile version. The version that should be used when building an application
object.setString("compileVersion", controller.applications().oldestInstalledPlatform(application.id()).toFullString());
+ application.majorVersion().ifPresent(majorVersion -> object.setLong("majorVersion", majorVersion));
+
// Rotation
Cursor globalRotationsArray = object.setArray("globalRotations");
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 6755a6f9ad5..f93f9cceec7 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
@@ -6,6 +6,7 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
import com.yahoo.test.ManualClock;
import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.ApplicationController;
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;
@@ -895,7 +896,7 @@ public class UpgraderTest {
}
@Test
- public void testPinningMajorVersionInApplication() {
+ public void testPinningMajorVersionInDeploymentXml() {
Version version = Version.fromString("6.2");
tester.upgradeSystem(version);
@@ -929,6 +930,41 @@ public class UpgraderTest {
}
@Test
+ public void testPinningMajorVersionInApplication() {
+ Version version = Version.fromString("6.2");
+ tester.upgradeSystem(version);
+
+ ApplicationPackage default0ApplicationPackage = new ApplicationPackageBuilder()
+ .upgradePolicy("default")
+ .environment(Environment.prod)
+ .region("us-west-1")
+ .build();
+
+ // Setup applications
+ Application canary0 = tester.createAndDeploy("canary0", 1, "canary");
+ Application default0 = tester.createAndDeploy("default0", 2, default0ApplicationPackage);
+ tester.applications().lockOrThrow(default0.id(), a -> tester.applications().store(a.withMajorVersion(6)));
+ assertEquals(Optional.of(6), tester.applications().get(default0.id()).get().majorVersion());
+
+ // New major version is released
+ version = Version.fromString("7.0");
+ tester.upgradeSystem(version);
+ assertEquals(version, tester.controller().versionStatus().systemVersion().get().versionNumber());
+ tester.triggerUntilQuiescence();
+
+ // ... canary upgrade to it
+ assertEquals(2, tester.buildService().jobs().size());
+ tester.completeUpgrade(canary0, version, "canary");
+ assertEquals(0, tester.buildService().jobs().size());
+ tester.computeVersionStatus();
+
+ // The other application does not because it has pinned to major version 6
+ tester.upgrader().maintain();
+ tester.triggerUntilQuiescence();
+ assertEquals(0, tester.buildService().jobs().size());
+ }
+
+ @Test
public void testPinningMajorVersionInUpgrader() {
Version version = Version.fromString("6.2");
tester.upgradeSystem(version);
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 0b337eb5380..d4cd7c0fe85 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
@@ -109,6 +109,7 @@ public class ApplicationSerializerTest {
Change.of(ApplicationVersion.from(new SourceRevision("repo", "master", "deadcafe"), 42)),
Optional.of(IssueId.from("1234")),
Optional.of(User.from("by-username")),
+ Optional.of(7),
new MetricsService.ApplicationMetrics(0.5, 0.9),
Optional.of(new RotationId("my-rotation")),
rotationStatus);
@@ -142,6 +143,7 @@ public class ApplicationSerializerTest {
assertEquals(original.ownershipIssueId(), serialized.ownershipIssueId());
assertEquals(original.owner(), serialized.owner());
+ assertEquals(original.majorVersion(), serialized.majorVersion());
assertEquals(original.change(), serialized.change());
assertEquals(original.rotation().get(), serialized.rotation().get());
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 30795008032..95f58326076 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
@@ -82,6 +82,7 @@ import java.util.function.Supplier;
import static com.yahoo.application.container.handler.Request.Method.DELETE;
import static com.yahoo.application.container.handler.Request.Method.GET;
+import static com.yahoo.application.container.handler.Request.Method.PATCH;
import static com.yahoo.application.container.handler.Request.Method.POST;
import static com.yahoo.application.container.handler.Request.Method.PUT;
import static org.junit.Assert.assertEquals;
@@ -313,6 +314,24 @@ public class ApplicationApiTest extends ControllerContainerTest {
.userIdentity(USER_ID),
new File("application2.json"));
+ // PATCH in a major version override
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", PATCH)
+ .userIdentity(USER_ID)
+ .data("{\"majorVersion\":7}"),
+ "{\"message\":\"Set major version to 7\"}");
+ // GET an application with a major version override
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET)
+ .userIdentity(USER_ID),
+ new File("application2-with-majorVersion.json"));
+ // PATCH in removal of the application major version override removal
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", PATCH)
+ .userIdentity(USER_ID)
+ .data("{\"majorVersion\":null}"),
+ "{\"message\":\"Set major version to empty\"}");
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET)
+ .userIdentity(USER_ID),
+ new File("application2.json"));
+
// DELETE application
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", DELETE)
.userIdentity(USER_ID)
@@ -1280,55 +1299,6 @@ public class ApplicationApiTest extends ControllerContainerTest {
"}";
}
- private static class RequestBuilder implements Supplier<Request> {
-
- private final String path;
- private final Request.Method method;
- private byte[] data = new byte[0];
- private AthenzIdentity identity;
- private OktaAccessToken oktaAccessToken;
- private String contentType = "application/json";
- private String recursive;
-
- private RequestBuilder(String path, Request.Method method) {
- this.path = path;
- this.method = method;
- }
-
- private RequestBuilder data(byte[] data) { this.data = data; return this; }
- private RequestBuilder data(String data) { return data(data.getBytes(StandardCharsets.UTF_8)); }
- private RequestBuilder data(HttpEntity data) {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- try {
- data.writeTo(out);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- return data(out.toByteArray()).contentType(data.getContentType().getValue());
- }
- private RequestBuilder userIdentity(UserId userId) { this.identity = HostedAthenzIdentities.from(userId); return this; }
- private RequestBuilder screwdriverIdentity(ScrewdriverId screwdriverId) { this.identity = HostedAthenzIdentities.from(screwdriverId); return this; }
- private RequestBuilder oktaAccessToken(OktaAccessToken oktaAccessToken) { this.oktaAccessToken = oktaAccessToken; return this; }
- private RequestBuilder contentType(String contentType) { this.contentType = contentType; return this; }
- private RequestBuilder recursive(String recursive) { this.recursive = recursive; return this; }
-
- @Override
- public Request get() {
- Request request = new Request("http://localhost:8080" + path +
- // user and domain parameters are translated to a Principal by MockAuthorizer as we do not run HTTP filters
- (recursive == null ? "" : "?recursive=" + recursive),
- data, method);
- request.getHeaders().put("Content-Type", contentType);
- if (identity != null) {
- addIdentityToRequest(request, identity);
- }
- if (oktaAccessToken != null) {
- addOktaAccessToken(request, oktaAccessToken);
- }
- return request;
- }
- }
-
/** Make a request with (athens) user domain1.mytenant */
private RequestBuilder request(String path, Request.Method method) {
return new RequestBuilder(path, method);
@@ -1495,4 +1465,53 @@ public class ApplicationApiTest extends ControllerContainerTest {
Collections.singletonList("bob")), "queue", Optional.empty()));
}
+ private static class RequestBuilder implements Supplier<Request> {
+
+ private final String path;
+ private final Request.Method method;
+ private byte[] data = new byte[0];
+ private AthenzIdentity identity;
+ private OktaAccessToken oktaAccessToken;
+ private String contentType = "application/json";
+ private String recursive;
+
+ private RequestBuilder(String path, Request.Method method) {
+ this.path = path;
+ this.method = method;
+ }
+
+ private RequestBuilder data(byte[] data) { this.data = data; return this; }
+ private RequestBuilder data(String data) { return data(data.getBytes(StandardCharsets.UTF_8)); }
+ private RequestBuilder data(HttpEntity data) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ data.writeTo(out);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return data(out.toByteArray()).contentType(data.getContentType().getValue());
+ }
+ private RequestBuilder userIdentity(UserId userId) { this.identity = HostedAthenzIdentities.from(userId); return this; }
+ private RequestBuilder screwdriverIdentity(ScrewdriverId screwdriverId) { this.identity = HostedAthenzIdentities.from(screwdriverId); return this; }
+ private RequestBuilder oktaAccessToken(OktaAccessToken oktaAccessToken) { this.oktaAccessToken = oktaAccessToken; return this; }
+ private RequestBuilder contentType(String contentType) { this.contentType = contentType; return this; }
+ private RequestBuilder recursive(String recursive) { this.recursive = recursive; return this; }
+
+ @Override
+ public Request get() {
+ Request request = new Request("http://localhost:8080" + path +
+ // user and domain parameters are translated to a Principal by MockAuthorizer as we do not run HTTP filters
+ (recursive == null ? "" : "?recursive=" + recursive),
+ data, method);
+ request.getHeaders().put("Content-Type", contentType);
+ if (identity != null) {
+ addIdentityToRequest(request, identity);
+ }
+ if (oktaAccessToken != null) {
+ addOktaAccessToken(request, oktaAccessToken);
+ }
+ return request;
+ }
+ }
+
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-majorVersion.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-majorVersion.json
new file mode 100644
index 00000000000..55803074ade
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-majorVersion.json
@@ -0,0 +1,87 @@
+{
+ "application": "application2",
+ "instance": "default",
+ "deployments": "http://localhost:8080/application/v4/tenant/tenant2/application/application2/instance/default/job/",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ },
+ "projectId": 456,
+ "deploying": {
+ "version": "(ignore)"
+ },
+ "outstandingChange": {
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ }
+ },
+ "deployedInternally": false,
+ "deploymentJobs": [
+ {
+ "type": "component",
+ "success": true,
+ "lastCompleted": {
+ "id": 42,
+ "version": "(ignore)",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "Application commit",
+ "at": "(ignore)"
+ },
+ "lastSuccess": {
+ "id": 42,
+ "version": "(ignore)",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "Application commit",
+ "at": "(ignore)"
+ }
+ },
+ {
+ "type": "system-test",
+ "success": false,
+ "lastTriggered": {
+ "id": -1,
+ "version": "7.0.0",
+ "revision": {
+ "hash": "1.0.42-commit1",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "Testing last changes outside prod",
+ "at": "(ignore)"
+ }
+ }
+ ],
+ "changeBlockers": [],
+ "compileVersion": "6.1.0",
+ "majorVersion": 7,
+ "globalRotations": [],
+ "instances": [],
+ "metrics": {
+ "queryServiceQuality": 0.0,
+ "writeServiceQuality": 0.0
+ },
+ "activity": {}
+}