summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java69
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java76
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json8
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",