From 5bd8b94ecaadfc2a4d0830361b291533390a548b Mon Sep 17 00:00:00 2001 From: matejnedic Date: Thu, 23 Oct 2025 22:25:48 +0200 Subject: [PATCH 1/4] 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; + } + } From 3d10c98103e7a2ffa4b815f8e7b59b9f61636fa0 Mon Sep 17 00:00:00 2001 From: matejnedic Date: Sat, 22 Nov 2025 02:06:43 +0100 Subject: [PATCH 2/4] Amend and prepare for release --- docs/src/main/asciidoc/parameter-store.adoc | 15 +++--- ...StoreConfigDataLoaderIntegrationTests.java | 26 +++++++++- .../ParameterStorePropertySource.java | 50 ++++++++++++++----- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/docs/src/main/asciidoc/parameter-store.adoc b/docs/src/main/asciidoc/parameter-store.adoc index 0fc85e032..769e951e7 100644 --- a/docs/src/main/asciidoc/parameter-store.adoc +++ b/docs/src/main/asciidoc/parameter-store.adoc @@ -88,24 +88,27 @@ 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: +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 ?extension= suffix: [source,properties] ---- -spring.config.import=aws-parameterstore:/config/my-datasource/?properties +spring.config.import=aws-parameterstore:/config/my-datasource/?extension=properties ---- +NOTE: Supported ?extension= types are `properties`, `json` and `yaml`. When any other format is specified exception will be raised and application will fail to start! + + 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 +my.properties.sqs.queue_name=random_name +my.properties.dynamodb.table_name=random_table_name ---- Spring Cloud AWS will automatically load these as: -Key: `spring.cloud.aws.region`, Value: `eu-central-1` -Key: `spring.cloud.aws.endpoint`, Value: `randomEndpoint` +Key: `my.properties.sqs.queue_name`, Value: `random_name` +Key: `my.properties.dynamodb.table_name`, Value: `random_table_name` NOTE: Standard Parameter Store parameters are limited to 4 KB of data. 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 38063d901..4dfbe84e7 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 @@ -135,7 +135,7 @@ void resolvesPropertiesWithPrefixProperties() { putParameter(localstack, "/test/path/secondMessage", applicationProperties, REGION); try (ConfigurableApplicationContext context = runApplication(application, - "aws-parameterstore:/test/path/?properties")) { + "aws-parameterstore:/test/path/?extension=properties")) { assertThat(context.getEnvironment().getProperty("first.message")).isEqualTo("value from tests"); assertThat(context.getEnvironment().getProperty("first.another-parameter")) .isEqualTo("another parameter value"); @@ -145,6 +145,30 @@ void resolvesPropertiesWithPrefixProperties() { } } + @Test + void resolvesPropertiesWithPrefixPropertiesYaml() { + SpringApplication application = new SpringApplication(App.class); + application.setWebApplicationType(WebApplicationType.NONE); + String applicationProperties = """ + first: + message: value from tests + 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/?extension=yaml")) { + 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 40e15a393..05264b19b 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,13 +16,16 @@ package io.awspring.cloud.parameterstore; import io.awspring.cloud.core.config.AwsPropertySource; + +import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Properties; +import java.util.*; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; import org.springframework.lang.Nullable; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest; @@ -45,7 +48,7 @@ public class ParameterStorePropertySource extends AwsPropertySource ALLOWED_TYPES = Set.of("properties", "json", "yaml"); + private final Map properties = new LinkedHashMap<>(); public ParameterStorePropertySource(String context, SsmClient ssmClient) { @@ -95,11 +102,15 @@ private void getParameters(GetParametersByPathRequest paramsRequest) { GetParametersByPathResponse paramsResult = this.source.getParametersByPath(paramsRequest); for (Parameter parameter : paramsResult.parameters()) { 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()); - } - }); + Properties props; + if (prefixType.equals("properties")) { + props = readProperties(parameter.value()); + } else { + props = readYaml(parameter.value()); + } + for (Map.Entry entry : props.entrySet()) { + properties.put(String.valueOf(entry.getKey()), entry.getValue()); + } } else { String key = parameter.name().replace(this.parameterPath, "").replace('/', '.').replaceAll("_(\\d)_", @@ -144,14 +155,21 @@ private String resolveParameterPath(String context) { prefixIndex = context.indexOf(PREFIX_PROPERTIES_LOAD); if (prefixIndex != -1) { this.propertiesType = true; + String extracted = context.substring(prefixIndex + PREFIX_PROPERTIES_LOAD.length()).toLowerCase(); + if (ALLOWED_TYPES.contains(extracted)) { + this.prefixType = extracted; + } else { + throw new IllegalArgumentException("Invalid prefixType: " + extracted + ". Must be one of properties, json, or yaml."); + } return context.substring(0, prefixIndex); } return context; } - private Properties readProperties(InputStream inputStream) { + private Properties readProperties(String input) { Properties properties = new Properties(); - try (InputStream in = inputStream) { + + try (InputStream in = new ByteArrayInputStream(input.getBytes())) { properties.load(in); } catch (Exception e) { @@ -160,4 +178,12 @@ private Properties readProperties(InputStream inputStream) { return properties; } + private Properties readYaml(String input) { + YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); + yaml.setResources( + new ByteArrayResource(input.getBytes()) + ); + return yaml.getObject(); + } + } From a76e3115c8230da1075db05d3e97e2d7ee0849fd Mon Sep 17 00:00:00 2001 From: matejnedic Date: Sat, 22 Nov 2025 02:14:51 +0100 Subject: [PATCH 3/4] Fix indent --- ...rStoreConfigDataLoaderIntegrationTests.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 4dfbe84e7..a3f9371ee 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 @@ -146,17 +146,17 @@ void resolvesPropertiesWithPrefixProperties() { } @Test - void resolvesPropertiesWithPrefixPropertiesYaml() { + void resolvesPropertiesWithPrefixYaml() { SpringApplication application = new SpringApplication(App.class); application.setWebApplicationType(WebApplicationType.NONE); - String applicationProperties = """ - first: - message: value from tests - another-parameter: another parameter value - second: - secondMessage: second value from tests - """; - putParameter(localstack, "/test/path/secondMessage", applicationProperties, REGION); + String applicationYaml = """ +first: + message: value from tests + another-parameter: another parameter value +second: + secondMessage: second value from tests +"""; + putParameter(localstack, "/test/path/secondMessage", applicationYaml, REGION); try (ConfigurableApplicationContext context = runApplication(application, "aws-parameterstore:/test/path/?extension=yaml")) { From 8670b8a1db49cc452fb07c71127b1a318910829a Mon Sep 17 00:00:00 2001 From: matejnedic Date: Sat, 22 Nov 2025 02:16:03 +0100 Subject: [PATCH 4/4] Fix indent --- .../ParameterStoreConfigDataLoaderIntegrationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a3f9371ee..a0da96ce8 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 @@ -156,10 +156,10 @@ void resolvesPropertiesWithPrefixYaml() { second: secondMessage: second value from tests """; - putParameter(localstack, "/test/path/secondMessage", applicationYaml, REGION); + putParameter(localstack, "/test/second/message", applicationYaml, REGION); try (ConfigurableApplicationContext context = runApplication(application, - "aws-parameterstore:/test/path/?extension=yaml")) { + "aws-parameterstore:/test/second/?extension=yaml")) { assertThat(context.getEnvironment().getProperty("first.message")).isEqualTo("value from tests"); assertThat(context.getEnvironment().getProperty("first.another-parameter")) .isEqualTo("another parameter value");