From 5bd8b94ecaadfc2a4d0830361b291533390a548b Mon Sep 17 00:00:00 2001 From: matejnedic Date: Thu, 23 Oct 2025 22:25:48 +0200 Subject: [PATCH] Introduce loading parameters as key value store --- docs/src/main/asciidoc/parameter-store.adoc | 24 ++++++++++ ...StoreConfigDataLoaderIntegrationTests.java | 22 +++++++++ .../ParameterStorePropertySource.java | 46 ++++++++++++++++--- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/parameter-store.adoc b/docs/src/main/asciidoc/parameter-store.adoc index ea13bc513..0fc85e032 100644 --- a/docs/src/main/asciidoc/parameter-store.adoc +++ b/docs/src/main/asciidoc/parameter-store.adoc @@ -88,6 +88,30 @@ With such config, properties `spring.datasource.url` and `spring.datasource.user NOTE: Prefixes are added as-is to all property names returned by Parameter Store. If you want key names to be separated with a dot between the prefix and key name, make sure to add a trailing dot to the prefix. +Sometimes it is useful to group multiple properties in a text-based format, similar to application.properties. With Spring Cloud AWS, you can load a Parameter Store parameter as a text-based key/value configuration by using the spring.config.import property with the ?properties suffix: + +[source,properties] +---- +spring.config.import=aws-parameterstore:/config/my-datasource/?properties +---- + +All parameters stored under this path will be interpreted as key/value pairs. For example, if the value of a parameter is: + +[source,properties] +---- +spring.cloud.aws.region=eu-central-1 +spring.cloud.aws.endpoint=randomEndpoint +---- + +Spring Cloud AWS will automatically load these as: +Key: `spring.cloud.aws.region`, Value: `eu-central-1` +Key: `spring.cloud.aws.endpoint`, Value: `randomEndpoint` + +NOTE: Standard Parameter Store parameters are limited to 4 KB of data. + +This approach allows you to maintain multiple related properties in a single parameter, making configuration management simpler and more organized. + + === Using SsmClient The starter automatically configures and registers a `SsmClient` bean in the Spring application context. The `SsmClient` bean can be used to create or retrieve parameters from Parameter Store. diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/config/parameterstore/ParameterStoreConfigDataLoaderIntegrationTests.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/config/parameterstore/ParameterStoreConfigDataLoaderIntegrationTests.java index 373cf997b..38063d901 100644 --- a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/config/parameterstore/ParameterStoreConfigDataLoaderIntegrationTests.java +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/config/parameterstore/ParameterStoreConfigDataLoaderIntegrationTests.java @@ -123,6 +123,28 @@ void resolvesPropertiesWithPrefixes() { } } + @Test + void resolvesPropertiesWithPrefixProperties() { + SpringApplication application = new SpringApplication(App.class); + application.setWebApplicationType(WebApplicationType.NONE); + String applicationProperties = """ + first.message=value from tests + first.another-parameter=another parameter value + second.secondMessage=second value from tests + """; + putParameter(localstack, "/test/path/secondMessage", applicationProperties, REGION); + + try (ConfigurableApplicationContext context = runApplication(application, + "aws-parameterstore:/test/path/?properties")) { + assertThat(context.getEnvironment().getProperty("first.message")).isEqualTo("value from tests"); + assertThat(context.getEnvironment().getProperty("first.another-parameter")) + .isEqualTo("another parameter value"); + assertThat(context.getEnvironment().getProperty("second.secondMessage")) + .isEqualTo("second value from tests"); + assertThat(context.getEnvironment().getProperty("non-existing-parameter")).isNull(); + } + } + @Test void clientIsConfiguredWithCustomizerProvidedToBootstrapRegistry() { SpringApplication application = new SpringApplication(App.class); diff --git a/spring-cloud-aws-parameter-store/src/main/java/io/awspring/cloud/parameterstore/ParameterStorePropertySource.java b/spring-cloud-aws-parameter-store/src/main/java/io/awspring/cloud/parameterstore/ParameterStorePropertySource.java index fda3321d1..40e15a393 100644 --- a/spring-cloud-aws-parameter-store/src/main/java/io/awspring/cloud/parameterstore/ParameterStorePropertySource.java +++ b/spring-cloud-aws-parameter-store/src/main/java/io/awspring/cloud/parameterstore/ParameterStorePropertySource.java @@ -16,8 +16,11 @@ package io.awspring.cloud.parameterstore; import io.awspring.cloud.core.config.AwsPropertySource; +import java.io.InputStream; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; @@ -41,6 +44,8 @@ public class ParameterStorePropertySource extends AwsPropertySource properties = new LinkedHashMap<>(); public ParameterStorePropertySource(String context, SsmClient ssmClient) { @@ -87,11 +94,20 @@ public Object getProperty(String name) { private void getParameters(GetParametersByPathRequest paramsRequest) { GetParametersByPathResponse paramsResult = this.source.getParametersByPath(paramsRequest); for (Parameter parameter : paramsResult.parameters()) { - String key = parameter.name().replace(this.parameterPath, "").replace('/', '.').replaceAll("_(\\d)_", - "[$1]"); - LOG.debug("Populating property retrieved from AWS Parameter Store: " + key); - String propertyKey = prefix != null ? prefix + key : key; - this.properties.put(propertyKey, parameter.value()); + if (propertiesType) { + Arrays.stream(parameter.value().split("\\n")).map(line -> line.split("=", 2)).forEach(keyValue -> { + if (keyValue.length == 2) { + this.properties.put(keyValue[0].trim(), keyValue[1].trim()); + } + }); + } + else { + String key = parameter.name().replace(this.parameterPath, "").replace('/', '.').replaceAll("_(\\d)_", + "[$1]"); + LOG.debug("Populating property retrieved from AWS Parameter Store: " + key); + String propertyKey = prefix != null ? prefix + key : key; + this.properties.put(propertyKey, parameter.value()); + } } if (paramsResult.nextToken() != null) { getParameters(paramsRequest.toBuilder().nextToken(paramsResult.nextToken()).build()); @@ -112,7 +128,7 @@ String getParameterPath() { } @Nullable - private static String resolvePrefix(String context) { + private String resolvePrefix(String context) { int prefixIndex = context.indexOf(PREFIX_PART); if (prefixIndex != -1) { return context.substring(prefixIndex + PREFIX_PART.length()); @@ -120,12 +136,28 @@ private static String resolvePrefix(String context) { return null; } - private static String resolveParameterPath(String context) { + private String resolveParameterPath(String context) { int prefixIndex = context.indexOf(PREFIX_PART); if (prefixIndex != -1) { return context.substring(0, prefixIndex); } + prefixIndex = context.indexOf(PREFIX_PROPERTIES_LOAD); + if (prefixIndex != -1) { + this.propertiesType = true; + return context.substring(0, prefixIndex); + } return context; } + private Properties readProperties(InputStream inputStream) { + Properties properties = new Properties(); + try (InputStream in = inputStream) { + properties.load(in); + } + catch (Exception e) { + throw new IllegalStateException("Cannot load environment", e); + } + return properties; + } + }