Skip to content

Commit f5cc267

Browse files
authored
Introduce loading parameters as key value store (#1495)
* Introduce loading parameters as key value store * Amend and prepare for release * Fix indent * Fix indent * update
1 parent 087ae3b commit f5cc267

File tree

3 files changed

+142
-10
lines changed

3 files changed

+142
-10
lines changed

docs/src/main/asciidoc/parameter-store.adoc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,33 @@ With such config, properties `spring.datasource.url` and `spring.datasource.user
8888
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.
8989

9090

91+
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:
92+
93+
[source,properties]
94+
----
95+
spring.config.import=aws-parameterstore:/config/my-datasource/?extension=properties
96+
----
97+
98+
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!
99+
100+
101+
All parameters stored under this path will be interpreted as key/value pairs. For example, if the value of a parameter is:
102+
103+
[source,properties]
104+
----
105+
my.properties.sqs.queue_name=random_name
106+
my.properties.dynamodb.table_name=random_table_name
107+
----
108+
109+
Spring Cloud AWS will automatically load these as:
110+
Key: `my.properties.sqs.queue_name`, Value: `random_name`
111+
Key: `my.properties.dynamodb.table_name`, Value: `random_table_name`
112+
113+
NOTE: Standard Parameter Store parameters are limited to 4 KB of data.
114+
115+
This approach allows you to maintain multiple related properties in a single parameter, making configuration management simpler and more organized.
116+
117+
91118
=== Using SsmClient
92119

93120
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.

spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/config/parameterstore/ParameterStoreConfigDataLoaderIntegrationTests.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,52 @@ void resolvesPropertiesWithPrefixes() {
123123
}
124124
}
125125

126+
@Test
127+
void resolvesPropertiesWithPrefixProperties() {
128+
SpringApplication application = new SpringApplication(App.class);
129+
application.setWebApplicationType(WebApplicationType.NONE);
130+
String applicationProperties = """
131+
first.message=value from tests
132+
first.another-parameter=another parameter value
133+
second.secondMessage=second value from tests
134+
""";
135+
putParameter(localstack, "/test/path/secondMessage", applicationProperties, REGION);
136+
137+
try (ConfigurableApplicationContext context = runApplication(application,
138+
"aws-parameterstore:/test/path/?extension=properties")) {
139+
assertThat(context.getEnvironment().getProperty("first.message")).isEqualTo("value from tests");
140+
assertThat(context.getEnvironment().getProperty("first.another-parameter"))
141+
.isEqualTo("another parameter value");
142+
assertThat(context.getEnvironment().getProperty("second.secondMessage"))
143+
.isEqualTo("second value from tests");
144+
assertThat(context.getEnvironment().getProperty("non-existing-parameter")).isNull();
145+
}
146+
}
147+
148+
@Test
149+
void resolvesPropertiesWithPrefixYaml() {
150+
SpringApplication application = new SpringApplication(App.class);
151+
application.setWebApplicationType(WebApplicationType.NONE);
152+
String applicationYaml = """
153+
first:
154+
message: value from tests
155+
another-parameter: another parameter value
156+
second:
157+
secondMessage: second value from tests
158+
""";
159+
putParameter(localstack, "/test/second/message", applicationYaml, REGION);
160+
161+
try (ConfigurableApplicationContext context = runApplication(application,
162+
"aws-parameterstore:/test/second/?extension=yaml")) {
163+
assertThat(context.getEnvironment().getProperty("first.message")).isEqualTo("value from tests");
164+
assertThat(context.getEnvironment().getProperty("first.another-parameter"))
165+
.isEqualTo("another parameter value");
166+
assertThat(context.getEnvironment().getProperty("second.secondMessage"))
167+
.isEqualTo("second value from tests");
168+
assertThat(context.getEnvironment().getProperty("non-existing-parameter")).isNull();
169+
}
170+
}
171+
126172
@Test
127173
void clientIsConfiguredWithCustomizerProvidedToBootstrapRegistry() {
128174
SpringApplication application = new SpringApplication(App.class);

spring-cloud-aws-parameter-store/src/main/java/io/awspring/cloud/parameterstore/ParameterStorePropertySource.java

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616
package io.awspring.cloud.parameterstore;
1717

1818
import io.awspring.cloud.core.config.AwsPropertySource;
19-
import java.util.LinkedHashMap;
20-
import java.util.Map;
19+
20+
import java.io.ByteArrayInputStream;
21+
import java.io.InputStream;
22+
import java.util.*;
23+
2124
import org.apache.commons.logging.Log;
2225
import org.apache.commons.logging.LogFactory;
23-
import org.springframework.lang.Nullable;
26+
import org.jspecify.annotations.Nullable;
27+
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
28+
import org.springframework.core.io.ByteArrayResource;
2429
import software.amazon.awssdk.services.ssm.SsmClient;
2530
import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest;
2631
import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse;
@@ -33,6 +38,7 @@
3338
* @author Joris Kuipers
3439
* @author Eddú Meléndez
3540
* @author Maciej Walkowiak
41+
* @author Matej Nedic
3642
* @since 2.0.0
3743
*/
3844
public class ParameterStorePropertySource extends AwsPropertySource<ParameterStorePropertySource, SsmClient> {
@@ -41,6 +47,8 @@ public class ParameterStorePropertySource extends AwsPropertySource<ParameterSto
4147
// ParameterStoreConfigDataLoader
4248
private static Log LOG = LogFactory.getLog(ParameterStorePropertySource.class);
4349
private static final String PREFIX_PART = "?prefix=";
50+
51+
private static final String PREFIX_PROPERTIES_LOAD = "?extension=";
4452
private final String context;
4553

4654
private final String parameterPath;
@@ -52,6 +60,13 @@ public class ParameterStorePropertySource extends AwsPropertySource<ParameterSto
5260
@Nullable
5361
private final String prefix;
5462

63+
private boolean propertiesType = false;
64+
65+
@Nullable
66+
private String prefixType;
67+
68+
private static final Set<String> ALLOWED_TYPES = Set.of("properties", "json", "yaml");
69+
5570
private final Map<String, Object> properties = new LinkedHashMap<>();
5671

5772
public ParameterStorePropertySource(String context, SsmClient ssmClient) {
@@ -87,11 +102,24 @@ public Object getProperty(String name) {
87102
private void getParameters(GetParametersByPathRequest paramsRequest) {
88103
GetParametersByPathResponse paramsResult = this.source.getParametersByPath(paramsRequest);
89104
for (Parameter parameter : paramsResult.parameters()) {
90-
String key = parameter.name().replace(this.parameterPath, "").replace('/', '.').replaceAll("_(\\d)_",
91-
"[$1]");
92-
LOG.debug("Populating property retrieved from AWS Parameter Store: " + key);
93-
String propertyKey = prefix != null ? prefix + key : key;
94-
this.properties.put(propertyKey, parameter.value());
105+
if (propertiesType) {
106+
Properties props;
107+
if (prefixType.equals("properties")) {
108+
props = readProperties(parameter.value());
109+
} else {
110+
props = readYaml(parameter.value());
111+
}
112+
for (Map.Entry<Object, Object> entry : props.entrySet()) {
113+
properties.put(String.valueOf(entry.getKey()), entry.getValue());
114+
}
115+
}
116+
else {
117+
String key = parameter.name().replace(this.parameterPath, "").replace('/', '.').replaceAll("_(\\d)_",
118+
"[$1]");
119+
LOG.debug("Populating property retrieved from AWS Parameter Store: " + key);
120+
String propertyKey = prefix != null ? prefix + key : key;
121+
this.properties.put(propertyKey, parameter.value());
122+
}
95123
}
96124
if (paramsResult.nextToken() != null) {
97125
getParameters(paramsRequest.toBuilder().nextToken(paramsResult.nextToken()).build());
@@ -112,20 +140,51 @@ String getParameterPath() {
112140
}
113141

114142
@Nullable
115-
private static String resolvePrefix(String context) {
143+
private String resolvePrefix(String context) {
116144
int prefixIndex = context.indexOf(PREFIX_PART);
117145
if (prefixIndex != -1) {
118146
return context.substring(prefixIndex + PREFIX_PART.length());
119147
}
120148
return null;
121149
}
122150

123-
private static String resolveParameterPath(String context) {
151+
private String resolveParameterPath(String context) {
124152
int prefixIndex = context.indexOf(PREFIX_PART);
125153
if (prefixIndex != -1) {
126154
return context.substring(0, prefixIndex);
127155
}
156+
prefixIndex = context.indexOf(PREFIX_PROPERTIES_LOAD);
157+
if (prefixIndex != -1) {
158+
this.propertiesType = true;
159+
String extracted = context.substring(prefixIndex + PREFIX_PROPERTIES_LOAD.length()).toLowerCase();
160+
if (ALLOWED_TYPES.contains(extracted)) {
161+
this.prefixType = extracted;
162+
} else {
163+
throw new IllegalArgumentException("Invalid prefixType: " + extracted + ". Must be one of properties, json, or yaml.");
164+
}
165+
return context.substring(0, prefixIndex);
166+
}
128167
return context;
129168
}
130169

170+
private Properties readProperties(String input) {
171+
Properties properties = new Properties();
172+
173+
try (InputStream in = new ByteArrayInputStream(input.getBytes())) {
174+
properties.load(in);
175+
}
176+
catch (Exception e) {
177+
throw new IllegalStateException("Cannot load environment", e);
178+
}
179+
return properties;
180+
}
181+
182+
private Properties readYaml(String input) {
183+
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
184+
yaml.setResources(
185+
new ByteArrayResource(input.getBytes())
186+
);
187+
return yaml.getObject();
188+
}
189+
131190
}

0 commit comments

Comments
 (0)