diff options
author | Ola Aunrønning <olaa@verizonmedia.com> | 2021-02-17 09:46:14 +0100 |
---|---|---|
committer | Ola Aunrønning <olaa@verizonmedia.com> | 2021-02-18 15:02:26 +0100 |
commit | cf904cc81f6a39a2e68c4aa7433befdaf9ca9cf3 (patch) | |
tree | 5e3f6b4c3a659e38a61a19a5bda3c7d13fc86ad5 | |
parent | 530582e559716a96cc108d7a04b7f8e18e306be3 (diff) |
Parameter validation from controller to container
7 files changed, 220 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 81ea8d76f14..8c13d52a201 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 @@ -23,6 +23,8 @@ import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.SecretStoreProvider; +import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.docproc.jdisc.metric.NullMetric; import com.yahoo.io.IOUtils; import com.yahoo.jdisc.Metric; @@ -39,6 +41,7 @@ import com.yahoo.vespa.config.server.application.ConfigConvergenceChecker; import com.yahoo.vespa.config.server.application.DefaultClusterReindexingStatusClient; import com.yahoo.vespa.config.server.application.FileDistributionStatus; import com.yahoo.vespa.config.server.application.HttpProxy; +import com.yahoo.vespa.config.server.http.SecretStoreValidator; import com.yahoo.vespa.config.server.application.TenantApplications; import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; import com.yahoo.vespa.config.server.configchange.RefeedActions; @@ -69,6 +72,7 @@ import com.yahoo.vespa.config.server.tenant.EndpointCertificateMetadataStore; import com.yahoo.vespa.config.server.tenant.Tenant; import com.yahoo.vespa.config.server.tenant.TenantMetaData; import com.yahoo.vespa.config.server.tenant.TenantRepository; +import com.yahoo.vespa.config.server.application.TenantSecretStore; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.curator.stats.LockStats; import com.yahoo.vespa.curator.stats.ThreadLockStats; @@ -88,7 +92,6 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -138,6 +141,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye private final LogRetriever logRetriever; private final TesterClient testerClient; private final Metric metric; + private final SecretStoreValidator secretStoreValidator; private final ClusterReindexingStatusClient clusterReindexingStatusClient; @Inject @@ -149,7 +153,8 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye ConfigserverConfig configserverConfig, Orchestrator orchestrator, TesterClient testerClient, - Metric metric) { + Metric metric, + SecretStore secretStore) { this(tenantRepository, hostProvisionerProvider.getHostProvisioner(), infraDeployerProvider.getInfraDeployer(), @@ -161,6 +166,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye Clock.systemUTC(), testerClient, metric, + new SecretStoreValidator(secretStore), new DefaultClusterReindexingStatusClient()); } @@ -175,6 +181,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye Clock clock, TesterClient testerClient, Metric metric, + SecretStoreValidator secretStoreValidator, ClusterReindexingStatusClient clusterReindexingStatusClient) { this.tenantRepository = Objects.requireNonNull(tenantRepository); this.hostProvisioner = Objects.requireNonNull(hostProvisioner); @@ -187,6 +194,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye this.clock = Objects.requireNonNull(clock); this.testerClient = Objects.requireNonNull(testerClient); this.metric = Objects.requireNonNull(metric); + this.secretStoreValidator = Objects.requireNonNull(secretStoreValidator); this.clusterReindexingStatusClient = clusterReindexingStatusClient; } @@ -200,6 +208,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye private LogRetriever logRetriever = new LogRetriever(); private TesterClient testerClient = new TesterClient(); private Metric metric = new NullMetric(); + private SecretStoreValidator secretStoreValidator = new SecretStoreValidator(new SecretStoreProvider().get()); private FlagSource flagSource = new InMemoryFlagSource(); public Builder withTenantRepository(TenantRepository tenantRepository) { @@ -271,6 +280,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye clock, testerClient, metric, + secretStoreValidator, ClusterReindexingStatusClient.DUMMY_INSTANCE); } @@ -671,6 +681,11 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye : applicationSet.get().getAllVersions(applicationId); } + public HttpResponse validateSecretStore(ApplicationId applicationId, TenantSecretStore tenantSecretStore, String tenantSecretName) { + Application application = getApplication(applicationId); + return secretStoreValidator.validateSecretStore(application, tenantSecretStore, tenantSecretName); + } + // ---------------- Convergence ---------------------------------------------------------------- public HttpResponse checkServiceForConfigConvergence(ApplicationId applicationId, String hostAndPort, URI uri, diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantSecretStore.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantSecretStore.java new file mode 100644 index 00000000000..61ebddbe78c --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantSecretStore.java @@ -0,0 +1,62 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import java.util.Objects; + +/** + * @author olaa + */ +public class TenantSecretStore { + + private final String name; + private final String awsId; + private final String role; + + public TenantSecretStore(String name, String awsId, String role) { + this.name = name; + this.awsId = awsId; + this.role = role; + } + + public String getName() { + return name; + } + + public String getAwsId() { + return awsId; + } + + public String getRole() { + return role; + } + + public boolean isValid() { + return !name.isBlank() && + !awsId.isBlank() && + !role.isBlank(); + } + + @Override + public String toString() { + return "TenantSecretStore{" + + "name='" + name + '\'' + + ", awsId='" + awsId + '\'' + + ", role='" + role + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TenantSecretStore that = (TenantSecretStore) o; + return name.equals(that.name) && + awsId.equals(that.awsId) && + role.equals(that.role); + } + + @Override + public int hashCode() { + return Objects.hash(name, awsId, role); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SecretStoreValidator.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SecretStoreValidator.java new file mode 100644 index 00000000000..d8c09eecd6d --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SecretStoreValidator.java @@ -0,0 +1,80 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import ai.vespa.util.http.VespaHttpClientBuilder; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.secretstore.SecretStore; +import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.application.TenantSecretStore; +import com.yahoo.yolean.Exceptions; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.IOException; +import java.net.URI; + +import static com.yahoo.config.model.api.container.ContainerServiceType.CONTAINER; +import static com.yahoo.yolean.Exceptions.uncheck; + +/** + * @author olaa + */ +public class SecretStoreValidator { + + private static final String AWS_PARAMETER_VALIDATION_HANDLER_POSTFIX = ":4080/validate-secret-store"; + private final SecretStore secretStore; + private final CloseableHttpClient httpClient = VespaHttpClientBuilder.create().build(); + + public SecretStoreValidator(SecretStore secretStore) { + this.secretStore = secretStore; + } + + public HttpResponse validateSecretStore(Application application, TenantSecretStore tenantSecretStore, String tenantSecretName) { + var slime = toSlime(tenantSecretStore, tenantSecretName); + var uri = getUri(application); + return postRequest(uri, slime); + } + + private Slime toSlime(TenantSecretStore tenantSecretStore, String tenantSecretName) { + var slime = new Slime(); + var cursor = slime.setObject(); + cursor.setString("externalId", secretStore.getSecret(tenantSecretName)); + cursor.setString("awsId", tenantSecretStore.getAwsId()); + cursor.setString("name", tenantSecretStore.getName()); + cursor.setString("role", tenantSecretStore.getRole()); + return slime; + } + + private URI getUri(Application application) { + var hostname = application.getModel().getHosts() + .stream() + .filter(hostInfo -> + hostInfo.getServices() + .stream() + .filter(service -> CONTAINER.serviceName.equals(service.getServiceType())) + .count() > 0) + .map(HostInfo::getHostname) + .findFirst().orElseThrow(); + return URI.create(hostname + AWS_PARAMETER_VALIDATION_HANDLER_POSTFIX); + } + + private HttpResponse postRequest(URI uri, Slime slime) { + var postRequest = new HttpPost(uri); + var data = uncheck(() -> SlimeUtils.toJsonBytes(slime)); + var entity = new ByteArrayEntity(data); + postRequest.setEntity(entity); + try (CloseableHttpResponse response = httpClient.execute(postRequest)){ + return new ProxyResponse(response); + } catch (IOException e) { + return HttpErrorResponse.internalServerError( + String.format("Failed to post request to %s: %s", uri, Exceptions.toMessageString(e)) + ); + } + } + +} 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 08ba028396f..ec714eed575 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 @@ -17,6 +17,7 @@ import com.yahoo.jdisc.Response; import com.yahoo.jdisc.application.BindingMatch; import com.yahoo.jdisc.application.UriPattern; import com.yahoo.slime.Cursor; +import com.yahoo.slime.SlimeUtils; import com.yahoo.text.StringUtilities; import com.yahoo.vespa.config.server.ApplicationRepository; import com.yahoo.vespa.config.server.application.ApplicationReindexing; @@ -28,12 +29,12 @@ import com.yahoo.vespa.config.server.http.HttpHandler; import com.yahoo.vespa.config.server.http.JSONResponse; import com.yahoo.vespa.config.server.http.NotFoundException; import com.yahoo.vespa.config.server.tenant.Tenant; +import com.yahoo.vespa.config.server.application.TenantSecretStore; import java.io.IOException; import java.net.URLDecoder; import java.time.Duration; import java.time.Instant; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -44,6 +45,7 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.stream.Stream; +import static com.yahoo.yolean.Exceptions.uncheck; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Map.Entry.comparingByKey; import static java.util.stream.Collectors.toList; @@ -68,6 +70,7 @@ public class ApplicationHandler extends HttpHandler { "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/metrics/*", "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/metrics/*", "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/logs", + "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/validate-secret-store/*", "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/tester/*/*", "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/tester/*", "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/quota", @@ -227,6 +230,12 @@ public class ApplicationHandler extends HttpHandler { return createMessageResponse("Reindexing enabled"); } + if (isValidateSecretStoreRequest(request)) { + var tenantSecretStore = tenantSecretStoreFromRequest(request); + var tenantSecretName = tenantSecretNameFromRequest(request); + return applicationRepository.validateSecretStore(applicationId, tenantSecretStore, tenantSecretName); + } + throw new NotFoundException("Illegal POST request '" + request.getUri() + "'"); } @@ -350,6 +359,11 @@ public class ApplicationHandler extends HttpHandler { request.getUri().getPath().endsWith("/logs"); } + private static boolean isValidateSecretStoreRequest(HttpRequest request) { + return getBindingMatch(request).groupCount() == 8 && + request.getUri().getPath().contains("/validate-secret-store/"); + } + private static boolean isServiceConvergeListRequest(HttpRequest request) { return getBindingMatch(request).groupCount() == 7 && request.getUri().getPath().endsWith("/serviceconverge"); @@ -410,6 +424,11 @@ public class ApplicationHandler extends HttpHandler { return bm.group(8); } + private static String tenantSecretNameFromRequest(HttpRequest req) { + BindingMatch<?> bm = getBindingMatch(req); + return bm.group(8); + } + private static ApplicationId getApplicationIdFromRequest(HttpRequest req) { // Two bindings for this: with full app id or only application name BindingMatch<?> bm = getBindingMatch(req); @@ -514,6 +533,14 @@ public class ApplicationHandler extends HttpHandler { } + private TenantSecretStore tenantSecretStoreFromRequest(HttpRequest httpRequest) { + var data = uncheck(() -> SlimeUtils.jsonToSlime(httpRequest.getData().readAllBytes()).get()); + var awsId = data.field("awsId").asString(); + var name = data.field("name").asString(); + var role = data.field("role").asString(); + return new TenantSecretStore(name, awsId, role); + } + private static JSONResponse createMessageResponse(String message) { return new JSONResponse(Response.Status.OK) { { object.setString("message", message); } }; } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java index a90155d4e3e..315665dbffc 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.configserver; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.flags.json.FlagData; import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics; @@ -14,6 +15,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.LogEntry; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud; import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter; +import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import com.yahoo.vespa.serviceview.bindings.ApplicationView; import java.io.InputStream; @@ -148,4 +150,7 @@ public interface ConfigServer { /** Sets suspension status — whether application node operations are orchestrated — for the given deployment. */ void setSuspension(DeploymentId deploymentId, boolean suspend); + /** Validates secret store configuration. */ + String validateSecretStore(DeploymentId deploymentId, TenantSecretStore tenantSecretStore); + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 38bcfda0ac7..1d3ccdb6fa7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -298,6 +298,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/reindexing")) return enableReindexing(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/suspend")) return suspend(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), true); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/validate-parameter-store")) return validateParameterStore(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/deploy")) return deploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); // legacy synonym of the above if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); @@ -582,6 +583,26 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(root); } + + private HttpResponse validateParameterStore(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) { + var tenant = TenantName.from(tenantName); + if (controller.tenants().require(tenant).type() != Tenant.Type.cloud) + throw new IllegalArgumentException("Tenant '" + tenant + "' is not a cloud tenant"); + + var application = ApplicationId.from(tenantName, applicationName, instanceName); + var zone = requireZone(environment, region); + var deployment = new DeploymentId(application, zone); + + var data = toSlime(request.getData()).get(); + var awsId = mandatory("awsId", data).asString(); + var name = mandatory("name", data).asString(); + var role = mandatory("role", data).asString(); + var tenantSecretStore = new TenantSecretStore(name, awsId, role); + + var response = controller.serviceRegistry().configServer().validateSecretStore(deployment, tenantSecretStore); + return new MessageResponse(response); + } + private HttpResponse removeDeveloperKey(String tenantName, HttpRequest request) { if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java index 7ce1e6b8b83..35b0a7ba5b3 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java @@ -12,6 +12,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; +import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.flags.json.FlagData; import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics; @@ -38,6 +39,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.ServiceCon import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud; import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter; +import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.serviceview.bindings.ApplicationView; @@ -573,6 +575,11 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer return q; } + @Override + public String validateSecretStore(DeploymentId deployment, TenantSecretStore tenantSecretStore) { + return ""; + } + public static class Application { private final ApplicationId id; |