diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2019-11-01 16:55:02 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-11-01 16:55:02 +0100 |
commit | 69719912deb821dbf8c6eb1be3e23a3f05ee2a99 (patch) | |
tree | 3909bfe11d703a53c9d8ae8ac09a0ed65b0e3185 /controller-server/src/main/java/com | |
parent | 197628b906de4fec5e341fe57041259823e3d05d (diff) | |
parent | 70731418933393c915d64df49c19d43aa9fd25ee (diff) |
Merge pull request #11183 from vespa-engine/freva/configserver-v1
Create /configserver/v1
Diffstat (limited to 'controller-server/src/main/java/com')
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); + } + } } |