diff options
author | jonmv <venstad@gmail.com> | 2023-09-28 15:31:08 +0200 |
---|---|---|
committer | jonmv <venstad@gmail.com> | 2023-09-28 15:39:53 +0200 |
commit | d10c4a0aebadf5ffb73218b8444d489a79b159ae (patch) | |
tree | b912f7966b0ad7e30197f33db71ccf5eef350271 /configserver | |
parent | 71bfc352333411f0a3bdebdb7cde032fdb2ff96a (diff) |
Fetch active tokens for an application, through /application/v2/
Diffstat (limited to 'configserver')
6 files changed, 301 insertions, 3 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..058606a789e 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,17 @@ 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.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 +130,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 +159,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 +189,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye metric, new SecretStoreValidator(secretStore), new DefaultClusterReindexingStatusClient(), + new ActiveTokenFingerprintsClient(), flagSource); } @@ -205,6 +207,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 +222,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 +241,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<String>> activeTokens = Map.of(); public Builder withTenantRepository(TenantRepository tenantRepository) { this.tenantRepository = tenantRepository; @@ -298,6 +303,11 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye return this; } + public Builder withActiveTokens(Map<String, List<String>> tokens) { + this.activeTokens = tokens; + return this; + } + public ApplicationRepository build() { return new ApplicationRepository(tenantRepository, tenantRepository.hostProvisionerProvider().getHostProvisioner(), @@ -313,6 +323,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye metric, secretStoreValidator, ClusterReindexingStatusClient.DUMMY_INSTANCE, + __ -> activeTokens, flagSource); } @@ -612,6 +623,10 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye return uncheck(() -> clusterReindexingStatusClient.getReindexingStatus(getApplication(applicationId))); } + public Map<String, List<String>> 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..cf67d6e4abd --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprints.java @@ -0,0 +1,16 @@ +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 token fingerprints for each token-enabled container host in the application, that is currently up. */ + Map<String, List<String>> get(ModelResult application); + +} 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..d5beb263155 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClient.java @@ -0,0 +1,119 @@ +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.toSet; + +/** + * @author jonmv + */ +public class ActiveTokenFingerprintsClient implements ActiveTokenFingerprints, AutoCloseable { + + private final CloseableHttpAsyncClient httpClient = createHttpClient(); + + public ActiveTokenFingerprintsClient() { + httpClient.start(); + } + + @Override + public Map<String, List<String>> 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<String>> getFingerprints(List<ServiceInfo> services) { + Map<String, List<String>> fingerprints = new ConcurrentHashMap<>(); + Phaser phaser = new Phaser(services.size() + 1); + for (ServiceInfo service : services) getFingerprints(fingerprints, service, phaser); + phaser.arriveAndAwaitAdvance(); + return fingerprints; + } + + // 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<String>> hostFingerprints, 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) hostFingerprints.put(service.getHostName(), parseFingerprints(result)); + phaser.arrive(); + } + @Override public void failed(Exception ex) { phaser.arrive(); } + @Override public void cancelled() { phaser.arrive(); } + }); + } + + private static List<String> parseFingerprints(SimpleHttpResponse response) { + return entriesStream(jsonToSlime(response.getBodyBytes()).get().field("fingerprints")).map(Inspector::asString).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 5206308a664..69d0365f089 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,6 +95,7 @@ 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)); @@ -187,6 +188,17 @@ 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, fingerprints) -> { + Cursor hostObject = hostsArray.addObject(); + hostObject.setString("host", host); + fingerprints.forEach(hostObject.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(""); 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..dea224d2e9b --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ActiveTokenFingerprintsClientTest.java @@ -0,0 +1,121 @@ +// 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.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(""" + { "fingerprints": [ "foo", "bar", "baz" ] } + """))); + server3.stubFor(get(urlEqualTo(uriPath)).willReturn(aResponse().withStatus(503))); + server4.stubFor(get(urlEqualTo(uriPath)).willReturn(okJson(""" + { "fingerprints": [ "quu", "qux", "fez" ] } + """))); + Map<String, List<String>> expected = Map.of("localhost", 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/v2/ApplicationHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java index 951ef9df2f4..5b2d5d491a1 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 @@ -117,6 +117,7 @@ public class ApplicationHandlerTest { private ManualClock clock; private List<Endpoint> expectedEndpoints; private Availability availability; + private Map<String, List<String>> activeTokenFingerprints; @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -140,6 +141,7 @@ public class ApplicationHandlerTest { .build(); tenantRepository.addTenant(mytenantName); orchestrator = new OrchestratorMock(); + activeTokenFingerprints = new HashMap<>(); applicationRepository = new ApplicationRepository.Builder() .withTenantRepository(tenantRepository) .withOrchestrator(orchestrator) @@ -149,6 +151,7 @@ public class ApplicationHandlerTest { .withConfigserverConfig(configserverConfig) .withSecretStoreValidator(secretStoreValidator) .withEndpointsChecker(endpoints -> { assertEquals(expectedEndpoints, endpoints); return availability; }) + .withActiveTokens(activeTokenFingerprints) .build(); } @@ -238,6 +241,18 @@ public class ApplicationHandlerTest { } @Test + public void testGetTokenFingerprints() throws IOException { + applicationRepository.deploy(testApp, prepareParams(applicationId)); + activeTokenFingerprints.putAll(Map.of("host", List.of("fingers", "toes"), + "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","fingerprints":["fingers","toes"]},{"host":"toast","fingerprints":[]}]}""", + 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); |