summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java1
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java172
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java105
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-in.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-initial.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-out.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-in.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-initial.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-out.json7
12 files changed, 348 insertions, 1 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
index f8f63df40a9..600a5cebc62 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
@@ -65,6 +65,7 @@ public interface ConfigServer {
* @param endpoint The endpoint to modify
* @param status The new status with metadata
*/
+ // TODO(mpolden): Implement a zone-variant of this
void setGlobalRotationStatus(DeploymentId deployment, String endpoint, EndpointStatus status);
/**
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
index bdc1c6e9794..998af030b6b 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
@@ -30,7 +30,8 @@ enum PathGroup {
"/orchestrator/v1/{*}",
"/os/v1/{*}",
"/provision/v2/{*}",
- "/zone/v2/{*}"),
+ "/zone/v2/{*}",
+ "/routing/v1/{*}"),
/** Paths used for creating and reading user resources. */
user(Optional.of("/api"),
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
new file mode 100644
index 00000000000..a62296b11e9
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
@@ -0,0 +1,172 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.routing;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.restapi.ErrorResponse;
+import com.yahoo.restapi.MessageResponse;
+import com.yahoo.restapi.Path;
+import com.yahoo.restapi.SlimeJsonResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
+import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
+import com.yahoo.vespa.hosted.controller.routing.GlobalRouting;
+import com.yahoo.vespa.hosted.controller.routing.RoutingPolicy;
+import com.yahoo.vespa.hosted.controller.routing.RoutingPolicyId;
+import com.yahoo.vespa.hosted.controller.routing.ZoneRoutingPolicy;
+import com.yahoo.yolean.Exceptions;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.logging.Level;
+
+/**
+ * This implements the /routing/v1 API, which provides operator with global routing control at both zone- and
+ * deployment-level.
+ *
+ * @author mpolden
+ */
+// TODO(mpolden): Add support for zones/deployments using rotations.
+public class RoutingApiHandler extends AuditLoggingRequestHandler {
+
+ private final Controller controller;
+
+ public RoutingApiHandler(Context ctx, Controller controller) {
+ super(ctx, controller.auditLogger());
+ this.controller = Objects.requireNonNull(controller, "controller must be non-null");
+ }
+
+ @Override
+ public HttpResponse auditAndHandle(HttpRequest request) {
+ try {
+ var path = new Path(request.getUri());
+ switch (request.getMethod()) {
+ case GET: return get(path);
+ case POST: return post(path);
+ case DELETE: return delete(path);
+ default: return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
+ }
+ } catch (IllegalArgumentException e) {
+ return ErrorResponse.badRequest(Exceptions.toMessageString(e));
+ } catch (RuntimeException e) {
+ log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
+ return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
+ }
+
+ private HttpResponse delete(Path path) {
+ if (path.matches("/routing/v1/inactive/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return setDeploymentStatus(path, true);
+ if (path.matches("/routing/v1/inactive/environment/{environment}/region/{region}")) return setZoneStatus(path, true);
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private HttpResponse post(Path path) {
+ if (path.matches("/routing/v1/inactive/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return setDeploymentStatus(path, false);
+ if (path.matches("/routing/v1/inactive/environment/{environment}/region/{region}")) return setZoneStatus(path, false);
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private HttpResponse get(Path path) {
+ if (path.matches("/routing/v1/status/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return deploymentStatus(path);
+ if (path.matches("/routing/v1/status/environment/{environment}/region/{region}")) return zoneStatus(path);
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private HttpResponse setZoneStatus(Path path, boolean in) {
+ var zone = zoneFrom(path);
+ var status = in ? GlobalRouting.Status.in : GlobalRouting.Status.out;
+ controller.applications().routingPolicies().setGlobalRoutingStatus(zone, status);
+ return new MessageResponse("Set global routing status for deployments in " + zone + " to '" +
+ (in ? "in" : "out") + "'");
+ }
+
+ private HttpResponse zoneStatus(Path path) {
+ var zone = zoneFrom(path);
+ var slime = new Slime();
+ var root = slime.setObject();
+ var zonePolicy = controller.applications().routingPolicies().get(zone);
+ zoneStatusToSlime(root, zonePolicy);
+ return new SlimeJsonResponse(slime);
+ }
+
+ private HttpResponse setDeploymentStatus(Path path, boolean in) {
+ var deployment = deploymentFrom(path);
+ routingPoliciesOf(deployment);
+ var status = in ? GlobalRouting.Status.in : GlobalRouting.Status.out;
+ var agent = GlobalRouting.Agent.operator; // Always operator as this is an operator API
+ controller.applications().routingPolicies().setGlobalRoutingStatus(deployment, status, agent);
+ return new MessageResponse("Set global routing status for " + deployment + " to '" + (in ? "in" : "out") + "'");
+ }
+
+ private HttpResponse deploymentStatus(Path path) {
+ var deployment = deploymentFrom(path);
+ var slime = new Slime();
+ var deploymentsObject = slime.setObject().setArray("deployments");
+ var routingPolicies = routingPoliciesOf(deployment);
+ for (var policy : routingPolicies.values()) {
+ deploymentStatusToSlime(deploymentsObject.addObject(), policy);
+ }
+ return new SlimeJsonResponse(slime);
+ }
+
+ private Map<RoutingPolicyId, RoutingPolicy> routingPoliciesOf(DeploymentId deployment) {
+ var routingPolicies = controller.applications().routingPolicies().get(deployment);
+ if (routingPolicies.isEmpty()) {
+ throw new IllegalArgumentException("No such deployment: " + deployment);
+ }
+ return routingPolicies;
+ }
+
+ private static void zoneStatusToSlime(Cursor object, ZoneRoutingPolicy policy) {
+ object.setString("environment", policy.zone().environment().value());
+ object.setString("region", policy.zone().region().value());
+ object.setString("status", asString(policy.globalRouting().status()));
+ object.setString("agent", asString(policy.globalRouting().agent()));
+ object.setLong("changedAt", policy.globalRouting().changedAt().toEpochMilli());
+ }
+
+ private static void deploymentStatusToSlime(Cursor object, RoutingPolicy policy) {
+ object.setString("instance", policy.id().owner().serializedForm());
+ object.setString("cluster", policy.id().cluster().value());
+ object.setString("environment", policy.id().zone().environment().value());
+ object.setString("region", policy.id().zone().region().value());
+ object.setString("status", asString(policy.status().globalRouting().status()));
+ object.setString("agent", asString(policy.status().globalRouting().agent()));
+ object.setLong("changedAt", policy.status().globalRouting().changedAt().toEpochMilli());
+ }
+
+ private DeploymentId deploymentFrom(Path path) {
+ return new DeploymentId(ApplicationId.from(path.get("tenant"), path.get("application"), path.get("instance")),
+ zoneFrom(path));
+ }
+
+ private ZoneId zoneFrom(Path path) {
+ var zone = ZoneId.from(path.get("environment"), path.get("region"));
+ if (!controller.zoneRegistry().zones().all().ids().contains(zone)) {
+ throw new IllegalArgumentException("No such zone: " + zone);
+ }
+ return zone;
+ }
+
+ private static String asString(GlobalRouting.Status status) {
+ switch (status) {
+ case in: return "in";
+ case out: return "out";
+ default: return "unknown";
+ }
+ }
+
+ private static String asString(GlobalRouting.Agent status) {
+ switch (status) {
+ case operator: return "operator";
+ case system: return "system";
+ case tenant: return "tenant";
+ default: return "unknown";
+ }
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
index c05152e7795..88f746a9c51 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
@@ -68,6 +68,11 @@ public class RoutingPolicies {
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
+ /** Read routing policy for given zone */
+ public ZoneRoutingPolicy get(ZoneId zone) {
+ return db.readZoneRoutingPolicy(zone);
+ }
+
/**
* Refresh routing policies for application in given zone. This is idempotent and changes will only be performed if
* load balancers for given application have changed.
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
index f3c437c76a1..b7adc3064fa 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
@@ -111,6 +111,9 @@ public class ControllerContainerTest {
" <binding>http://*/user/v1/*</binding>\n" +
" <binding>http://*/api/user/v1/*</binding>\n" +
" </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.routing.RoutingApiHandler'>\n" +
+ " <binding>http://*/routing/v1/*</binding>\n" +
+ " </handler>\n" +
variablePartXml() +
"</container>";
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java
new file mode 100644
index 00000000000..4339382432b
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java
@@ -0,0 +1,105 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.routing;
+
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
+import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+
+/**
+ * @author mpolden
+ */
+public class RoutingApiTest extends ControllerContainerTest {
+
+ private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/";
+
+ private ContainerTester tester;
+ private DeploymentTester deploymentTester;
+
+ @Before
+ public void before() {
+ tester = new ContainerTester(container, responseFiles);
+ deploymentTester = new DeploymentTester(new ControllerTester(tester));
+ }
+
+ @Test
+ public void requests() {
+ var context = deploymentTester.newDeploymentContext();
+
+ // Deploy application
+ var westZone = ZoneId.from("prod", "us-west-1");
+ var eastZone = ZoneId.from("prod", "us-east-3");
+ var applicationPackage = new ApplicationPackageBuilder()
+ .region(westZone.region())
+ .region(eastZone.region())
+ .build();
+ context.submit(applicationPackage).deploy();
+ context.addRoutingPolicy(westZone, true);
+ context.addRoutingPolicy(eastZone, true);
+
+ // GET initial deployment status
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
+ "", Request.Method.GET),
+ new File("deployment-status-initial.json"));
+
+ // POST sets deployment out
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
+ "", Request.Method.POST),
+ "{\"message\":\"Set global routing status for tenant.application in prod.us-west-1 to 'out'\"}");
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
+ "", Request.Method.GET),
+ new File("deployment-status-out.json"));
+
+ // DELETE sets deployment in
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
+ "", Request.Method.DELETE),
+ "{\"message\":\"Set global routing status for tenant.application in prod.us-west-1 to 'in'\"}");
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
+ "", Request.Method.GET),
+ new File("deployment-status-in.json"));
+
+ // GET initial zone status
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1",
+ "", Request.Method.GET),
+ new File("zone-status-initial.json"));
+
+ // POST sets zone out
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/environment/prod/region/us-west-1",
+ "", Request.Method.POST),
+ "{\"message\":\"Set global routing status for deployments in prod.us-west-1 to 'out'\"}");
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1",
+ "", Request.Method.GET),
+ new File("zone-status-out.json"));
+
+ // DELETE sets zone in
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/environment/prod/region/us-west-1",
+ "", Request.Method.DELETE),
+ "{\"message\":\"Set global routing status for deployments in prod.us-west-1 to 'in'\"}");
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1",
+ "", Request.Method.GET),
+ new File("zone-status-in.json"));
+ }
+
+ @Test
+ public void invalid_requests() {
+ // GET non-existent application
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/t1/application/a1/instance/default/environment/prod/region/us-west-1",
+ "", Request.Method.GET),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No such deployment: t1.a1 in prod.us-west-1\"}",
+ 400);
+
+ // GET non-existent zone
+ tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-north-1",
+ "", Request.Method.GET),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No such zone: prod.us-north-1\"}",
+ 400);
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-in.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-in.json
new file mode 100644
index 00000000000..a364be4b62e
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-in.json
@@ -0,0 +1,13 @@
+{
+ "deployments": [
+ {
+ "instance": "tenant:application:default",
+ "cluster": "default",
+ "environment": "prod",
+ "region": "us-west-1",
+ "status": "in",
+ "agent": "operator",
+ "changedAt": "(ignore)"
+ }
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-initial.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-initial.json
new file mode 100644
index 00000000000..fba0206fdda
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-initial.json
@@ -0,0 +1,13 @@
+{
+ "deployments": [
+ {
+ "instance": "tenant:application:default",
+ "cluster": "default",
+ "environment": "prod",
+ "region": "us-west-1",
+ "status": "in",
+ "agent": "system",
+ "changedAt": "(ignore)"
+ }
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-out.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-out.json
new file mode 100644
index 00000000000..5e23e443559
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/deployment-status-out.json
@@ -0,0 +1,13 @@
+{
+ "deployments": [
+ {
+ "instance": "tenant:application:default",
+ "cluster": "default",
+ "environment": "prod",
+ "region": "us-west-1",
+ "status": "out",
+ "agent": "operator",
+ "changedAt": "(ignore)"
+ }
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-in.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-in.json
new file mode 100644
index 00000000000..b840589c288
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-in.json
@@ -0,0 +1,7 @@
+{
+ "environment": "prod",
+ "region": "us-west-1",
+ "status": "in",
+ "agent": "operator",
+ "changedAt": "(ignore)"
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-initial.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-initial.json
new file mode 100644
index 00000000000..cbf9b0c9d0a
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-initial.json
@@ -0,0 +1,7 @@
+{
+ "environment": "prod",
+ "region": "us-west-1",
+ "status": "in",
+ "agent": "system",
+ "changedAt": "(ignore)"
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-out.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-out.json
new file mode 100644
index 00000000000..86b98e75c6c
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/zone-status-out.json
@@ -0,0 +1,7 @@
+{
+ "environment": "prod",
+ "region": "us-west-1",
+ "status": "out",
+ "agent": "operator",
+ "changedAt": "(ignore)"
+}