aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2019-11-01 16:55:02 +0100
committerGitHub <noreply@github.com>2019-11-01 16:55:02 +0100
commit69719912deb821dbf8c6eb1be3e23a3f05ee2a99 (patch)
tree3909bfe11d703a53c9d8ae8ac09a0ed65b0e3185 /controller-server/src/main/java/com
parent197628b906de4fec5e341fe57041259823e3d05d (diff)
parent70731418933393c915d64df49c19d43aa9fd25ee (diff)
Merge pull request #11183 from vespa-engine/freva/configserver-v1
Create /configserver/v1
Diffstat (limited to 'controller-server/src/main/java/com')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java93
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java127
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java13
5 files changed, 218 insertions, 57 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 e3c048e865a..4fa7a40d38a 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
@@ -2,9 +2,10 @@
package com.yahoo.vespa.hosted.controller.proxy;
import com.google.inject.Inject;
-import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.component.AbstractComponent;
import com.yahoo.jdisc.http.HttpRequest.Method;
import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
@@ -17,14 +18,17 @@ 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;
+import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UncheckedIOException;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
@@ -33,7 +37,9 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
import static com.yahoo.yolean.Exceptions.uncheck;
@@ -43,33 +49,37 @@ import static com.yahoo.yolean.Exceptions.uncheck;
* @author bjorncs
*/
@SuppressWarnings("unused") // Injected
-public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor {
+public class ConfigServerRestExecutorImpl extends AbstractComponent implements ConfigServerRestExecutor {
private static final Logger log = Logger.getLogger(ConfigServerRestExecutorImpl.class.getName());
private static final Duration PROXY_REQUEST_TIMEOUT = Duration.ofSeconds(10);
private static final Set<String> HEADERS_TO_COPY = Set.of("X-HTTP-Method-Override", "Content-Type");
- private final ZoneRegistry zoneRegistry;
- private final ServiceIdentityProvider sslContextProvider;
+ private final CloseableHttpClient client;
@Inject
public ConfigServerRestExecutorImpl(ZoneRegistry zoneRegistry, ServiceIdentityProvider sslContextProvider) {
- this.zoneRegistry = zoneRegistry;
- this.sslContextProvider = sslContextProvider;
+ RequestConfig config = RequestConfig.custom()
+ .setConnectTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis())
+ .setConnectionRequestTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis())
+ .setSocketTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()).build();
+
+ this.client = createHttpClient(config, sslContextProvider,
+ new ControllerOrConfigserverHostnameVerifier(zoneRegistry));
}
@Override
public ProxyResponse handle(ProxyRequest proxyRequest) throws ProxyException {
- HostnameVerifier hostnameVerifier = createHostnameVerifier(proxyRequest.getZoneId());
- List<URI> allServers = getConfigserverEndpoints(proxyRequest.getZoneId());
+ // Make a local copy of the list as we want to manipulate it in case of ping problems.
+ List<URI> allServers = new ArrayList<>(proxyRequest.getTargets());
StringBuilder errorBuilder = new StringBuilder();
- if (queueFirstServerIfDown(allServers, hostnameVerifier)) {
+ if (queueFirstServerIfDown(allServers)) {
errorBuilder.append("Change ordering due to failed ping.");
}
for (URI uri : allServers) {
- Optional<ProxyResponse> proxyResponse = proxyCall(uri, proxyRequest, hostnameVerifier, errorBuilder);
+ Optional<ProxyResponse> proxyResponse = proxyCall(uri, proxyRequest, errorBuilder);
if (proxyResponse.isPresent()) {
return proxyResponse.get();
}
@@ -79,32 +89,14 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor {
+ errorBuilder.toString()));
}
- private List<URI> getConfigserverEndpoints(ZoneId zoneId) {
- // TODO: Use config server VIP for all zones that have one
- // Make a local copy of the list as we want to manipulate it in case of ping problems.
- if (zoneId.region().value().startsWith("aws-") || zoneId.region().value().contains("-aws-")) {
- return List.of(zoneRegistry.getConfigServerVipUri(zoneId));
- } else {
- return new ArrayList<>(zoneRegistry.getConfigServerUris(zoneId));
- }
- }
-
- private Optional<ProxyResponse> proxyCall(
- URI uri, ProxyRequest proxyRequest, HostnameVerifier hostnameVerifier, StringBuilder errorBuilder)
+ private Optional<ProxyResponse> proxyCall(URI uri, ProxyRequest proxyRequest, StringBuilder errorBuilder)
throws ProxyException {
final HttpRequestBase requestBase = createHttpBaseRequest(
proxyRequest.getMethod(), proxyRequest.createConfigServerRequestUri(uri), proxyRequest.getData());
// Empty list of headers to copy for now, add headers when needed, or rewrite logic.
copyHeaders(proxyRequest.getHeaders(), requestBase);
- RequestConfig config = RequestConfig.custom()
- .setConnectTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis())
- .setConnectionRequestTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis())
- .setSocketTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()).build();
- try (
- CloseableHttpClient client = createHttpClient(config, sslContextProvider, hostnameVerifier);
- CloseableHttpResponse response = client.execute(requestBase)
- ) {
+ try (CloseableHttpResponse response = client.execute(requestBase)) {
String content = getContent(response);
int status = response.getStatusLine().getStatusCode();
if (status / 100 == 5) {
@@ -182,7 +174,7 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor {
* if it is not responding, we try the other servers first. False positive/negatives are not critical,
* but will increase latency to some extent.
*/
- private boolean queueFirstServerIfDown(List<URI> allServers, HostnameVerifier hostnameVerifier) {
+ private boolean queueFirstServerIfDown(List<URI> allServers) {
if (allServers.size() < 2) {
return false;
}
@@ -194,10 +186,8 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor {
.setConnectTimeout(timeout)
.setConnectionRequestTimeout(timeout)
.setSocketTimeout(timeout).build();
- try (
- CloseableHttpClient client = createHttpClient(config, sslContextProvider, hostnameVerifier);
- CloseableHttpResponse response = client.execute(httpget)
- ) {
+ httpget.setConfig(config);
+ try (CloseableHttpResponse response = client.execute(httpget)) {
if (response.getStatusLine().getStatusCode() == 200) {
return false;
}
@@ -210,8 +200,13 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor {
return true;
}
- private HostnameVerifier createHostnameVerifier(ZoneId zoneId) {
- return new AthenzIdentityVerifier(Set.of(zoneRegistry.getConfigServerHttpsIdentity(zoneId)));
+ @Override
+ public void deconstruct() {
+ try {
+ client.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
}
private static CloseableHttpClient createHttpClient(RequestConfig config,
@@ -222,7 +217,31 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor {
.setSslcontext(sslContextProvider.getIdentitySslContext())
.setSSLHostnameVerifier(hostnameVerifier)
.setDefaultRequestConfig(config)
+ .setMaxConnPerRoute(10)
+ .setMaxConnTotal(500)
+ .setConnectionTimeToLive(1, TimeUnit.MINUTES)
.build();
}
+ private static class ControllerOrConfigserverHostnameVerifier implements HostnameVerifier {
+
+ private final HostnameVerifier controllerVerifier = new DefaultHostnameVerifier();
+ private final HostnameVerifier configserverVerifier;
+
+ ControllerOrConfigserverHostnameVerifier(ZoneRegistry registry) {
+ this.configserverVerifier = createConfigserverVerifier(registry);
+ }
+
+ private static HostnameVerifier createConfigserverVerifier(ZoneRegistry registry) {
+ Set<AthenzIdentity> configserverIdentities = registry.zones().all().zones().stream()
+ .map(zone -> registry.getConfigServerHttpsIdentity(zone.getId()))
+ .collect(Collectors.toSet());
+ return new AthenzIdentityVerifier(configserverIdentities);
+ }
+
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return controllerVerifier.verify(hostname, session) || configserverVerifier.verify(hostname, session);
+ }
+ }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java
index 100292a0bdc..f398683567b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java
@@ -1,7 +1,6 @@
// 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.proxy;
-import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.container.jdisc.HttpRequest;
import java.io.InputStream;
@@ -26,36 +25,36 @@ public class ProxyRequest {
private final Map<String, List<String>> headers;
private final InputStream requestData;
- private final ZoneId zoneId;
- private final String proxyPath;
+ private final List<URI> targets;
+ private final String targetPath;
/**
* The constructor calls exception if the request is invalid.
*
* @param request the request from the jdisc framework.
- * @param zoneId the zone to proxy to.
- * @param proxyPath the path to proxy to.
+ * @param targets list of targets this request should be proxied to (targets are tried once in order until a response is returned).
+ * @param targetPath the path to proxy to.
* @throws ProxyException on errors
*/
- public ProxyRequest(HttpRequest request, ZoneId zoneId, String proxyPath) throws ProxyException {
+ public ProxyRequest(HttpRequest request, List<URI> targets, String targetPath) throws ProxyException {
this(request.getMethod(), request.getUri(), request.getJDiscRequest().headers(), request.getData(),
- zoneId, proxyPath);
+ targets, targetPath);
}
ProxyRequest(Method method, URI requestUri, Map<String, List<String>> headers, InputStream body,
- ZoneId zoneId, String proxyPath) throws ProxyException {
+ List<URI> targets, String targetPath) throws ProxyException {
Objects.requireNonNull(requestUri, "Request must be non-null");
- if (!requestUri.getPath().endsWith(proxyPath))
+ if (!requestUri.getPath().endsWith(targetPath))
throw new ProxyException(ErrorResponse.badRequest(String.format(
- "Request path '%s' does not end with proxy path '%s'", requestUri.getPath(), proxyPath)));
+ "Request path '%s' does not end with proxy path '%s'", requestUri.getPath(), targetPath)));
this.method = Objects.requireNonNull(method);
this.requestUri = Objects.requireNonNull(requestUri);
this.headers = Objects.requireNonNull(headers);
this.requestData = body;
- this.zoneId = Objects.requireNonNull(zoneId);
- this.proxyPath = proxyPath.startsWith("/") ? proxyPath : "/" + proxyPath;
+ this.targets = List.copyOf(targets);
+ this.targetPath = targetPath.startsWith("/") ? targetPath : "/" + targetPath;
}
@@ -71,23 +70,23 @@ public class ProxyRequest {
return requestData;
}
- public ZoneId getZoneId() {
- return zoneId;
+ public List<URI> getTargets() {
+ return targets;
}
public URI createConfigServerRequestUri(URI baseURI) {
try {
return new URI(baseURI.getScheme(), baseURI.getUserInfo(), baseURI.getHost(),
- baseURI.getPort(), proxyPath, requestUri.getQuery(), requestUri.getFragment());
+ baseURI.getPort(), targetPath, requestUri.getQuery(), requestUri.getFragment());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
public URI getControllerPrefixUri() {
- String prefixPath = proxyPath.equals("/") && !requestUri.getPath().endsWith("/") ?
- requestUri.getPath() + proxyPath :
- requestUri.getPath().substring(0, requestUri.getPath().length() - proxyPath.length() + 1);
+ String prefixPath = targetPath.equals("/") && !requestUri.getPath().endsWith("/") ?
+ requestUri.getPath() + targetPath :
+ requestUri.getPath().substring(0, requestUri.getPath().length() - targetPath.length() + 1);
try {
return new URI(requestUri.getScheme(), requestUri.getUserInfo(), requestUri.getHost(),
requestUri.getPort(), prefixPath, null, null);
@@ -98,7 +97,7 @@ public class ProxyRequest {
@Override
public String toString() {
- return "[zone: " + zoneId + " request: " + proxyPath + "]";
+ return "[targets: " + targets + " request: " + targetPath + "]";
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
new file mode 100644
index 00000000000..99cc78a2614
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
@@ -0,0 +1,127 @@
+// Copyright 2019 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.configserver;
+
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.config.provision.zone.ZoneList;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.restapi.ErrorResponse;
+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.integration.zone.ZoneRegistry;
+import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
+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.yolean.Exceptions;
+
+import java.net.URI;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.stream.Stream;
+
+/**
+ * REST API for proxying operator APIs to config servers in a given zone.
+ *
+ * @author freva
+ */
+@SuppressWarnings("unused")
+public class ConfigServerApiHandler extends AuditLoggingRequestHandler {
+
+ private static final String OPTIONAL_PREFIX = "/api";
+ private static final ZoneId CONTROLLER_ZONE = ZoneId.from("prod", "controller");
+ private static final List<String> WHITELISTED_APIS = List.of("/flags/v1/", "/nodes/v2/", "/orchestrator/v1/");
+
+ private final ZoneRegistry zoneRegistry;
+ private final ConfigServerRestExecutor proxy;
+
+ public ConfigServerApiHandler(Context parentCtx, ZoneRegistry zoneRegistry,
+ ConfigServerRestExecutor proxy, Controller controller) {
+ super(parentCtx, controller.auditLogger());
+ this.zoneRegistry = zoneRegistry;
+ this.proxy = proxy;
+ }
+
+ @Override
+ public HttpResponse auditAndHandle(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() + "', "
+ + Exceptions.toMessageString(e));
+ return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
+ }
+
+ private HttpResponse get(HttpRequest request) {
+ Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ if (path.matches("/configserver/v1")) {
+ return root(request);
+ }
+ return proxy(request);
+ }
+
+ private HttpResponse proxy(HttpRequest request) {
+ Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ if ( ! path.matches("/configserver/v1/{environment}/{region}/{*}")) {
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ ZoneId zoneId = ZoneId.from(path.get("environment"), path.get("region"));
+ if (! zoneRegistry.hasZone(zoneId) && ! CONTROLLER_ZONE.equals(zoneId)) {
+ throw new IllegalArgumentException("No such zone: " + zoneId.value());
+ }
+
+ String cfgPath = "/" + path.getRest();
+ if (WHITELISTED_APIS.stream().noneMatch(cfgPath::startsWith)) {
+ return ErrorResponse.forbidden("Cannot access '" + cfgPath +
+ "' through /configserver/v1, following APIs are permitted: " + String.join(", ", WHITELISTED_APIS));
+ }
+
+ try {
+ return proxy.handle(new ProxyRequest(request, List.of(getEndpoint(zoneId)), cfgPath));
+ } catch (ProxyException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private HttpResponse root(HttpRequest request) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ ZoneList zoneList = zoneRegistry.zones().reachable();
+
+ Cursor zones = root.setArray("zones");
+ Stream.concat(Stream.of(CONTROLLER_ZONE), zoneRegistry.zones().reachable().ids().stream())
+ .forEach(zone -> {
+ Cursor object = zones.addObject();
+ object.setString("environment", zone.environment().value());
+ object.setString("region", zone.region().value());
+ object.setString("uri", request.getUri().resolve(
+ "/configserver/v1/" + zone.environment().value() + "/" + zone.region().value()).toString());
+ });
+ return new SlimeJsonResponse(slime);
+ }
+
+ private HttpResponse notFound(Path path) {
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private URI getEndpoint(ZoneId zoneId) {
+ return CONTROLLER_ZONE.equals(zoneId) ? zoneRegistry.apiUrl() : zoneRegistry.getConfigServerVipUri(zoneId);
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java
new file mode 100644
index 00000000000..9949c2d17bf
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author freva
+ */
+package com.yahoo.vespa.hosted.controller.restapi.configserver;
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
index be601511763..1a7002c5759 100644
--- 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
@@ -19,6 +19,8 @@ import com.yahoo.vespa.hosted.controller.proxy.ProxyException;
import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
import com.yahoo.yolean.Exceptions;
+import java.net.URI;
+import java.util.List;
import java.util.logging.Level;
/**
@@ -82,7 +84,7 @@ public class ZoneApiHandler extends AuditLoggingRequestHandler {
throw new IllegalArgumentException("No such zone: " + zoneId.value());
}
try {
- return proxy.handle(new ProxyRequest(request, zoneId, path.getRest()));
+ return proxy.handle(new ProxyRequest(request, getConfigserverEndpoints(zoneId), path.getRest()));
} catch (ProxyException e) {
throw new RuntimeException(e);
}
@@ -110,4 +112,13 @@ public class ZoneApiHandler extends AuditLoggingRequestHandler {
private HttpResponse notFound(Path path) {
return ErrorResponse.notFoundError("Nothing at " + path);
}
+
+ private List<URI> getConfigserverEndpoints(ZoneId zoneId) {
+ // TODO: Use config server VIP for all zones that have one
+ if (zoneId.region().value().startsWith("aws-") || zoneId.region().value().contains("-aws-")) {
+ return List.of(zoneRegistry.getConfigServerVipUri(zoneId));
+ } else {
+ return zoneRegistry.getConfigServerUris(zoneId);
+ }
+ }
}