aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2017-11-22 14:27:18 +0100
committerMartin Polden <mpolden@mpolden.no>2017-11-22 15:44:13 +0100
commitb52f35351da2e8eba99eedd9a0b2a30ce08c8eff (patch)
treee377d121f96559f64b715b84bc799af4d50084f7
parent8ad3cbb455ab08eb427b8ba439a737a25d2e7592 (diff)
Reimplement /zone/v2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java116
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java43
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java117
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json4
9 files changed, 325 insertions, 2 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
index 1a3a0658488..e8b68d0c55a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
@@ -14,6 +14,7 @@ import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
@@ -165,6 +166,12 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor {
return put;
case DELETE:
return new HttpDelete(uri);
+ case PATCH:
+ HttpPatch patch = new HttpPatch(uri);
+ if (data != null) {
+ patch.setEntity(new InputStreamEntity(data));
+ }
+ return patch;
default:
throw new ProxyException(ErrorResponse.methodNotAllowed("Will not proxy such calls."));
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java
index 9853320e482..e9db4f9b717 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java
@@ -76,8 +76,8 @@ public class Path {
StringBuilder rest = new StringBuilder();
for (int i = specElements.length; i < this.elements.length; i++)
rest.append(elements[i]).append("/");
- if ( ! pathString.endsWith("/"))
- rest.setLength(rest.length() -1);
+ if ( ! pathString.endsWith("/") && rest.length() > 0)
+ rest.setLength(rest.length() - 1);
this.rest = rest.toString();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java
new file mode 100644
index 00000000000..529b2b25785
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java
@@ -0,0 +1,116 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
+
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
+import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor;
+import com.yahoo.vespa.hosted.controller.proxy.ProxyException;
+import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
+import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse;
+import com.yahoo.vespa.hosted.controller.restapi.Path;
+import com.yahoo.vespa.hosted.controller.restapi.SlimeJsonResponse;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+
+/**
+ * REST API for proxying requests to config servers in a given zone (version 2).
+ *
+ * This API does something completely different from /zone/v1, but such is the world.
+ *
+ * @author mpolden
+ */
+@SuppressWarnings("unused")
+public class ZoneApiHandler extends LoggingRequestHandler {
+
+ private final ZoneRegistry zoneRegistry;
+ private final ConfigServerRestExecutor proxy;
+
+ public ZoneApiHandler(Executor executor, AccessLog accessLog, ZoneRegistry zoneRegistry,
+ ConfigServerRestExecutor proxy) {
+ super(executor, accessLog);
+ this.zoneRegistry = zoneRegistry;
+ this.proxy = proxy;
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ try {
+ switch (request.getMethod()) {
+ case GET:
+ return get(request);
+ case POST:
+ case PUT:
+ case DELETE:
+ case PATCH:
+ return proxy(request);
+ default:
+ return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
+ }
+ } 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 get(HttpRequest request) {
+ Path path = new Path(request.getUri().getPath());
+ if (path.matches("/zone/v2")) {
+ return root(request);
+ }
+ return proxy(request);
+ }
+
+ private HttpResponse proxy(HttpRequest request) {
+ Path path = new Path(request.getUri().getPath());
+ if (!path.matches("/zone/v2/{environment}/{region}/{*}")) {
+ return notFound(path);
+ }
+ Environment environment = Environment.from(path.get("environment"));
+ RegionName region = RegionName.from(path.get("region"));
+ Optional<Zone> zone = zoneRegistry.getZone(environment, region);
+ if (!zone.isPresent()) {
+ throw new IllegalArgumentException("No such zone: " + environment.value() + "." + region.value());
+ }
+ try {
+ return proxy.handle(new ProxyRequest(request, "/zone/v2/"));
+ } catch (ProxyException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private HttpResponse root(HttpRequest request) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ Cursor uris = root.setArray("uris");
+ zoneRegistry.zones().forEach(zone -> uris.addString(request.getUri()
+ .resolve("/zone/v2/")
+ .resolve(zone.environment().value() + "/")
+ .resolve(zone.region().value())
+ .toString()));
+ Cursor zones = root.setArray("zones");
+ zoneRegistry.zones().forEach(zone -> {
+ Cursor object = zones.addObject();
+ object.setString("environment", zone.environment().value());
+ object.setString("region", zone.region().value());
+ });
+ return new SlimeJsonResponse(slime);
+ }
+
+ private HttpResponse notFound(Path path) {
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java
new file mode 100644
index 00000000000..95dfed8b7b2
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author mpolden
+ */
+package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java
new file mode 100644
index 00000000000..cc915d4d9a1
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java
@@ -0,0 +1,43 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor;
+import com.yahoo.vespa.hosted.controller.proxy.ProxyException;
+import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
+import com.yahoo.vespa.hosted.controller.restapi.StringResponse;
+
+import java.io.InputStream;
+import java.util.Optional;
+import java.util.Scanner;
+
+/**
+ * @author mpolden
+ */
+public class ConfigServerProxyMock extends AbstractComponent implements ConfigServerRestExecutor {
+
+ private volatile ProxyRequest lastReceived = null;
+ private volatile String requestBody = null;
+
+ @Override
+ public HttpResponse handle(ProxyRequest proxyRequest) throws ProxyException {
+ lastReceived = proxyRequest;
+ // Copy request body as the input stream is drained once the request completes
+ requestBody = asString(proxyRequest.getData());
+ return new StringResponse("ok");
+ }
+
+ public Optional<ProxyRequest> lastReceived() {
+ return Optional.ofNullable(lastReceived);
+ }
+
+ public Optional<String> lastRequestBody() {
+ return Optional.ofNullable(requestBody);
+ }
+
+ private static String asString(InputStream inputStream) {
+ Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+ return scanner.hasNext() ? scanner.next() : "";
+ }
+}
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 19c4def819f..044c5d75d12 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
@@ -48,6 +48,7 @@ public class ControllerContainerTest {
" <component id='com.yahoo.vespa.hosted.controller.ConfigServerClientMock'/>" +
" <component id='com.yahoo.vespa.hosted.controller.ZoneRegistryMock'/>" +
" <component id='com.yahoo.vespa.hosted.controller.Controller'/>" +
+ " <component id='com.yahoo.vespa.hosted.controller.ConfigServerProxyMock'/>" +
" <component id='com.yahoo.vespa.hosted.controller.integration.MockMetricsService'/>" +
" <component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>" +
" <component id='com.yahoo.vespa.hosted.controller.maintenance.JobControl'/>" +
@@ -74,6 +75,10 @@ public class ControllerContainerTest {
" <binding>http://*/zone/v1</binding>" +
" <binding>http://*/zone/v1/*</binding>" +
" </handler>" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v2.ZoneApiHandler'>" +
+ " <binding>http://*/zone/v2</binding>" +
+ " <binding>http://*/zone/v2/*</binding>" +
+ " </handler>" +
"</jdisc>";
protected void assertResponse(Request request, int responseStatus, String responseMessage) throws IOException {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java
new file mode 100644
index 00000000000..63899d808f9
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java
@@ -0,0 +1,117 @@
+package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
+
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.application.container.handler.Request.Method;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.hosted.controller.ConfigServerProxyMock;
+import com.yahoo.vespa.hosted.controller.ZoneRegistryMock;
+import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester;
+import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * @author mpolden
+ */
+public class ZoneApiTest extends ControllerContainerTest {
+
+ private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/";
+ private static final List<Zone> zones = Arrays.asList(
+ new Zone(Environment.prod, RegionName.from("us-north-1")),
+ new Zone(Environment.dev, RegionName.from("us-north-2")),
+ new Zone(Environment.test, RegionName.from("us-north-3")),
+ new Zone(Environment.staging, RegionName.from("us-north-4"))
+ );
+
+ private ContainerControllerTester tester;
+ private ConfigServerProxyMock proxy;
+
+ @Before
+ public void before() {
+ ZoneRegistryMock zoneRegistry = (ZoneRegistryMock) container.components()
+ .getComponent(ZoneRegistryMock.class.getName());
+ zoneRegistry.setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2"))
+ .setZones(zones);
+ this.tester = new ContainerControllerTester(container, responseFiles);
+ this.proxy = (ConfigServerProxyMock) container.components().getComponent(ConfigServerProxyMock.class.getName());
+ }
+
+ @Test
+ public void test_requests() throws Exception {
+ // GET /zone/v2
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2"),
+ new File("root.json"));
+
+ // GET /zone/v2/prod/us-north-1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1"),
+ "ok");
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("GET", proxy.lastReceived().get().getMethod());
+
+ // GET /zone/v2/nodes/v2/node/?recursive=true
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/?recursive=true"),
+ "ok");
+
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/node/?recursive=true", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("GET", proxy.lastReceived().get().getMethod());
+
+ // POST /zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1",
+ new byte[0], Method.POST),
+ "ok");
+ assertEquals("dev", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-2", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/command/restart?hostname=node1", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("POST", proxy.lastReceived().get().getMethod());
+
+ // PUT /zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1",
+ new byte[0], Method.PUT), "ok");
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/state/dirty/node1", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("PUT", proxy.lastReceived().get().getMethod());
+
+ // DELETE /zone/v2/prod/us-north-1/nodes/v2/node/node1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1",
+ new byte[0], Method.DELETE), "ok");
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/node/node1", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("DELETE", proxy.lastReceived().get().getMethod());
+
+ // PATCH /zone/v2/prod/us-north-1/nodes/v2/node/node1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1",
+ Utf8.toBytes("{\"currentRestartGeneration\": 1}"),
+ Method.PATCH), "ok");
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/node/node1", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("PATCH", proxy.lastReceived().get().getMethod());
+ assertEquals("{\"currentRestartGeneration\": 1}", proxy.lastRequestBody().get());
+ }
+
+ @Test
+ public void test_invalid_requests() throws Exception {
+ // GET /zone/v2/prod/us-north-34/nodes/v2
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-42/nodes/v2",
+ new byte[0], Method.POST),
+ new File("unknown-zone.json"), 400);
+ assertFalse(proxy.lastReceived().isPresent());
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json
new file mode 100644
index 00000000000..ab168854267
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json
@@ -0,0 +1,26 @@
+{
+ "uris": [
+ "http://localhost:8080/zone/v2/prod/us-north-1",
+ "http://localhost:8080/zone/v2/dev/us-north-2",
+ "http://localhost:8080/zone/v2/test/us-north-3",
+ "http://localhost:8080/zone/v2/staging/us-north-4"
+ ],
+ "zones": [
+ {
+ "environment": "prod",
+ "region": "us-north-1"
+ },
+ {
+ "environment": "dev",
+ "region": "us-north-2"
+ },
+ {
+ "environment": "test",
+ "region": "us-north-3"
+ },
+ {
+ "environment": "staging",
+ "region": "us-north-4"
+ }
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json
new file mode 100644
index 00000000000..c7d6e4b8400
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json
@@ -0,0 +1,4 @@
+{
+ "error-code": "BAD_REQUEST",
+ "message": "No such zone: prod.us-north-42"
+}