diff options
5 files changed, 111 insertions, 33 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMavenRepository.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMavenRepository.java index 56efb2e9c85..ddc81bc8998 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMavenRepository.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMavenRepository.java @@ -6,6 +6,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.maven.ArtifactId; import com.yahoo.vespa.hosted.controller.api.integration.maven.Metadata; import com.yahoo.vespa.hosted.controller.api.integration.maven.MavenRepository; +import java.util.ArrayList; import java.util.List; /** @@ -17,11 +18,24 @@ public class MockMavenRepository implements MavenRepository { public static final ArtifactId id = new ArtifactId("ai.vespa", "search"); + private final List<Version> versions = new ArrayList<>(); + + public MockMavenRepository() { + versions.addAll(List.of(Version.fromString("6.0"), + Version.fromString("6.1"), + Version.fromString("6.2"))); + } + + public MockMavenRepository addVersion(String... versions) { + for (var version : versions) { + this.versions.add(Version.fromString(version)); + } + return this; + } + @Override public Metadata metadata() { - return new Metadata(id, List.of(Version.fromString("6.0"), - Version.fromString("6.1"), - Version.fromString("6.2"))); + return new Metadata(id, versions); } @Override 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 a4aac26b973..94ba8d66cae 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 @@ -86,10 +86,10 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalInt; import java.util.Set; import java.util.TreeMap; import java.util.function.Consumer; -import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -299,6 +299,48 @@ public class ApplicationController { } /** + * Returns a non-broken, released version at least as old as the oldest platform the given application is on. + * + * If no known version is applicable, the newest version at least as old as the oldest platform is selected, + * among all versions released for this system. If no such versions exists, throws an IllegalStateException. + * + * If a non-empty wantedMajor is given, return a version within that major if such version is exists in this system. + */ + public Version compileVersion(TenantAndApplicationId id, OptionalInt wantedMajor) { + Version oldestPlatform = oldestInstalledPlatform(id); + VersionStatus versionStatus = controller.readVersionStatus(); + Set<Version> versionsInSystem = versionStatus.versions().stream() + .map(VespaVersion::versionNumber) + .collect(Collectors.toSet()); + Version compileVersion = versionStatus.versions().stream() + .filter(version -> version.confidence().equalOrHigherThan(VespaVersion.Confidence.low)) + .filter(VespaVersion::isReleased) + .map(VespaVersion::versionNumber) + .filter(version -> !version.isAfter(oldestPlatform)) + .max(Comparator.naturalOrder()) + .orElseGet(() -> publishedVersionNotAfter(oldestPlatform, versionsInSystem)); + if (wantedMajor.isPresent() && compileVersion.getMajor() != wantedMajor.getAsInt()) { + // Choose the oldest version matching wanted major so that the returned version is stable in the transition + // to next major + Optional<Version> versionMatchingMajor = versionsInSystem.stream() + .filter(version -> version.getMajor() == wantedMajor.getAsInt()) + .min(Comparator.naturalOrder()); + return versionMatchingMajor.orElse(compileVersion); + } + return compileVersion; + } + + /** Returns a version that is published to a Maven repository not older than oldestPlatform and known by the system */ + private Version publishedVersionNotAfter(Version oldestPlatform, Set<Version> versionsInSystem) { + return controller.mavenRepository().metadata().versions().stream() + .filter(version -> !version.isAfter(oldestPlatform)) + .filter(versionsInSystem::contains) + .max(Comparator.naturalOrder()) + .orElseThrow(() -> new IllegalStateException("No available releases of " + + controller.mavenRepository().artifactId())); + } + + /** * Creates a new application for an existing tenant. * * @throws IllegalArgumentException if the application already exists 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 6490ade655a..c2146dd5329 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 @@ -146,6 +146,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.OptionalInt; import java.util.Scanner; import java.util.SortedSet; import java.util.StringJoiner; @@ -248,7 +249,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}/validate")) return validateSecretStore(path.get("tenant"), path.get("name"), request); if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), Optional.empty(), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/compile-version")) return compileVersion(path.get("tenant"), path.get("application")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/compile-version")) return compileVersion(path.get("tenant"), path.get("application"), request.getProperty("allowMajor")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deployment")) return JobControllerApiHandlerHelper.overviewResponse(controller, TenantAndApplicationId.from(path.get("tenant"), path.get("application")), request.getUri()); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/package")) return applicationPackage(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/diff/{number}")) return applicationPackageDiff(path.get("tenant"), path.get("application"), path.get("number")); @@ -852,10 +853,18 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { return new SlimeJsonResponse(slime); } - private HttpResponse compileVersion(String tenantName, String applicationName) { + private HttpResponse compileVersion(String tenantName, String applicationName, String allowMajorParam) { Slime slime = new Slime(); - slime.setObject().setString("compileVersion", - compileVersion(TenantAndApplicationId.from(tenantName, applicationName)).toFullString()); + OptionalInt allowMajor = OptionalInt.empty(); + if (allowMajorParam != null) { + try { + allowMajor = OptionalInt.of(Integer.parseInt(allowMajorParam)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid major version '" + allowMajorParam + "'", e); + } + } + Version compileVersion = controller.applications().compileVersion(TenantAndApplicationId.from(tenantName, applicationName), allowMajor); + slime.setObject().setString("compileVersion", compileVersion.toFullString()); return new SlimeJsonResponse(slime); } @@ -1698,31 +1707,6 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { return controller.zoneRegistry().getMonitoringSystemUri(deploymentId); } - /** - * Returns a non-broken, released version at least as old as the oldest platform the given application is on. - * - * If no known version is applicable, the newest version at least as old as the oldest platform is selected, - * among all versions released for this system. If no such versions exists, throws an IllegalStateException. - */ - private Version compileVersion(TenantAndApplicationId id) { - Version oldestPlatform = controller.applications().oldestInstalledPlatform(id); - VersionStatus versionStatus = controller.readVersionStatus(); - return versionStatus.versions().stream() - .filter(version -> version.confidence().equalOrHigherThan(VespaVersion.Confidence.low)) - .filter(VespaVersion::isReleased) - .map(VespaVersion::versionNumber) - .filter(version -> ! version.isAfter(oldestPlatform)) - .max(Comparator.naturalOrder()) - .orElseGet(() -> controller.mavenRepository().metadata().versions().stream() - .filter(version -> ! version.isAfter(oldestPlatform)) - .filter(version -> ! versionStatus.versions().stream() - .map(VespaVersion::versionNumber) - .collect(Collectors.toSet()).contains(version)) - .max(Comparator.naturalOrder()) - .orElseThrow(() -> new IllegalStateException("No available releases of " + - controller.mavenRepository().artifactId()))); - } - private HttpResponse setGlobalRotationOverride(String tenantName, String applicationName, String instanceName, String environment, String region, boolean inService, HttpRequest request) { Instance instance = controller.applications().requireInstance(ApplicationId.from(tenantName, applicationName, instanceName)); ZoneId zone = requireZone(environment, region); 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 63c0193ba7f..f1b7311ed24 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 @@ -27,9 +27,11 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedAliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMavenRepository; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.Endpoint; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext; @@ -1050,4 +1052,34 @@ public class ControllerTest { } } + @Test + public void testCompileVersion() { + DeploymentContext context = tester.newDeploymentContext(); + ApplicationPackage applicationPackage = new ApplicationPackageBuilder().region("us-west-1").build(); + TenantAndApplicationId application = TenantAndApplicationId.from(context.instanceId()); + ((MockMavenRepository) tester.controller().mavenRepository()).addVersion("7.1", "7.2", "8.0"); + + // No deployments result in system version + Version version0 = Version.fromString("7.1"); + tester.controllerTester().upgradeSystem(version0); + assertEquals(version0, tester.applications().compileVersion(application, OptionalInt.empty())); + context.submit(applicationPackage).deploy(); + + // System is upgraded + Version version1 = Version.fromString("7.2"); + tester.controllerTester().upgradeSystem(version1); + assertEquals(version0, tester.applications().compileVersion(application, OptionalInt.empty())); + + // Application is upgraded and compile version is bumped + tester.upgrader().maintain(); + context.deployPlatform(version1); + assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.empty())); + + // A new major is released to the system + Version version2 = Version.fromString("8.0"); + tester.controllerTester().upgradeController(version2); + assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.empty())); + assertEquals(version2, tester.applications().compileVersion(application, OptionalInt.of(8))); + } + } 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 d9192598452..74bab37773a 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 @@ -859,6 +859,12 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(USER_ID), "{\"message\":\"Aborting run 2 of staging-test for tenant1.application1.instance1\"}"); + // GET compile version for specific major + deploymentTester.controllerTester().upgradeSystem(Version.fromString("7.0")); + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/compile-version", GET) + .userIdentity(USER_ID).properties(Map.of("allowMajor", "7")), + "{\"compileVersion\":\"7.0.0\"}"); + // OPTIONS return 200 OK tester.assertResponse(request("/application/v4/", Request.Method.OPTIONS) .userIdentity(USER_ID), |