summaryrefslogtreecommitdiffstats
path: root/configserver
diff options
context:
space:
mode:
authorJon Marius Venstad <jonmv@users.noreply.github.com>2023-09-29 14:21:20 +0200
committerGitHub <noreply@github.com>2023-09-29 14:21:20 +0200
commiteb4d4bee35b5152f9b461f33e2467978d8486e24 (patch)
treea75e34c6716abad9e5fbaead4e18eef58365fed7 /configserver
parentf0862db365dd351f5adc480649f9aba18cbd409d (diff)
parent1b0cfae1d7c756c2caeef468d5d2725f81493fdc (diff)
Merge pull request #28711 from vespa-engine/jonmv/token-tell-handler
Jonmv/token tell handler
Diffstat (limited to 'configserver')
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java22
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprints.java18
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClient.java123
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java56
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClientTest.java123
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java4
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java17
7 files changed, 333 insertions, 30 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
index 63faf806e9c..e675e00b642 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
@@ -41,16 +41,18 @@ import com.yahoo.slime.Slime;
import com.yahoo.transaction.NestedTransaction;
import com.yahoo.transaction.Transaction;
import com.yahoo.vespa.applicationmodel.InfrastructureApplication;
+import com.yahoo.vespa.config.server.application.ActiveTokenFingerprints.Token;
+import com.yahoo.vespa.config.server.application.ActiveTokenFingerprintsClient;
import com.yahoo.vespa.config.server.application.Application;
import com.yahoo.vespa.config.server.application.ApplicationCuratorDatabase;
import com.yahoo.vespa.config.server.application.ApplicationData;
import com.yahoo.vespa.config.server.application.ApplicationReindexing;
-import com.yahoo.vespa.config.server.application.ApplicationReindexing.Status;
import com.yahoo.vespa.config.server.application.ApplicationVersions;
import com.yahoo.vespa.config.server.application.ClusterReindexing;
import com.yahoo.vespa.config.server.application.ClusterReindexingStatusClient;
import com.yahoo.vespa.config.server.application.CompressedApplicationInputStream;
import com.yahoo.vespa.config.server.application.ConfigConvergenceChecker;
+import com.yahoo.vespa.config.server.application.ActiveTokenFingerprints;
import com.yahoo.vespa.config.server.application.DefaultClusterReindexingStatusClient;
import com.yahoo.vespa.config.server.application.FileDistributionStatus;
import com.yahoo.vespa.config.server.application.HttpProxy;
@@ -129,7 +131,6 @@ import static com.yahoo.vespa.config.server.tenant.TenantRepository.HOSTED_VESPA
import static com.yahoo.vespa.curator.Curator.CompletionWaiter;
import static com.yahoo.yolean.Exceptions.uncheck;
import static java.nio.file.Files.readAttributes;
-import static java.util.Comparator.naturalOrder;
/**
* The API for managing applications.
@@ -159,6 +160,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
private final Metric metric;
private final SecretStoreValidator secretStoreValidator;
private final ClusterReindexingStatusClient clusterReindexingStatusClient;
+ private final ActiveTokenFingerprints activeTokenFingerprints;
private final FlagSource flagSource;
@Inject
@@ -188,6 +190,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
metric,
new SecretStoreValidator(secretStore),
new DefaultClusterReindexingStatusClient(),
+ new ActiveTokenFingerprintsClient(),
flagSource);
}
@@ -205,6 +208,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
Metric metric,
SecretStoreValidator secretStoreValidator,
ClusterReindexingStatusClient clusterReindexingStatusClient,
+ ActiveTokenFingerprints activeTokenFingerprints,
FlagSource flagSource) {
this.tenantRepository = Objects.requireNonNull(tenantRepository);
this.hostProvisioner = Objects.requireNonNull(hostProvisioner);
@@ -219,7 +223,8 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
this.testerClient = Objects.requireNonNull(testerClient);
this.metric = Objects.requireNonNull(metric);
this.secretStoreValidator = Objects.requireNonNull(secretStoreValidator);
- this.clusterReindexingStatusClient = clusterReindexingStatusClient;
+ this.clusterReindexingStatusClient = Objects.requireNonNull(clusterReindexingStatusClient);
+ this.activeTokenFingerprints = Objects.requireNonNull(activeTokenFingerprints);
this.flagSource = flagSource;
}
@@ -237,6 +242,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
private SecretStoreValidator secretStoreValidator = new SecretStoreValidator(new SecretStoreProvider().get());
private FlagSource flagSource = new InMemoryFlagSource();
private ConfigConvergenceChecker configConvergenceChecker = new ConfigConvergenceChecker();
+ private Map<String, List<Token>> activeTokens = Map.of();
public Builder withTenantRepository(TenantRepository tenantRepository) {
this.tenantRepository = tenantRepository;
@@ -298,6 +304,11 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
return this;
}
+ public Builder withActiveTokens(Map<String, List<Token>> tokens) {
+ this.activeTokens = tokens;
+ return this;
+ }
+
public ApplicationRepository build() {
return new ApplicationRepository(tenantRepository,
tenantRepository.hostProvisionerProvider().getHostProvisioner(),
@@ -313,6 +324,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
metric,
secretStoreValidator,
ClusterReindexingStatusClient.DUMMY_INSTANCE,
+ __ -> activeTokens,
flagSource);
}
@@ -612,6 +624,10 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
return uncheck(() -> clusterReindexingStatusClient.getReindexingStatus(getApplication(applicationId)));
}
+ public Map<String, List<Token>> activeTokenFingerprints(ApplicationId applicationId) {
+ return activeTokenFingerprints.get(getApplication(applicationId));
+ }
+
public Long getApplicationGeneration(ApplicationId applicationId) {
return getApplication(applicationId).getApplicationGeneration();
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprints.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprints.java
new file mode 100644
index 00000000000..9cde5e38302
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprints.java
@@ -0,0 +1,18 @@
+package com.yahoo.vespa.config.server.application;
+
+import com.yahoo.vespa.config.server.modelfactory.ModelResult;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author jonmv
+ */
+public interface ActiveTokenFingerprints {
+
+ /** Lists all active tokens and their fingerprints for each token-enabled container host in the application, that is currently up. */
+ Map<String, List<Token>> get(ModelResult application);
+
+ record Token(String id, List<String> fingerprints) { }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClient.java
new file mode 100644
index 00000000000..4e9eac7a9a6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClient.java
@@ -0,0 +1,123 @@
+package com.yahoo.vespa.config.server.application;
+
+import ai.vespa.http.DomainName;
+import ai.vespa.http.HttpURL;
+import ai.vespa.http.HttpURL.Path;
+import ai.vespa.http.HttpURL.Scheme;
+import ai.vespa.util.http.hc5.VespaAsyncHttpClientBuilder;
+import com.yahoo.config.model.api.ApplicationClusterEndpoint.AuthMethod;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.slime.Inspector;
+import com.yahoo.vespa.config.server.modelfactory.ModelResult;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Phaser;
+
+import static com.yahoo.config.model.api.container.ContainerServiceType.CONTAINER;
+import static com.yahoo.config.model.api.container.ContainerServiceType.QRSERVER;
+import static com.yahoo.slime.SlimeUtils.entriesStream;
+import static com.yahoo.slime.SlimeUtils.jsonToSlime;
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * @author jonmv
+ */
+public class ActiveTokenFingerprintsClient implements ActiveTokenFingerprints, AutoCloseable {
+
+ private final CloseableHttpAsyncClient httpClient = createHttpClient();
+
+ public ActiveTokenFingerprintsClient() {
+ httpClient.start();
+ }
+
+ @Override
+ public Map<String, List<Token>> get(ModelResult application) {
+ Set<String> containersWithTokenFilter = application.getModel().applicationClusterInfo().stream()
+ .flatMap(cluster -> cluster.endpoints().stream())
+ .filter(endpoint -> endpoint.authMethod() == AuthMethod.token)
+ .flatMap(endpoint -> endpoint.hostNames().stream())
+ .collect(toSet());
+ return getFingerprints(application.getModel().getHosts().stream()
+ .filter(host -> containersWithTokenFilter.contains(host.getHostname()))
+ .flatMap(host -> host.getServices().stream())
+ .filter(service -> service.getServiceType().equals(CONTAINER.serviceName)
+ || service.getServiceType().equals(QRSERVER.serviceName))
+ .toList());
+ }
+
+ private Map<String, List<Token>> getFingerprints(List<ServiceInfo> services) {
+ Map<String, List<Token>> tokens = new ConcurrentHashMap<>();
+ Phaser phaser = new Phaser(services.size() + 1);
+ for (ServiceInfo service : services) getFingerprints(tokens, service, phaser);
+ phaser.arriveAndAwaitAdvance();
+ return tokens;
+ }
+
+ // A container may be unable to provide its fingerprints for a number of reasons, which may be OK, so
+ // we only track those containers which return an OK response, but we do require at least one such response.
+ private void getFingerprints(Map<String, List<Token>> hostTokens, ServiceInfo service, Phaser phaser) {
+ URI uri = HttpURL.create(Scheme.http,
+ DomainName.of(service.getHostName()),
+ service.getPorts().stream().filter(port -> port.getTags().stream().anyMatch("http"::equals)).findAny().get().getPort(),
+ Path.parse("/data-plane-tokens/v1"))
+ .asURI();
+ httpClient.execute(SimpleRequestBuilder.get(uri).build(), new FutureCallback<>() {
+ @Override public void completed(SimpleHttpResponse result) {
+ if (result.getCode() == 200) hostTokens.put(service.getHostName(), parseTokens(result));
+ phaser.arrive();
+ }
+ @Override public void failed(Exception ex) { phaser.arrive(); }
+ @Override public void cancelled() { phaser.arrive(); }
+ });
+ }
+
+ private static List<Token> parseTokens(SimpleHttpResponse response) {
+ return entriesStream(jsonToSlime(response.getBodyBytes()).get().field("tokens"))
+ .map(entry -> new Token(entry.field("id").asString(),
+ entriesStream(entry.field("fingerprints")).map(Inspector::asString).toList()))
+ .toList();
+ }
+
+ private static CloseableHttpAsyncClient createHttpClient() {
+ return VespaAsyncHttpClientBuilder
+ .create(tlsStrategy -> PoolingAsyncClientConnectionManagerBuilder.create()
+ .setTlsStrategy(tlsStrategy)
+ .setDefaultConnectionConfig(ConnectionConfig.custom()
+ .setConnectTimeout(Timeout.ofSeconds(2))
+ .build())
+ .build())
+ .setIOReactorConfig(IOReactorConfig.custom()
+ .setSoTimeout(Timeout.ofSeconds(2))
+ .build())
+ .setDefaultRequestConfig(
+ RequestConfig.custom()
+ .setConnectionRequestTimeout(Timeout.ofSeconds(2))
+ .setResponseTimeout(Timeout.ofSeconds(2))
+ .build())
+ .setUserAgent("data-plane-token-client")
+ .build();
+ }
+
+ @Override
+ public void close() throws Exception {
+ httpClient.close(CloseMode.GRACEFUL);
+ httpClient.awaitShutdown(TimeValue.ofSeconds(10));
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
index bd6e0f90b54..f39feceeeb1 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
@@ -95,9 +95,11 @@ public class ApplicationHandler extends HttpHandler {
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}")) return getApplicationResponse(applicationId(path));
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/content/{*}")) return content(applicationId(path), path.getRest(), request);
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/filedistributionstatus")) return filedistributionStatus(applicationId(path), request);
+ if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/active-token-fingerprints")) return activeTokenFingerprints(applicationId(path));
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/logs")) return logs(applicationId(path), request);
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/metrics/deployment")) return deploymentMetrics(applicationId(path));
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/metrics/searchnode")) return searchNodeMetrics(applicationId(path));
+ if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/quota")) return quotaUsage(applicationId(path));
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/reindexing")) return getReindexingStatus(applicationId(path));
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/service/{service}/{hostname}/status/{*}")) return serviceStatusPage(applicationId(path), path.get("service"), path.get("hostname"), path.getRest(), request);
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/service/{service}/{hostname}/state/v1/{*}")) return serviceStateV1(applicationId(path), path.get("service"), path.get("hostname"), path.getRest(), request);
@@ -105,7 +107,6 @@ public class ApplicationHandler extends HttpHandler {
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/serviceconverge/{hostAndPort}")) return checkServiceConverge(applicationId(path), path.get("hostAndPort"), request);
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/suspended")) return isSuspended(applicationId(path));
if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/tester/{command}")) return testerRequest(applicationId(path), path.get("command"), request);
- if (path.matches("/application/v2/tenant/{tenant}/application/{application}/environment/{ignore}/region/{ignore}/instance/{instance}/quota")) return quotaUsage(applicationId(path));
return ErrorResponse.notFoundError("Nothing at " + path);
}
@@ -150,18 +151,11 @@ public class ApplicationHandler extends HttpHandler {
}
private HttpResponse serviceStatusPage(ApplicationId applicationId, String service, String hostname, HttpURL.Path pathSuffix, HttpRequest request) {
- HttpURL.Path pathPrefix = HttpURL.Path.empty();
- switch (service) {
- case "container-clustercontroller":
- pathPrefix = pathPrefix.append("clustercontroller-status").append("v1");
- break;
- case "distributor":
- case "storagenode":
- pathPrefix = pathPrefix.append("contentnode-status").append("v1");
- break;
- default:
- throw new com.yahoo.vespa.config.server.NotFoundException("No status page for service: " + service);
- }
+ HttpURL.Path pathPrefix = switch (service) {
+ case "container-clustercontroller" -> HttpURL.Path.empty().append("clustercontroller-status").append("v1");
+ case "distributor", "storagenode" -> HttpURL.Path.empty().append("contentnode-status").append("v1");
+ default -> throw new NotFoundException("No status page for service: " + service);
+ };
return applicationRepository.proxyServiceHostnameRequest(applicationId, hostname, service, pathPrefix.append(pathSuffix), Query.empty().add(request.getJDiscRequest().parameters()), null);
}
@@ -194,6 +188,22 @@ public class ApplicationHandler extends HttpHandler {
return applicationRepository.fileDistributionStatus(applicationId, getTimeoutFromRequest(request));
}
+ private HttpResponse activeTokenFingerprints(ApplicationId applicationId) {
+ Slime slime = new Slime();
+ Cursor hostsArray = slime.setObject().setArray("hosts");
+ applicationRepository.activeTokenFingerprints(applicationId).forEach((host, tokens) -> {
+ Cursor hostObject = hostsArray.addObject();
+ hostObject.setString("host", host);
+ Cursor tokensArray = hostObject.setArray("tokens");
+ tokens.forEach(token -> {
+ Cursor tokenObject = tokensArray.addObject();
+ tokenObject.setString("id", token.id());
+ token.fingerprints().forEach(tokenObject.setArray("fingerprints")::addString);
+ });
+ });
+ return new SlimeJsonResponse(slime);
+ }
+
private HttpResponse logs(ApplicationId applicationId, HttpRequest request) {
Optional<DomainName> hostname = Optional.ofNullable(request.getProperty("hostname")).map(DomainName::of);
String apiParams = Optional.ofNullable(request.getUri().getQuery()).map(q -> "?" + q).orElse("");
@@ -213,19 +223,13 @@ public class ApplicationHandler extends HttpHandler {
}
private HttpResponse testerRequest(ApplicationId applicationId, String command, HttpRequest request) {
- switch (command) {
- case "status":
- return applicationRepository.getTesterStatus(applicationId);
- case "log":
- Long after = Long.valueOf(request.getProperty("after"));
- return applicationRepository.getTesterLog(applicationId, after);
- case "ready":
- return applicationRepository.isTesterReady(applicationId);
- case "report":
- return applicationRepository.getTestReport(applicationId);
- default:
- throw new IllegalArgumentException("Unknown tester command in request " + request.getUri().toString());
- }
+ return switch (command) {
+ case "status" -> applicationRepository.getTesterStatus(applicationId);
+ case "log" -> applicationRepository.getTesterLog(applicationId, Long.valueOf(request.getProperty("after")));
+ case "ready" -> applicationRepository.isTesterReady(applicationId);
+ case "report" -> applicationRepository.getTestReport(applicationId);
+ default -> throw new IllegalArgumentException("Unknown tester command in request " + request.getUri().toString());
+ };
}
private HttpResponse quotaUsage(ApplicationId applicationId) {
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClientTest.java
new file mode 100644
index 00000000000..03e379311cc
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClientTest.java
@@ -0,0 +1,123 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.application;// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.yahoo.config.ConfigInstance.Builder;
+import com.yahoo.config.FileReference;
+import com.yahoo.config.model.api.ApplicationClusterEndpoint;
+import com.yahoo.config.model.api.ApplicationClusterEndpoint.AuthMethod;
+import com.yahoo.config.model.api.ApplicationClusterEndpoint.DnsName;
+import com.yahoo.config.model.api.ApplicationClusterEndpoint.RoutingMethod;
+import com.yahoo.config.model.api.ApplicationClusterEndpoint.Scope;
+import com.yahoo.config.model.api.ApplicationClusterInfo;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.api.PortInfo;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.config.provision.AllocatedHosts;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.server.application.ActiveTokenFingerprints.Token;
+import com.yahoo.vespa.config.server.modelfactory.ModelResult;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.serverError;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static com.yahoo.config.model.api.container.ContainerServiceType.CONTAINER;
+import static com.yahoo.config.model.api.container.ContainerServiceType.LOGSERVER_CONTAINER;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author jonmv
+ */
+public class ActiveTokenFingerprintsClientTest {
+
+ @Rule public final WireMockRule server1 = new WireMockRule(options().dynamicPort(), true);
+ @Rule public final WireMockRule server2 = new WireMockRule(options().dynamicPort(), true);
+ @Rule public final WireMockRule server3 = new WireMockRule(options().dynamicPort(), true);
+ @Rule public final WireMockRule server4 = new WireMockRule(options().dynamicPort(), true);
+
+ @Test
+ public void verifyMultipleResponsesCombine() throws Exception {
+ try (ActiveTokenFingerprintsClient client = new ActiveTokenFingerprintsClient()) {
+ ModelResult app = MockModel::new;
+ String uriPath = "/data-plane-tokens/v1";
+ server1.stubFor(get(urlEqualTo(uriPath)).willReturn(serverError()));
+ server2.stubFor(get(urlEqualTo(uriPath)).willReturn(okJson("""
+ { "tokens": [ {"id": "t1", "fingerprints": [ "foo", "bar", "baz" ] } ] }
+ """)));
+ server3.stubFor(get(urlEqualTo(uriPath)).willReturn(aResponse().withStatus(503)));
+ server4.stubFor(get(urlEqualTo(uriPath)).willReturn(okJson("""
+ { "tokens": [ {"id": "t2", "fingerprints": [ "quu" ] } ] }
+ """)));
+ Map<String, List<Token>> expected = Map.of("localhost",
+ List.of(new Token("t1", List.of("foo", "bar", "baz"))));
+ assertEquals(expected, client.get(app));
+ }
+ }
+
+ private class MockModel implements Model {
+
+ @Override
+ public Collection<HostInfo> getHosts() {
+ return List.of(host(server1.port(), "localhost"),
+ host(server2.port(), "localhost"),
+ host(server3.port(), "localhost"),
+ host(server4.port(), "127.0.0.1")); // Should not be included, see application cluster info below.
+
+ }
+
+ private HostInfo host(int port, String host) {
+ return new HostInfo(host,
+ List.of(new ServiceInfo("container",
+ CONTAINER.serviceName,
+ List.of(new PortInfo(port, List.of("http"))),
+ Map.of(),
+ "myconfigId",
+ host),
+ new ServiceInfo("logserver",
+ LOGSERVER_CONTAINER.serviceName,
+ List.of(new PortInfo(port, List.of("http"))),
+ Map.of(),
+ "myconfigId",
+ "127.0.0.1"))); // Don't hit this.
+ }
+
+ @Override
+ public Set<ApplicationClusterInfo> applicationClusterInfo() {
+ return Set.of(new ApplicationClusterInfo() {
+ @Override public List<ApplicationClusterEndpoint> endpoints() {
+ return List.of(ApplicationClusterEndpoint.builder()
+ .dnsName(DnsName.from("foo"))
+ .routingMethod(RoutingMethod.exclusive)
+ .authMethod(AuthMethod.token)
+ .scope(Scope.zone)
+ .clusterId("bar")
+ .hosts(List.of("localhost"))
+ .build());
+ }
+ @Override public boolean getDeferChangesUntilRestart() { throw new UnsupportedOperationException(); }
+ @Override public String name() { throw new UnsupportedOperationException(); }
+ });
+ }
+
+ @Override public Builder getConfigInstance(ConfigKey<?> configKey, ConfigDefinition configDefinition) { throw new UnsupportedOperationException(); }
+ @Override public Set<ConfigKey<?>> allConfigsProduced() { throw new UnsupportedOperationException(); }
+ @Override public Set<String> allConfigIds() { throw new UnsupportedOperationException(); }
+ @Override public Set<FileReference> fileReferences() { throw new UnsupportedOperationException(); }
+ @Override public AllocatedHosts allocatedHosts() { throw new UnsupportedOperationException(); }
+
+ }
+
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java
index 6bd20a29cf8..72cfe466993 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java
@@ -10,6 +10,8 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
/**
* Base class for session handler tests
*
@@ -52,7 +54,7 @@ public class SessionHandlerTest {
public static String getRenderedString(HttpResponse response) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
response.render(baos);
- return baos.toString(StandardCharsets.UTF_8);
+ return baos.toString(UTF_8);
}
public enum Cmd {
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java
index 951ef9df2f4..6fb5db70b68 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java
@@ -28,6 +28,7 @@ import com.yahoo.vespa.config.server.MockLogRetriever;
import com.yahoo.vespa.config.server.MockProvisioner;
import com.yahoo.vespa.config.server.MockSecretStoreValidator;
import com.yahoo.vespa.config.server.MockTesterClient;
+import com.yahoo.vespa.config.server.application.ActiveTokenFingerprints.Token;
import com.yahoo.vespa.config.server.application.ApplicationCuratorDatabase;
import com.yahoo.vespa.config.server.application.ApplicationReindexing;
import com.yahoo.vespa.config.server.application.ClusterReindexing;
@@ -117,6 +118,7 @@ public class ApplicationHandlerTest {
private ManualClock clock;
private List<Endpoint> expectedEndpoints;
private Availability availability;
+ private Map<String, List<Token>> activeTokenFingerprints;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -140,6 +142,7 @@ public class ApplicationHandlerTest {
.build();
tenantRepository.addTenant(mytenantName);
orchestrator = new OrchestratorMock();
+ activeTokenFingerprints = new HashMap<>();
applicationRepository = new ApplicationRepository.Builder()
.withTenantRepository(tenantRepository)
.withOrchestrator(orchestrator)
@@ -149,6 +152,7 @@ public class ApplicationHandlerTest {
.withConfigserverConfig(configserverConfig)
.withSecretStoreValidator(secretStoreValidator)
.withEndpointsChecker(endpoints -> { assertEquals(expectedEndpoints, endpoints); return availability; })
+ .withActiveTokens(activeTokenFingerprints)
.build();
}
@@ -238,6 +242,19 @@ public class ApplicationHandlerTest {
}
@Test
+ public void testGetTokenFingerprints() throws IOException {
+ applicationRepository.deploy(testApp, prepareParams(applicationId));
+ activeTokenFingerprints.putAll(Map.of("host", List.of(new Token("t1", List.of("fingers", "toes")),
+ new Token("t2", List.of())),
+ "toast", List.of()));
+ HttpResponse response = createApplicationHandler().handleGET(createTestRequest(toUrlPath(applicationId, Zone.defaultZone(), true) + "/active-token-fingerprints", GET));
+ assertEquals(200, response.getStatus());
+ assertEquals("""
+ {"hosts":[{"host":"host","tokens":[{"id":"t1","fingerprints":["fingers","toes"]},{"id":"t2","fingerprints":[]}]},{"host":"toast","tokens":[]}]}""",
+ getRenderedString(response));
+ }
+
+ @Test
public void testReindex() throws Exception {
ApplicationCuratorDatabase database = applicationRepository.getTenant(applicationId).getApplicationRepo().database();
reindexing(applicationId, GET, "{\"error-code\": \"NOT_FOUND\", \"message\": \"Application 'default.default' not found\"}", 404);