diff options
27 files changed, 375 insertions, 118 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java index 91ecb981e12..ebafd26f942 100644 --- a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java @@ -115,6 +115,8 @@ public interface ModelContext { default Quota quota() { return Quota.unlimited(); } + default List<TenantSecretStore> tenantSecretStores() { return List.of(); } + /// Default setting for the gc-options attribute if not specified explicit by application String jvmGCOptions(); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantSecretStore.java b/config-model-api/src/main/java/com/yahoo/config/model/api/TenantSecretStore.java index 61ebddbe78c..f39a3901177 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantSecretStore.java +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/TenantSecretStore.java @@ -1,7 +1,8 @@ // 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; +package com.yahoo.config.model.api; import java.util.Objects; +import java.util.Optional; /** * @author olaa @@ -11,6 +12,7 @@ public class TenantSecretStore { private final String name; private final String awsId; private final String role; + private Optional<String> externalId; public TenantSecretStore(String name, String awsId, String role) { this.name = name; @@ -30,10 +32,12 @@ public class TenantSecretStore { return role; } - public boolean isValid() { - return !name.isBlank() && - !awsId.isBlank() && - !role.isBlank(); + public Optional<String> getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = Optional.of(externalId); } @Override diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/SecretStoreValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/SecretStoreValidator.java index bafc0b8b3de..b57d2841a98 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/SecretStoreValidator.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/SecretStoreValidator.java @@ -19,26 +19,16 @@ import java.util.List; */ public class SecretStoreValidator extends Validator { - private final List<String> VALID_TYPES = List.of("oath-ckms", "cloud"); - @Override public void validate(VespaModel model, DeployState deployState) { if (! deployState.isHosted()) return; if (model.getAdmin().getApplicationType() != ApplicationType.DEFAULT) return; for (ContainerCluster cluster : model.getContainerClusters().values()) { - - if (cluster.getSecretStore().isPresent() ) { - var secretStore = (SecretStore) cluster.getSecretStore().get(); - - if (!isValidType(secretStore.getType())) { - throw new IllegalArgumentException("Secret store must be one of the following types: " + VALID_TYPES); - } - if (! secretStore.isCloud() && ! hasIdentityProvider(cluster)) + if (cluster.getSecretStore().isPresent() && ! hasIdentityProvider(cluster)) throw new IllegalArgumentException(String.format( "Container cluster '%s' uses a secret store, so an Athenz domain and an Athenz service" + " must be declared in deployment.xml.", cluster.getName())); - } } } @@ -49,7 +39,4 @@ public class SecretStoreValidator extends Validator { return false; } - private boolean isValidType(String type) { - return VALID_TYPES.contains(type); - } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java index 55d6c0ba6a9..a1db3c43f1c 100755 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java @@ -4,7 +4,6 @@ package com.yahoo.vespa.model.container; import com.yahoo.cloud.config.ClusterInfoConfig; import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.cloud.config.CuratorConfig; -import com.yahoo.cloud.config.SecretStoreConfig; import com.yahoo.component.ComponentId; import com.yahoo.config.application.api.ApplicationMetaData; import com.yahoo.config.docproc.DocprocConfig; @@ -102,8 +101,7 @@ public abstract class ContainerCluster<CONTAINER extends Container> DocprocConfig.Producer, ClusterInfoConfig.Producer, ConfigserverConfig.Producer, - CuratorConfig.Producer, - SecretStoreConfig.Producer + CuratorConfig.Producer { /** @@ -579,16 +577,6 @@ public abstract class ContainerCluster<CONTAINER extends Container> builder.zookeeperLocalhostAffinity(zooKeeperLocalhostAffinity); } - @Override - public void getConfig(SecretStoreConfig.Builder builder) { - secretStore.getGroups().forEach(group -> - builder.groups(new SecretStoreConfig.Groups.Builder() - .name(group.name) - .region(group.environment) - ) - ); - } - private List<ClusterInfoConfig.Services.Ports.Builder> getPorts(Service service) { List<ClusterInfoConfig.Services.Ports.Builder> builders = new ArrayList<>(); PortsMeta portsMeta = service.getPortsMeta(); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/SecretStore.java b/config-model/src/main/java/com/yahoo/vespa/model/container/SecretStore.java index 0332192c5c6..c803168af81 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/SecretStore.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/SecretStore.java @@ -11,11 +11,6 @@ import java.util.List; */ public class SecretStore { private final List<Group> groups = new ArrayList<>(); - private final String type; - - public SecretStore(String type) { - this.type = type; - } public void addGroup(String name, String environment) { groups.add(new Group(name, environment)); @@ -25,14 +20,6 @@ public class SecretStore { return ImmutableList.copyOf(groups); } - public String getType() { - return type; - } - - public boolean isCloud() { - return "cloud".equals(type); - } - public static class Group { public final String name; public final String environment; diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudSecretStore.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudSecretStore.java new file mode 100644 index 00000000000..392ab382b45 --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudSecretStore.java @@ -0,0 +1,64 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.container.xml; + +import com.yahoo.cloud.config.SecretStoreConfig; +import com.yahoo.container.bundle.BundleInstantiationSpecification; +import com.yahoo.osgi.provider.model.ComponentModel; +import com.yahoo.vespa.model.container.component.SimpleComponent; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author olaa + */ +public class CloudSecretStore extends SimpleComponent implements SecretStoreConfig.Producer { + + private static final String CLASS = "com.yahoo.jdisc.cloud.aws.AwsParameterStore"; + private static final String BUNDLE = "jdisc-cloud-aws"; + + private final List<StoreConfig> configList; + + public CloudSecretStore() { + super(new ComponentModel(BundleInstantiationSpecification.getFromStrings(CLASS, CLASS, BUNDLE))); + configList = new ArrayList<>(); + } + + public void addConfig(String name, String region, String awsId, String role, String externalId) { + configList.add( + new StoreConfig(name, region, awsId, role, externalId) + ); + } + + @Override + public void getConfig(SecretStoreConfig.Builder builder) { + builder.groups( + configList.stream() + .map(config -> new SecretStoreConfig.Groups.Builder() + .name(config.name) + .region(config.region) + .awsId(config.awsId) + .role(config.role) + .externalId(config.externalId) + ).collect(Collectors.toList()) + ); + } + + class StoreConfig { + private final String name; + private final String region; + private final String awsId; + private final String role; + private final String externalId; + + public StoreConfig(String name, String region, String awsId, String role, String externalId) { + this.name = name; + this.region = region; + this.awsId = awsId; + this.role = role; + this.externalId = externalId; + } + + } +} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index 4dc20fc668b..4efd88c7307 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -14,6 +14,7 @@ import com.yahoo.config.model.ConfigModelContext.ApplicationType; import com.yahoo.config.model.api.ConfigServerSpec; import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.EndpointCertificateSecrets; +import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.model.application.provider.IncludeDirs; import com.yahoo.config.model.builder.xml.ConfigModelBuilder; import com.yahoo.config.model.builder.xml.ConfigModelId; @@ -180,7 +181,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { DeployState deployState = context.getDeployState(); DocumentFactoryBuilder.buildDocumentFactories(cluster, spec); addConfiguredComponents(deployState, cluster, spec); - addSecretStore(cluster, spec); + addSecretStore(cluster, spec, deployState); addRestApis(deployState, spec, cluster); addServlets(deployState, spec, cluster); @@ -254,18 +255,49 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { return new SimpleComponent(new ComponentModel(idSpec, null, "zookeeper-server", configId)); } - private void addSecretStore(ApplicationContainerCluster cluster, Element spec) { + private void addSecretStore(ApplicationContainerCluster cluster, Element spec, DeployState deployState) { + Element secretStoreElement = XML.getChild(spec, "secret-store"); if (secretStoreElement != null) { - SecretStore secretStore = new SecretStore(secretStoreElement.getAttribute("type")); - String attributeName = secretStore.isCloud() ? "region" : "environment"; - for (Element group : XML.getChildren(secretStoreElement, "group")) { - secretStore.addGroup(group.getAttribute("name"), group.getAttribute(attributeName)); + String type = secretStoreElement.getAttribute("type"); + if ("cloud".equals(type)) { + addCloudSecretStore(cluster, secretStoreElement, deployState); + } else { + SecretStore secretStore = new SecretStore(); + for (Element group : XML.getChildren(secretStoreElement, "group")) { + secretStore.addGroup(group.getAttribute("name"), group.getAttribute("environment")); + } + cluster.setSecretStore(secretStore); } - cluster.setSecretStore(secretStore); } } + private void addCloudSecretStore(ApplicationContainerCluster cluster, Element secretStoreElement, DeployState deployState) { + CloudSecretStore cloudSecretStore = new CloudSecretStore(); + Map<String, TenantSecretStore> secretStoresByName = deployState.getProperties().tenantSecretStores() + .stream() + .collect(Collectors.toMap( + TenantSecretStore::getName, + store -> store + )); + + for (Element group : XML.getChildren(secretStoreElement, "group")) { + String name = group.getAttribute("name"); + String region = group.getAttribute("region"); + TenantSecretStore secretStore = secretStoresByName.get(name); + + if (secretStore == null) + throw new RuntimeException("No configured secret store named " + name); + + if (secretStore.getExternalId().isEmpty()) + throw new RuntimeException("No external ID has been set"); + + cloudSecretStore.addConfig(name, region, secretStore.getAwsId(), secretStore.getRole(), secretStore.getExternalId().get()); + } + + cluster.addComponent(cloudSecretStore); + } + private void addAthensCopperArgos(ApplicationContainerCluster cluster, ConfigModelContext context) { if ( ! context.getDeployState().isHosted()) return; app.getDeployment().map(DeploymentSpec::fromXml) 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 21ee9a6e1b1..54ff693dbf5 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 @@ -73,7 +73,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.config.model.api.TenantSecretStore; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.curator.stats.LockStats; import com.yahoo.vespa.curator.stats.ThreadLockStats; diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java index af2f91b62d9..415c73ed37f 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java @@ -25,6 +25,9 @@ import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; +import com.yahoo.config.model.api.TenantSecretStore; +import com.yahoo.container.jdisc.secretstore.SecretStore; +import com.yahoo.vespa.config.server.tenant.SecretStoreExternalIdRetriever; import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.FlagSource; import com.yahoo.vespa.flags.Flags; @@ -255,6 +258,8 @@ public class ModelContextImpl implements ModelContext { private final Optional<AthenzDomain> athenzDomain; private final Optional<ApplicationRoles> applicationRoles; private final Quota quota; + private final List<TenantSecretStore> tenantSecretStores; + private final SecretStore secretStore; private final boolean dedicatedClusterControllerCluster; private final String jvmGcOptions; @@ -270,6 +275,8 @@ public class ModelContextImpl implements ModelContext { Optional<AthenzDomain> athenzDomain, Optional<ApplicationRoles> applicationRoles, Optional<Quota> maybeQuota, + List<TenantSecretStore> tenantSecretStores, + SecretStore secretStore, boolean dedicatedClusterControllerCluster) { this.featureFlags = new FeatureFlags(flagSource, applicationId); this.applicationId = applicationId; @@ -288,6 +295,8 @@ public class ModelContextImpl implements ModelContext { this.applicationRoles = applicationRoles; this.quota = maybeQuota.orElseGet(Quota::unlimited); this.dedicatedClusterControllerCluster = dedicatedClusterControllerCluster; + this.tenantSecretStores = tenantSecretStores; + this.secretStore = secretStore; jvmGcOptions = flagValue(flagSource, applicationId, PermanentFlags.JVM_GC_OPTIONS); } @@ -344,6 +353,11 @@ public class ModelContextImpl implements ModelContext { @Override public Quota quota() { return quota; } + @Override + public List<TenantSecretStore> tenantSecretStores() { + return SecretStoreExternalIdRetriever.populateExternalId(secretStore, applicationId.tenant(), zone.system(), tenantSecretStores); + } + @Override public String jvmGCOptions() { return jvmGcOptions; } @Override public boolean dedicatedClusterControllerCluster() { return dedicatedClusterControllerCluster; } 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 index d3ce5096583..72f5747e15c 100644 --- 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 @@ -8,7 +8,7 @@ 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.config.model.api.TenantSecretStore; import com.yahoo.yolean.Exceptions; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ByteArrayEntity; 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 e4aae404919..ba2989164ee 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 @@ -29,7 +29,7 @@ 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 com.yahoo.config.model.api.TenantSecretStore; import java.io.IOException; import java.net.URLDecoder; diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java index 5a72175e42a..fd04d4b58fc 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java @@ -32,6 +32,7 @@ import com.yahoo.vespa.config.server.tenant.ApplicationRolesStore; import com.yahoo.vespa.config.server.tenant.ContainerEndpointsCache; import com.yahoo.vespa.config.server.tenant.EndpointCertificateMetadataStore; import com.yahoo.vespa.config.server.tenant.EndpointCertificateRetriever; +import com.yahoo.vespa.config.server.tenant.SecretStoreExternalIdRetriever; import com.yahoo.vespa.config.server.tenant.TenantListener; import com.yahoo.vespa.config.server.tenant.TenantRepository; import com.yahoo.vespa.curator.Curator; @@ -162,6 +163,8 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> { new ApplicationRolesStore(curator, TenantRepository.getTenantPath(tenant)) .readApplicationRoles(applicationId), zkClient.readQuota(), + zkClient.readTenantSecretStores(), + secretStore, zkClient.readDedicatedClusterControllerCluster()); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java index 1cee80038e0..fc8fbe4d7d5 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java @@ -13,9 +13,11 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.vespa.config.server.http.SessionHandler; import com.yahoo.vespa.config.server.tenant.ContainerEndpointSerializer; import com.yahoo.vespa.config.server.tenant.EndpointCertificateMetadataSerializer; +import com.yahoo.vespa.config.server.tenant.TenantSecretStoreSerializer; import java.time.Clock; import java.time.Duration; @@ -43,6 +45,7 @@ public final class PrepareParams { static final String APPLICATION_HOST_ROLE = "applicationHostRole"; static final String APPLICATION_CONTAINER_ROLE = "applicationContainerRole"; static final String QUOTA_PARAM_NAME = "quota"; + static final String TENANT_SECRET_STORES_PARAM_NAME = "tenantSecretStores"; static final String FORCE_PARAM_NAME = "force"; static final String WAIT_FOR_RESOURCES_IN_PREPARE = "waitForResourcesInPrepare"; @@ -61,14 +64,15 @@ public final class PrepareParams { private final Optional<AthenzDomain> athenzDomain; private final Optional<ApplicationRoles> applicationRoles; private final Optional<Quota> quota; + private final List<TenantSecretStore> tenantSecretStores; private PrepareParams(ApplicationId applicationId, TimeoutBudget timeoutBudget, boolean ignoreValidationErrors, boolean dryRun, boolean verbose, boolean isBootstrap, Optional<Version> vespaVersion, List<ContainerEndpoint> containerEndpoints, Optional<EndpointCertificateMetadata> endpointCertificateMetadata, Optional<DockerImage> dockerImageRepository, Optional<AthenzDomain> athenzDomain, - Optional<ApplicationRoles> applicationRoles, Optional<Quota> quota, boolean force, - boolean waitForResourcesInPrepare) { + Optional<ApplicationRoles> applicationRoles, Optional<Quota> quota, List<TenantSecretStore> tenantSecretStores, + boolean force, boolean waitForResourcesInPrepare) { this.timeoutBudget = timeoutBudget; this.applicationId = Objects.requireNonNull(applicationId); this.ignoreValidationErrors = ignoreValidationErrors; @@ -82,6 +86,7 @@ public final class PrepareParams { this.athenzDomain = athenzDomain; this.applicationRoles = applicationRoles; this.quota = quota; + this.tenantSecretStores = tenantSecretStores; this.force = force; this.waitForResourcesInPrepare = waitForResourcesInPrepare; } @@ -103,6 +108,7 @@ public final class PrepareParams { private Optional<AthenzDomain> athenzDomain = Optional.empty(); private Optional<ApplicationRoles> applicationRoles = Optional.empty(); private Optional<Quota> quota = Optional.empty(); + private List<TenantSecretStore> tenantSecretStores = List.of(); public Builder() { } @@ -198,6 +204,13 @@ public final class PrepareParams { return this; } + public Builder tenantSecretStores(String serialized) { + this.tenantSecretStores = (serialized == null) + ? List.of() + : TenantSecretStoreSerializer.listFromSlime(SlimeUtils.jsonToSlime(serialized).get()); + return this; + } + public Builder waitForResourcesInPrepare(boolean waitForResourcesInPrepare) { this.waitForResourcesInPrepare = waitForResourcesInPrepare; return this; @@ -212,7 +225,7 @@ public final class PrepareParams { return new PrepareParams(applicationId, timeoutBudget, ignoreValidationErrors, dryRun, verbose, isBootstrap, vespaVersion, containerEndpoints, endpointCertificateMetadata, dockerImageRepository, athenzDomain, - applicationRoles, quota, force, waitForResourcesInPrepare); + applicationRoles, quota, tenantSecretStores, force, waitForResourcesInPrepare); } } @@ -229,6 +242,7 @@ public final class PrepareParams { .athenzDomain(request.getProperty(ATHENZ_DOMAIN)) .applicationRoles(ApplicationRoles.fromString(request.getProperty(APPLICATION_HOST_ROLE), request.getProperty(APPLICATION_CONTAINER_ROLE))) .quota(request.getProperty(QUOTA_PARAM_NAME)) + .tenantSecretStores(request.getProperty(TENANT_SECRET_STORES_PARAM_NAME)) .force(request.getBooleanProperty(FORCE_PARAM_NAME)) .waitForResourcesInPrepare(request.getBooleanProperty(WAIT_FOR_RESOURCES_IN_PREPARE)) .build(); @@ -303,4 +317,8 @@ public final class PrepareParams { public Optional<Quota> quota() { return quota; } + + public List<TenantSecretStore> tenantSecretStores() { + return tenantSecretStores; + } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java index 5e2a4b60948..2201ce47d84 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java @@ -17,6 +17,7 @@ import com.yahoo.config.model.api.EndpointCertificateSecrets; import com.yahoo.config.model.api.FileDistribution; import com.yahoo.config.model.api.ModelContext; import com.yahoo.config.model.api.Quota; +import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.provision.AllocatedHosts; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; @@ -44,6 +45,7 @@ import com.yahoo.vespa.config.server.tenant.ContainerEndpointsCache; import com.yahoo.vespa.config.server.tenant.EndpointCertificateMetadataSerializer; import com.yahoo.vespa.config.server.tenant.EndpointCertificateMetadataStore; import com.yahoo.vespa.config.server.tenant.EndpointCertificateRetriever; +import com.yahoo.vespa.config.server.tenant.SecretStoreExternalIdRetriever; import com.yahoo.vespa.config.server.tenant.TenantRepository; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.flags.FlagSource; @@ -116,6 +118,7 @@ public class SessionPreparer { ApplicationPackage applicationPackage, SessionZooKeeperClient sessionZooKeeperClient) { ApplicationId applicationId = params.getApplicationId(); boolean dedicatedClusterControllerCluster = new ApplicationCuratorDatabase(applicationId.tenant(), curator).getDedicatedClusterControllerCluster(applicationId); + Preparation preparation = new Preparation(hostValidator, logger, params, activeApplicationSet, TenantRepository.getTenantPath(applicationId.tenant()), serverDbSessionDir, applicationPackage, sessionZooKeeperClient, @@ -204,6 +207,8 @@ public class SessionPreparer { athenzDomain, applicationRoles, params.quota(), + params.tenantSecretStores(), + secretStore, dedicatedClusterControllerCluster); this.fileDistributionProvider = fileDistributionFactory.createProvider(serverDbSessionDir); this.preparedModelsBuilder = new PreparedModelsBuilder(modelFactoryRegistry, @@ -277,6 +282,7 @@ public class SessionPreparer { prepareResult.allocatedHosts(), athenzDomain, params.quota(), + params.tenantSecretStores(), properties.dedicatedClusterControllerCluster()); checkTimeout("write state to zookeeper"); } @@ -327,6 +333,7 @@ public class SessionPreparer { AllocatedHosts allocatedHosts, Optional<AthenzDomain> athenzDomain, Optional<Quota> quota, + List<TenantSecretStore> tenantSecretStores, boolean dedicatedClusterControllerCluster) { ZooKeeperDeployer zkDeployer = zooKeeperClient.createDeployer(deployLogger); try { @@ -338,6 +345,7 @@ public class SessionPreparer { zooKeeperClient.writeDockerImageRepository(dockerImageRepository); zooKeeperClient.writeAthenzDomain(athenzDomain); zooKeeperClient.writeQuota(quota); + zooKeeperClient.writeTenantSecretStores(tenantSecretStores); if (dedicatedClusterControllerCluster) zooKeeperClient.writeDedicatedClusterControllerCluster(); } catch (RuntimeException | IOException e) { diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java index 197964c2677..0757267ab0d 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java @@ -8,6 +8,7 @@ import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.application.api.DeployLogger; import com.yahoo.config.model.api.ConfigDefinitionRepo; import com.yahoo.config.model.api.Quota; +import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.provision.AllocatedHosts; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; @@ -21,6 +22,7 @@ import com.yahoo.vespa.config.server.UserConfigDefinitionRepo; import com.yahoo.vespa.config.server.deploy.ZooKeeperClient; import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer; import com.yahoo.vespa.config.server.tenant.TenantRepository; +import com.yahoo.vespa.config.server.tenant.TenantSecretStoreSerializer; import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage; import com.yahoo.vespa.curator.Curator; @@ -28,6 +30,7 @@ import com.yahoo.vespa.curator.transaction.CuratorOperations; import com.yahoo.vespa.curator.transaction.CuratorTransaction; import java.time.Instant; +import java.util.List; import java.util.Optional; import java.util.logging.Level; @@ -52,6 +55,7 @@ public class SessionZooKeeperClient { private static final String DOCKER_IMAGE_REPOSITORY_PATH = "dockerImageRepository"; private static final String ATHENZ_DOMAIN = "athenzDomain"; private static final String QUOTA_PATH = "quota"; + private static final String TENANT_SECRET_STORES_PATH = "tenantSecretStores"; // Whether the deployment of this particular session should use a dedicated CCC. // The one in ApplicationCuratorDatabase signals what all future preparations should use, i.e., here. @@ -193,6 +197,10 @@ public class SessionZooKeeperClient { return sessionPath.append(QUOTA_PATH).getAbsolute(); } + private String tenantSecretStorePath() { + return sessionPath.append(TENANT_SECRET_STORES_PATH).getAbsolute(); + } + private String dedicatedClusterControllerClusterPath() { return sessionPath.append(DEDICATED_CLUSTER_CONTROLLER_CLUSTER_PATH).getAbsolute(); } @@ -272,6 +280,22 @@ public class SessionZooKeeperClient { .map(slime -> Quota.fromSlime(slime.get())); } + public void writeTenantSecretStores(List<TenantSecretStore> tenantSecretStores) { + if (!tenantSecretStores.isEmpty()) { + var bytes = uncheck(() -> SlimeUtils.toJsonBytes(TenantSecretStoreSerializer.toSlime(tenantSecretStores))); + configCurator.putData(tenantSecretStorePath(), bytes); + } + + } + + public List<TenantSecretStore> readTenantSecretStores() { + if ( ! configCurator.exists(tenantSecretStorePath())) return List.of(); + return Optional.ofNullable(configCurator.getData(tenantSecretStorePath())) + .map(SlimeUtils::jsonToSlime) + .map(slime -> TenantSecretStoreSerializer.listFromSlime(slime.get())) + .orElse(List.of()); + } + public void writeDedicatedClusterControllerCluster() { configCurator.createNode(dedicatedClusterControllerClusterPath()); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/SecretStoreExternalIdRetriever.java b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/SecretStoreExternalIdRetriever.java new file mode 100644 index 00000000000..86eb2590ae6 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/SecretStoreExternalIdRetriever.java @@ -0,0 +1,40 @@ +// 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.tenant; + +import com.yahoo.config.model.api.TenantSecretStore; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.secretstore.SecretStore; + +import java.util.List; + +/** + * @author olaa + */ +public class SecretStoreExternalIdRetriever { + + private static final String SECRET_NAME_FORMAT = "%s.external.id.%s.%s"; + + public static List<TenantSecretStore> populateExternalId(SecretStore secretStore, TenantName tenant, SystemName system, List<TenantSecretStore> tenantSecretStores) { + tenantSecretStores.forEach(tenantSecretStore -> { + var secretName = secretName(tenant, system, tenantSecretStore.getName()); + tenantSecretStore.setExternalId(secretStore.getSecret(secretName)); + }); + return tenantSecretStores; + } + + private static String secretName(TenantName tenant, SystemName system, String storeName) { + return String.format(SECRET_NAME_FORMAT, tenantSecretGroup(system), tenant.value(), storeName); + } + + private static String tenantSecretGroup(SystemName system) { + switch (system) { + case Public: + return "vespa.external.tenant.secrets"; + case PublicCd: + return "vespa.external.cd.tenant.secrets"; + default: + throw new IllegalArgumentException("No tenant secret store key group defined for system " + system); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TenantSecretStoreSerializer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TenantSecretStoreSerializer.java new file mode 100644 index 00000000000..f55a29efe01 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TenantSecretStoreSerializer.java @@ -0,0 +1,59 @@ +// 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.tenant; + +import com.yahoo.config.model.api.TenantSecretStore; +import com.yahoo.slime.ArrayTraverser; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Inspector; +import com.yahoo.slime.Slime; +import com.yahoo.slime.Type; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author olaa + */ +public class TenantSecretStoreSerializer { + + // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one + // (and rewrite all nodes on startup), changes to the serialized format must be made + // such that what is serialized on version N+1 can be read by version N: + // - ADDING FIELDS: Always ok + // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version. + // - CHANGING THE FORMAT OF A FIELD: Don't do it bro. + + private final static String awsIdField = "awsId"; + private final static String roleField = "role"; + private final static String nameField = "name"; + + public static Slime toSlime(List<TenantSecretStore> tenantSecretStores) { + Slime slime = new Slime(); + Cursor cursor = slime.setArray(); + tenantSecretStores.forEach(tenantSecretStore -> toSlime(tenantSecretStore, cursor.addObject())); + return slime; + } + + public static void toSlime(TenantSecretStore tenantSecretStore, Cursor object) { + object.setString(awsIdField, tenantSecretStore.getAwsId()); + object.setString(nameField, tenantSecretStore.getName()); + object.setString(roleField, tenantSecretStore.getRole()); + } + + public static TenantSecretStore fromSlime(Inspector inspector) { + if (inspector.type() == Type.OBJECT) { + return new TenantSecretStore( + inspector.field(nameField).asString(), + inspector.field(awsIdField).asString(), + inspector.field(roleField).asString() + ); + } + throw new IllegalArgumentException("Unknown format encountered for endpoint certificate metadata!"); + } + + public static List<TenantSecretStore> listFromSlime(Inspector inspector) { + List<TenantSecretStore> tenantSecretStores = new ArrayList<>(); + inspector.traverse(((ArrayTraverser)(idx, store) -> tenantSecretStores.add(fromSlime(store)))); + return tenantSecretStores; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockSecretStoreValidator.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockSecretStoreValidator.java index a46cfc4fef0..c464af404d9 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/MockSecretStoreValidator.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockSecretStoreValidator.java @@ -5,7 +5,7 @@ import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.SecretStoreProvider; import com.yahoo.restapi.StringResponse; import com.yahoo.vespa.config.server.application.Application; -import com.yahoo.vespa.config.server.application.TenantSecretStore; +import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.vespa.config.server.http.SecretStoreValidator; /** diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java index 718c001d930..0d066f748c8 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java @@ -14,6 +14,7 @@ import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.test.MockApplicationPackage; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.SecretStoreProvider; import com.yahoo.vespa.config.server.deploy.ModelContextImpl; import com.yahoo.vespa.flags.InMemoryFlagSource; import org.junit.Test; @@ -72,6 +73,8 @@ public class ModelContextImplTest { Optional.empty(), Optional.empty(), Optional.empty(), + List.of(), + new SecretStoreProvider().get(), false), Optional.empty(), Optional.empty(), diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SecretStoreValidatorTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SecretStoreValidatorTest.java index affab6ab835..d308cd72b55 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SecretStoreValidatorTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SecretStoreValidatorTest.java @@ -6,7 +6,7 @@ import com.yahoo.config.model.api.Model; import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.vespa.config.server.application.Application; -import com.yahoo.vespa.config.server.application.TenantSecretStore; +import com.yahoo.config.model.api.TenantSecretStore; import org.junit.Rule; import org.junit.Test; diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java index ac1a5547646..f977011efbc 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.config.server.session; import com.yahoo.config.FileReference; import com.yahoo.config.model.api.Quota; +import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.TenantName; import com.yahoo.path.Path; @@ -18,6 +19,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import java.time.Instant; +import java.util.List; import java.util.Optional; import static org.hamcrest.core.Is.is; @@ -148,6 +150,17 @@ public class SessionZooKeeperClientTest { assertEquals(quota, zkc.readQuota()); } + @Test + public void require_tenant_secret_stores_written_and_parsed() { + var secretStores = List.of( + new TenantSecretStore("name1", "awsId1", "role1"), + new TenantSecretStore("name2", "awsId2", "role2") + ); + var zkc = createSessionZKClient(4); + zkc.writeTenantSecretStores(secretStores); + assertEquals(secretStores, zkc.readTenantSecretStores()); + } + private void assertApplicationIdParse(long sessionId, String idString, String expectedIdString) { SessionZooKeeperClient zkc = createSessionZKClient(sessionId); String path = sessionPath(sessionId).append(SessionZooKeeperClient.APPLICATION_ID_PATH).getAbsolute(); diff --git a/container-disc/src/main/resources/configdefinitions/cloud.config.secret-store.def b/container-disc/src/main/resources/configdefinitions/cloud.config.secret-store.def index 04408441889..9e4789a8597 100644 --- a/container-disc/src/main/resources/configdefinitions/cloud.config.secret-store.def +++ b/container-disc/src/main/resources/configdefinitions/cloud.config.secret-store.def @@ -2,4 +2,7 @@ namespace=cloud.config groups[].name string -groups[].region string
\ No newline at end of file +groups[].region string +groups[].awsId string +groups[].role string +groups[].externalId string
\ No newline at end of file diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java index 702c6bd0f41..4a577632746 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java @@ -9,7 +9,9 @@ import com.yahoo.vespa.hosted.controller.api.integration.aws.TenantRoles; import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint; +import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -32,6 +34,7 @@ public class DeploymentData { private final Optional<AthenzDomain> athenzDomain; private final Optional<TenantRoles> tenantRoles; private final Quota quota; + private final List<TenantSecretStore> tenantSecretStores; public DeploymentData(ApplicationId instance, ZoneId zone, byte[] applicationPackage, Version platform, Set<ContainerEndpoint> containerEndpoints, @@ -39,7 +42,8 @@ public class DeploymentData { Optional<DockerImage> dockerImageRepo, Optional<AthenzDomain> athenzDomain, Optional<TenantRoles> tenantRoles, - Quota quota) { + Quota quota, + List<TenantSecretStore> tenantSecretStores) { this.instance = requireNonNull(instance); this.zone = requireNonNull(zone); this.applicationPackage = requireNonNull(applicationPackage); @@ -50,6 +54,7 @@ public class DeploymentData { this.athenzDomain = athenzDomain; this.tenantRoles = tenantRoles; this.quota = quota; + this.tenantSecretStores = tenantSecretStores; } public ApplicationId instance() { @@ -92,4 +97,8 @@ public class DeploymentData { return quota; } + public List<TenantSecretStore> tenantSecretStores() { + return tenantSecretStores; + } + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index c966a923139..0a1af8817d7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -47,6 +47,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId; 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.ApplicationPackageValidator; import com.yahoo.vespa.hosted.controller.application.Deployment; @@ -67,6 +68,7 @@ import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.security.AccessControl; import com.yahoo.vespa.hosted.controller.security.Credentials; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; import com.yahoo.yolean.Exceptions; @@ -594,10 +596,16 @@ public class ApplicationController { Quota deploymentQuota = DeploymentQuotaCalculator.calculate(billingController.getQuota(application.tenant()), asList(application.tenant()), application, zone, applicationPackage.deploymentSpec()); + List<TenantSecretStore> tenantSecretStores = controller.tenants() + .get(application.tenant()) + .filter(tenant-> tenant instanceof CloudTenant) + .map(tenant -> ((CloudTenant) tenant).tenantSecretStores()) + .orElse(List.of()); + ConfigServer.PreparedApplication preparedApplication = configServer.deploy(new DeploymentData(application, zone, applicationPackage.zippedContent(), platform, endpoints, endpointCertificateMetadata, dockerImageRepo, domain, - tenantRoles, deploymentQuota)); + tenantRoles, deploymentQuota, tenantSecretStores)); return new ActivateResult(new RevisionId(applicationPackage.hash()), preparedApplication.prepareResponse(), applicationPackage.zippedContent().length); diff --git a/jdisc-cloud-aws/pom.xml b/jdisc-cloud-aws/pom.xml index dcedab50a64..bbb31d380ff 100644 --- a/jdisc-cloud-aws/pom.xml +++ b/jdisc-cloud-aws/pom.xml @@ -49,6 +49,12 @@ <version>${project.version}</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>component</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> </dependencies> <build> diff --git a/jdisc-cloud-aws/src/main/java/com/yahoo/jdisc/cloud/aws/AwsParameterStore.java b/jdisc-cloud-aws/src/main/java/com/yahoo/jdisc/cloud/aws/AwsParameterStore.java index 1636c6aeb6d..e23dc54f4c6 100644 --- a/jdisc-cloud-aws/src/main/java/com/yahoo/jdisc/cloud/aws/AwsParameterStore.java +++ b/jdisc-cloud-aws/src/main/java/com/yahoo/jdisc/cloud/aws/AwsParameterStore.java @@ -9,55 +9,56 @@ import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClient; import com.amazonaws.services.simplesystemsmanagement.model.GetParametersRequest; import com.amazonaws.services.simplesystemsmanagement.model.GetParametersResult; +import com.google.inject.Inject; import com.yahoo.cloud.config.SecretStoreConfig; +import com.yahoo.component.AbstractComponent; import com.yahoo.container.jdisc.secretstore.SecretNotFoundException; import com.yahoo.container.jdisc.secretstore.SecretStore; /** * @author mortent */ -public class AwsParameterStore implements SecretStore { +public class AwsParameterStore extends AbstractComponent implements SecretStore { private final VespaAwsCredentialsProvider credentialsProvider; - private final String roleToAssume; - private final String externalId; - private final String region; + private final SecretStoreConfig secretStoreConfig; - AwsParameterStore(VespaAwsCredentialsProvider credentialsProvider, String roleToAssume, String externalId, String region) { - this.credentialsProvider = credentialsProvider; - this.roleToAssume = roleToAssume; - this.externalId = externalId; - this.region = region; + @Inject + public AwsParameterStore(SecretStoreConfig secretStoreConfig) { + this.secretStoreConfig = secretStoreConfig; + this.credentialsProvider = new VespaAwsCredentialsProvider(); } @Override public String getSecret(String key) { - AWSSecurityTokenService tokenService = AWSSecurityTokenServiceClientBuilder - .standard() - .withRegion(region) - .withCredentials(credentialsProvider) - .build(); + for (var group : secretStoreConfig.groups()) { + AWSSecurityTokenService tokenService = AWSSecurityTokenServiceClientBuilder + .standard() + .withRegion(group.region()) + .withCredentials(credentialsProvider) + .build(); - STSAssumeRoleSessionCredentialsProvider assumeExtAccountRole = new STSAssumeRoleSessionCredentialsProvider - .Builder(roleToAssume, "vespa") - .withExternalId(externalId) - .withStsClient(tokenService) - .build(); + STSAssumeRoleSessionCredentialsProvider assumeExtAccountRole = new STSAssumeRoleSessionCredentialsProvider + .Builder(toRoleArn(group.awsId(), group.role()), "vespa") + .withExternalId(group.externalId()) + .withStsClient(tokenService) + .build(); - AWSSimpleSystemsManagement client = AWSSimpleSystemsManagementClient.builder() - .withCredentials(assumeExtAccountRole) - .withRegion(region) - .build(); + AWSSimpleSystemsManagement client = AWSSimpleSystemsManagementClient.builder() + .withCredentials(assumeExtAccountRole) + .withRegion(group.region()) + .build(); - GetParametersRequest parametersRequest = new GetParametersRequest().withNames(key).withWithDecryption(true); - GetParametersResult parameters = client.getParameters(parametersRequest); - int count = parameters.getParameters().size(); - if (count < 1) { - throw new SecretNotFoundException("Could not find secret " + key + " using role " + roleToAssume); - } else if (count > 1) { - throw new RuntimeException("Found too many parameters, expected 1, but found " + count); + GetParametersRequest parametersRequest = new GetParametersRequest().withNames(key).withWithDecryption(true); + GetParametersResult parameters = client.getParameters(parametersRequest); + int count = parameters.getParameters().size(); + if (count == 1) { + return parameters.getParameters().get(0).getValue(); + } else if (count > 1) { + throw new RuntimeException("Found too many parameters, expected 1, but found " + count); + } } - return parameters.getParameters().get(0).getValue(); + throw new SecretNotFoundException("Could not find secret " + key + " in any configured secret store"); } @Override @@ -65,4 +66,8 @@ public class AwsParameterStore implements SecretStore { // TODO return getSecret(key); } + + private String toRoleArn(String awsId, String role) { + return "arn:aws:iam::" + awsId + ":role/" + role; + } } diff --git a/jdisc-cloud-aws/src/main/java/com/yahoo/jdisc/cloud/aws/AwsParameterStoreValidationHandler.java b/jdisc-cloud-aws/src/main/java/com/yahoo/jdisc/cloud/aws/AwsParameterStoreValidationHandler.java index 5d5cad2f75d..8b6e3d52d37 100644 --- a/jdisc-cloud-aws/src/main/java/com/yahoo/jdisc/cloud/aws/AwsParameterStoreValidationHandler.java +++ b/jdisc-cloud-aws/src/main/java/com/yahoo/jdisc/cloud/aws/AwsParameterStoreValidationHandler.java @@ -28,22 +28,14 @@ import java.util.logging.Logger; public class AwsParameterStoreValidationHandler extends LoggingRequestHandler { private static final Logger log = Logger.getLogger(AwsParameterStoreValidationHandler.class.getName()); - private final VespaAwsCredentialsProvider credentialsProvider; - private final SecretStoreConfig secretStoreConfig; + private final AwsParameterStore awsParameterStore; @Inject - public AwsParameterStoreValidationHandler(Context ctx, SecretStoreConfig secretStoreConfig) { - this(ctx, secretStoreConfig, new VespaAwsCredentialsProvider()); - } - - - public AwsParameterStoreValidationHandler(Context ctx, SecretStoreConfig secretStoreConfig, VespaAwsCredentialsProvider credentialsProvider) { + public AwsParameterStoreValidationHandler(Context ctx, AwsParameterStore awsParameterStore) { super(ctx); - this.credentialsProvider = credentialsProvider; - this.secretStoreConfig = secretStoreConfig; + this.awsParameterStore = awsParameterStore; } - @Override public HttpResponse handle(HttpRequest request) { try { @@ -66,10 +58,7 @@ public class AwsParameterStoreValidationHandler extends LoggingRequestHandler { settings.toSlime(root.setObject("settings")); try { - var arn = "arn:aws:iam::" + settings.awsId + ":role/" + settings.role; - var region = getRegion(settings); - var store = new AwsParameterStore(this.credentialsProvider, arn, settings.externalId, region); - store.getSecret("vespa-secret"); + awsParameterStore.getSecret("vespa-secret"); root.setString("status", "ok"); } catch (RuntimeException e) { root.setString("status", "error"); @@ -90,15 +79,6 @@ public class AwsParameterStoreValidationHandler extends LoggingRequestHandler { } } - private String getRegion(AwsSettings settings) { - return secretStoreConfig.groups() - .stream() - .filter(group -> group.name().equals(settings.name)) - .map(SecretStoreConfig.Groups::region) - .findFirst() - .orElseThrow(() -> new RuntimeException("No secret store named '" + settings.name + "' configured in services.xml")); - } - private static class AwsSettings { String name; String role; |