diff options
5 files changed, 150 insertions, 23 deletions
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 7db75d73ea4..3392576643f 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 @@ -60,7 +60,6 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.JobStatus; -import com.yahoo.vespa.hosted.controller.rotation.RotationState; import com.yahoo.vespa.hosted.controller.application.RoutingPolicy; import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger; @@ -70,6 +69,9 @@ import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse; import com.yahoo.vespa.hosted.controller.restapi.MessageResponse; import com.yahoo.vespa.hosted.controller.restapi.ResourceResponse; import com.yahoo.vespa.hosted.controller.restapi.SlimeJsonResponse; +import com.yahoo.vespa.hosted.controller.rotation.RotationId; +import com.yahoo.vespa.hosted.controller.rotation.RotationState; +import com.yahoo.vespa.hosted.controller.rotation.RotationStatus; import com.yahoo.vespa.hosted.controller.security.AccessControlRequests; import com.yahoo.vespa.hosted.controller.security.Credentials; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; @@ -195,7 +197,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/service/{service}/{*}")) return service(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.getRest(), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/nodes")) return nodes(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/logs")) return logs(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap()); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation")) return rotationStatus(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation")) return rotationStatus(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), Optional.ofNullable(request.getProperty("endpointId"))); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return getGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deployment(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deployment(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); @@ -204,7 +206,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/service/{service}/{*}")) return service(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.getRest(), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/nodes")) return nodes(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/logs")) return logs(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap()); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation")) return rotationStatus(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation")) return rotationStatus(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), Optional.ofNullable(request.getProperty("endpointId"))); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override")) return getGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); return ErrorResponse.notFoundError("Nothing at " + path); } @@ -550,9 +552,19 @@ public class ApplicationApiHandler extends LoggingRequestHandler { for (Deployment deployment : deployments) { Cursor deploymentObject = instancesArray.addObject(); - if (!application.rotations().isEmpty() && deployment.zone().environment() == Environment.prod) { - // TODO(mpolden): Support retrieving status for multiple rotations - toSlime(application.rotationStatus().of(application.rotations().get(0).rotationId(), deployment), deploymentObject); + // Rotation status for this deployment + if (deployment.zone().environment() == Environment.prod) { + // 0 rotations: No fields written + // 1 rotation : Write legacy field and endpointStatus field + // >1 rotation : Write only endpointStatus field + if (application.rotations().size() == 1) { + // TODO(mpolden): Stop writing this field once clients stop expecting it + toSlime(application.rotationStatus().of(application.rotations().get(0).rotationId(), deployment), + deploymentObject); + } + if (!application.rotations().isEmpty()) { + toSlime(application.rotations(), application.rotationStatus(), deployment, deploymentObject); + } } if (recurseOverDeployments(request)) // List full deployment information when recursive. @@ -688,7 +700,16 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private void toSlime(RotationState state, Cursor object) { Cursor bcpStatus = object.setObject("bcpStatus"); - bcpStatus.setString("rotationStatus", state.name().toUpperCase()); + bcpStatus.setString("rotationStatus", rotationStateString(state)); + } + + private void toSlime(List<AssignedRotation> rotations, RotationStatus status, Deployment deployment, Cursor object) { + var array = object.setArray("endpointStatus"); + for (var rotation : rotations) { + var statusObject = array.addObject(); + statusObject.setString("endpointId", rotation.endpointId().id()); + statusObject.setString("status", rotationStateString(status.of(rotation.rotationId(), deployment))); + } } private URI monitoringSystemUri(DeploymentId deploymentId) { @@ -763,13 +784,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(slime); } - private HttpResponse rotationStatus(String tenantName, String applicationName, String instanceName, String environment, String region) { + private HttpResponse rotationStatus(String tenantName, String applicationName, String instanceName, String environment, String region, Optional<String> endpointId) { ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, instanceName); Application application = controller.applications().require(applicationId); ZoneId zone = ZoneId.from(environment, region); - if (application.rotations().isEmpty()) { - throw new NotExistsException("global rotation does not exist for " + application); - } + RotationId rotation = findRotationId(application, endpointId); Deployment deployment = application.deployments().get(zone); if (deployment == null) { throw new NotExistsException(application + " has no deployment in " + zone); @@ -777,8 +796,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Slime slime = new Slime(); Cursor response = slime.setObject(); - // TODO(mpolden): Support retrieving status for multiple rotations - toSlime(application.rotationStatus().of(application.rotations().get(0).rotationId(), deployment), response); + toSlime(application.rotationStatus().of(rotation, deployment), response); return new SlimeJsonResponse(slime); } @@ -1541,4 +1559,29 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return dataParts; } + private static RotationId findRotationId(Application application, Optional<String> endpointId) { + if (application.rotations().isEmpty()) { + throw new NotExistsException("global rotation does not exist for " + application); + } + if (endpointId.isPresent()) { + return application.rotations().stream() + .filter(r -> r.endpointId().id().equals(endpointId.get())) + .map(AssignedRotation::rotationId) + .findFirst() + .orElseThrow(() -> new NotExistsException("endpoint " + endpointId.get() + + " does not exist for " + application)); + } else if (application.rotations().size() > 1) { + throw new IllegalArgumentException(application + " has multiple rotations. Query parameter 'endpointId' must be given"); + } + return application.rotations().get(0).rotationId(); + } + + private static String rotationStateString(RotationState state) { + switch (state) { + case in: return "IN"; + case out: return "OUT"; + } + return "UNKNOWN"; + } + } 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 4fdce088853..b7755d01f75 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 @@ -11,6 +11,7 @@ import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.slime.Cursor; @@ -20,8 +21,9 @@ import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.athenz.api.OktaAccessToken; import com.yahoo.vespa.config.SlimeUtils; +import com.yahoo.vespa.flags.Flags; +import com.yahoo.vespa.flags.InMemoryFlagSource; import com.yahoo.vespa.hosted.controller.Application; -import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.LockedTenant; import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; @@ -59,6 +61,8 @@ import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.deployment.BuildJob; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger; import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock; +import com.yahoo.vespa.hosted.controller.maintenance.JobControl; +import com.yahoo.vespa.hosted.controller.maintenance.RotationStatusUpdater; import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; @@ -75,6 +79,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Base64; @@ -378,6 +383,8 @@ public class ApplicationApiTest extends ControllerContainerTest { controllerTester.upgrader().overrideConfidence(Version.fromString("6.1"), VespaVersion.Confidence.broken); tester.computeVersionStatus(); setDeploymentMaintainedInfo(controllerTester); + setZoneInRotation("rotation-fqdn-1", ZoneId.from("prod", "us-central-1")); + // GET tenant application deployments tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", GET) .userIdentity(USER_ID), @@ -754,6 +761,64 @@ public class ApplicationApiTest extends ControllerContainerTest { } @Test + public void multiple_endpoints() { + // Setup + ((InMemoryFlagSource) tester.controller().flagSource()).withBooleanFlag(Flags.MULTIPLE_GLOBAL_ENDPOINTS.id(), true); + tester.computeVersionStatus(); + createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); + ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .region("us-west-1") + .region("us-east-3") + .region("eu-west-1") + .endpoint("eu", "default", "eu-west-1") + .endpoint("default", "default", "us-west-1", "us-east-3") + .build(); + + // Create tenant and deploy + ApplicationId id = createTenantAndApplication(); + long projectId = 1; + MultiPartStreamer deployData = createApplicationDeployData(Optional.empty(), false); + startAndTestChange(controllerTester, id, projectId, applicationPackage, deployData, 100); + for (var job : List.of(JobType.productionUsWest1, JobType.productionUsEast3, JobType.productionEuWest1)) { + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/" + job.zone(SystemName.main).region().value() + "/deploy", POST) + .data(deployData) + .screwdriverIdentity(SCREWDRIVER_ID), + new File("deploy-result.json")); + controllerTester.jobCompletion(job) + .application(id) + .projectId(projectId) + .submit(); + } + setZoneInRotation("rotation-fqdn-2", ZoneId.from("prod", "us-west-1")); + setZoneInRotation("rotation-fqdn-2", ZoneId.from("prod", "us-east-3")); + setZoneInRotation("rotation-fqdn-1", ZoneId.from("prod", "eu-west-1")); + + // GET global rotation status without specifying endpointId fails + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation", GET) + .userIdentity(USER_ID), + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"application 'tenant1.application1.instance1' has multiple rotations. Query parameter 'endpointId' must be given\"}", + 400); + + // GET global rotation status for us-west-1 in default endpoint + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation?endpointId=default", GET) + .userIdentity(USER_ID), + "{\"bcpStatus\":{\"rotationStatus\":\"IN\"}}", + 200); + + // GET global rotation status for us-west-1 in eu endpoint + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation?endpointId=eu", GET) + .userIdentity(USER_ID), + "{\"bcpStatus\":{\"rotationStatus\":\"UNKNOWN\"}}", + 200); + + // GET global rotation status for eu-west-1 in eu endpoint + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/eu-west-1/global-rotation?endpointId=eu", GET) + .userIdentity(USER_ID), + "{\"bcpStatus\":{\"rotationStatus\":\"IN\"}}", + 200); + } + + @Test public void testDeployDirectly() { // Setup tester.computeVersionStatus(); @@ -865,7 +930,7 @@ public class ApplicationApiTest extends ControllerContainerTest { } @Test - public void testMeteringResponses() { + public void testMeteringResponses() { MockMeteringClient mockMeteringClient = (MockMeteringClient) controllerTester.controller().meteringClient(); // Mock response for MeteringClient @@ -1671,12 +1736,7 @@ public class ApplicationApiTest extends ControllerContainerTest { private void setZoneInRotation(String rotationName, ZoneId zone) { globalRoutingService().setStatus(rotationName, zone, com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus.IN); - ApplicationController applicationController = controllerTester.controller().applications(); - List<Application> applicationList = applicationController.asList(); - applicationList.forEach(application -> { - applicationController.lockIfPresent(application.id(), locked -> - applicationController.store(locked.with(rotationStatus(application)))); - }); + new RotationStatusUpdater(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator())).run(); } private RotationStatus rotationStatus(Application application) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json index 383a1b667f7..1f79e960782 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json @@ -239,6 +239,12 @@ "bcpStatus": { "rotationStatus": "IN" }, + "endpointStatus": [ + { + "endpointId": "default", + "status": "IN" + } + ], "environment": "prod", "region": "us-west-1", "instance": "instance1", @@ -248,6 +254,12 @@ "bcpStatus": { "rotationStatus": "UNKNOWN" }, + "endpointStatus": [ + { + "endpointId": "default", + "status": "UNKNOWN" + } + ], "environment": "prod", "region": "us-east-3", "instance": "instance1", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json index 1d719133ac3..65ea213ebbc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json @@ -231,8 +231,14 @@ }, { "bcpStatus": { - "rotationStatus": "UNKNOWN" + "rotationStatus": "IN" }, + "endpointStatus": [ + { + "endpointId": "default", + "status": "IN" + } + ], "environment": "prod", "region": "us-central-1", "instance": "instance1", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json index 4810c8f92b2..bb68904bee6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json @@ -1,7 +1,13 @@ { "bcpStatus": { - "rotationStatus": "UNKNOWN" + "rotationStatus": "IN" }, + "endpointStatus": [ + { + "endpointId": "default", + "status": "IN" + } + ], "tenant": "tenant1", "application": "application1", "instance": "instance1", |