Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to set property prefix on Parameter Store property sources #927

Merged
merged 2 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/src/main/asciidoc/parameter-store.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,33 @@ spring.config.import[1]=aws-parameterstore=/config/optional-params/
If you add indexed parameter names such as `/config/application/cloud.aws.stack_0_.name`, `/config/application/cloud.aws.stack_1_.name`, ... to Parameter Store,
these will become accessible as array properties `cloud.aws.stack[0].name`, `cloud.aws.stack[1].name`, ... in Spring.

==== Adding prefix to property keys

To avoid property key collisions it is possible to configure a property key prefix that gets added to each resolved parameter.

As an example, assuming the following parameters are stored under path `/config/my-datasource`:

|===
| Parameter Name | Parameter Value

| `/config/my-datasource/url` | `jdbc:mysql://localhost:3306`

| `/config/my-datasource/username` | `db-user`

|===

By default, `url` and `username` properties will be added to the Spring environment. To add a prefix to property keys configure `spring.config.import` property with `?prefix=` added to the parameter path:

[source,properties]
----
spring.config.import=aws-parameterstore:/config/my-datasource/?prefix=spring.datasource.
----

With such config, properties `spring.datasource.url` and `spring.datasource.username` are added to the Spring environment.

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.


=== 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class ParameterStoreConfigDataLoaderIntegrationTests {
static void beforeAll() {
putParameter(localstack, "/config/spring/message", "value from tests", REGION);
putParameter(localstack, "/config/spring/another-parameter", "another parameter value", REGION);
putParameter(localstack, "/config/second/secondMessage", "second value from tests", REGION);
}

@Test
Expand All @@ -91,6 +92,22 @@ void resolvesPropertyFromParameterStore() {
}
}

@Test
void resolvesPropertiesWithPrefixes() {
SpringApplication application = new SpringApplication(App.class);
application.setWebApplicationType(WebApplicationType.NONE);

try (ConfigurableApplicationContext context = runApplication(application,
"aws-parameterstore:/config/spring/?prefix=first.;/config/second/?prefix=second.")) {
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 clientIsConfiguredWithConfigurerProvidedToBootstrapRegistry() {
SpringApplication application = new SpringApplication(App.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,31 @@ public class ParameterStorePropertySource extends AwsPropertySource<ParameterSto
// logger must stay static non-final so that it can be set with a value in
// ParameterStoreConfigDataLoader
private static Log LOG = LogFactory.getLog(ParameterStorePropertySource.class);

private static final String PREFIX_PART = "?prefix=";
private final String context;

private final String parameterPath;

/**
* Prefix that gets added to resolved property keys. Useful when same property keys are returned by multiple
* parameter paths. sources.
maciejwalkowiak marked this conversation as resolved.
Show resolved Hide resolved
*/
@Nullable
private final String prefix;

private final Map<String, Object> properties = new LinkedHashMap<>();

public ParameterStorePropertySource(String context, SsmClient ssmClient) {
super("aws-parameterstore:" + context, ssmClient);
this.context = context;
this.parameterPath = resolveParameterPath(context);
this.prefix = resolvePrefix(context);
}

@Override
public void init() {
GetParametersByPathRequest paramsRequest = GetParametersByPathRequest.builder().path(context).recursive(true)
.withDecryption(true).build();
GetParametersByPathRequest paramsRequest = GetParametersByPathRequest.builder().path(parameterPath)
.recursive(true).withDecryption(true).build();
getParameters(paramsRequest);
}

Expand All @@ -76,13 +87,45 @@ 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.context, "").replace('/', '.').replaceAll("_(\\d)_", "[$1]");
String key = parameter.name().replace(this.parameterPath, "").replace('/', '.')
.replaceAll("_(\\d)_", "[$1]");
LOG.debug("Populating property retrieved from AWS Parameter Store: " + key);
this.properties.put(key, parameter.value());
String propertyKey = prefix != null ? prefix + key : key;
this.properties.put(propertyKey, parameter.value());
}
if (paramsResult.nextToken() != null) {
getParameters(paramsRequest.toBuilder().nextToken(paramsResult.nextToken()).build());
}
}

@Nullable
String getPrefix() {
return prefix;
}

String getContext() {
return context;
}

String getParameterPath() {
return parameterPath;
}

@Nullable
private static String resolvePrefix(String context) {
int prefixIndex = context.indexOf(PREFIX_PART);
if (prefixIndex != -1) {
return context.substring(prefixIndex + PREFIX_PART.length());
}
return null;
}

private static String resolveParameterPath(String context) {
int prefixIndex = context.indexOf(PREFIX_PART);
if (prefixIndex != -1) {
return context.substring(0, prefixIndex);
}
return context;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,41 @@ void arrayParameterNames() {
it.assertThat(propertySource.getProperty("key[1].nested[1].nestedValue")).isEqualTo("key_nestedValue2");
});
}

@Test
void resolvesPrefixAndParameterPathFromContext() {
ParameterStorePropertySource propertySource = new ParameterStorePropertySource("/config/myservice/?prefix=xxx",
ssmClient);
assertThat(propertySource.getName()).isEqualTo("aws-parameterstore:/config/myservice/?prefix=xxx");
assertThat(propertySource.getPrefix()).isEqualTo("xxx");
assertThat(propertySource.getContext()).isEqualTo("/config/myservice/?prefix=xxx");
assertThat(propertySource.getParameterPath()).isEqualTo("/config/myservice/");
}

@Test
void addsPrefixToParameter() {
ParameterStorePropertySource propertySource = new ParameterStorePropertySource("/config/myservice/?prefix=yyy.",
ssmClient);

Parameter parameter = Parameter.builder().name("key1").value("my parameter").build();
GetParametersByPathResponse parametersByPathResponse = GetParametersByPathResponse.builder()
.parameters(parameter).build();

when(ssmClient.getParametersByPath(any(GetParametersByPathRequest.class))).thenReturn(parametersByPathResponse);

propertySource.init();

assertThat(propertySource.getPropertyNames()).containsExactly("yyy.key1");
assertThat(propertySource.getProperty("yyy.key1")).isEqualTo("my parameter");
assertThat(propertySource.getProperty("key1")).isNull();
}

@Test
void copyPreservesPrefix() {
ParameterStorePropertySource propertySource = new ParameterStorePropertySource("/config/myservice/?prefix=yyy",
ssmClient);
ParameterStorePropertySource copy = propertySource.copy();
assertThat(propertySource.getContext()).isEqualTo(copy.getContext());
assertThat(propertySource.getPrefix()).isEqualTo(copy.getPrefix());
}
}
Loading