Skip to content

Commit 40182da

Browse files
Support custom Json serde for SecretsManager (#423)
Co-authored-by: Sam <sam@sksamuel.com>
1 parent 5967b2f commit 40182da

File tree

6 files changed

+89
-9
lines changed

6 files changed

+89
-9
lines changed

hoplite-aws/src/main/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessor.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@ import kotlinx.serialization.json.Json
2929
*/
3030
class AwsSecretsManagerPreprocessor(
3131
private val report: Boolean = false,
32+
private val json: Json,
3233
private val createClient: () -> AWSSecretsManager = { AWSSecretsManagerClientBuilder.standard().build() }
3334
) : TraversingPrimitivePreprocessor() {
3435

36+
constructor(
37+
report: Boolean = false,
38+
createClient: () -> AWSSecretsManager = { AWSSecretsManagerClientBuilder.standard().build() }
39+
) : this(report, Json.Default, createClient)
40+
3541
private val client by lazy { createClient() }
3642
private val regex1 = "\\$\\{awssecret:(.+?)}".toRegex()
3743
private val regex2 = "secretsmanager://(.+?)".toRegex()
@@ -84,7 +90,7 @@ class AwsSecretsManagerPreprocessor(
8490
.withMeta(CommonMetadata.RemoteLookup, "AWS '$key'")
8591
.valid()
8692
} else {
87-
val map = runCatching { Json.Default.decodeFromString<Map<String, String>>(secret) }.getOrElse { emptyMap() }
93+
val map = runCatching { json.decodeFromString<Map<String, String>>(secret) }.getOrElse { emptyMap() }
8894
val indexedValue = map[index]
8995
if (indexedValue == null)
9096
ConfigFailure.PreprocessorWarning(

hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerContextResolverTest.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import java.util.Properties
2626
@OptIn(ExperimentalHoplite::class)
2727
class AwsSecretsManagerContextResolverTest : FunSpec() {
2828

29-
private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:1.3.1"))
29+
private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:3.6.0"))
3030
.withServices(LocalStackContainer.Service.SECRETSMANAGER)
3131
.withEnv("SKIP_SSL_CERT_DOWNLOAD", "true")
3232

@@ -83,10 +83,10 @@ class AwsSecretsManagerContextResolverTest : FunSpec() {
8383
.shouldBeInstanceOf<Validated.Invalid<ConfigFailure>>().error.description().shouldContain("qwerty")
8484
}
8585

86-
test("empty secret should return error and include empty secret message") {
86+
test("blank secret should return error and include empty secret message") {
8787
val props = Properties()
8888
props["a"] = "\${{ aws-secrets-manager:bibblebobble }}"
89-
client.createSecret(CreateSecretRequest().withName("bibblebobble").withSecretString(""))
89+
client.createSecret(CreateSecretRequest().withName("bibblebobble").withSecretString(" "))
9090
ConfigLoaderBuilder.newBuilder()
9191
.addResolver(AwsSecretsManagerContextResolver { client })
9292
.addPropertySource(PropsPropertySource(props))

hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessorTest.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import java.util.Properties
3131

3232
class AwsSecretsManagerPreprocessorTest : FunSpec() {
3333

34-
private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:1.3.1"))
34+
private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:3.6.0"))
3535
.withServices(LocalStackContainer.Service.SECRETSMANAGER)
3636
.withEnv("SKIP_SSL_CERT_DOWNLOAD", "true")
3737

@@ -95,8 +95,8 @@ class AwsSecretsManagerPreprocessorTest : FunSpec() {
9595
).shouldBeInstanceOf<Validated.Invalid<ConfigFailure>>().error.description().shouldContain("unkunk")
9696
}
9797

98-
test("empty secret should return error and include key") {
99-
client.createSecret(CreateSecretRequest().withName("bibblebobble").withSecretString(""))
98+
test("blank secret should return error and include key") {
99+
client.createSecret(CreateSecretRequest().withName("bibblebobble").withSecretString(" "))
100100
AwsSecretsManagerPreprocessor { client }.process(
101101
StringNode(
102102
"secretsmanager://bibblebobble",

hoplite-aws2/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ dependencies {
77
api(libs.regions)
88
api(libs.secretsmanager)
99
implementation(libs.kotlinx.serialization.json)
10+
testApi("io.kotest.extensions:kotest-extensions-testcontainers:2.0.2")
11+
testApi(libs.testcontainers.localstack)
1012
}
1113

1214
apply("../publish.gradle.kts")

hoplite-aws2/src/main/kotlin/com/sksamuel/hoplite/aws2/AwsSecretsManagerPreprocessor.kt

+8-2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@ import software.amazon.awssdk.services.secretsmanager.model.SecretsManagerExcept
2727
*/
2828
class AwsSecretsManagerPreprocessor(
2929
private val report: Boolean = false,
30-
private val createClient: () -> SecretsManagerClient = { SecretsManagerClient.create() }
30+
private val json: Json,
31+
private val createClient: () -> SecretsManagerClient = { SecretsManagerClient.create() },
3132
) : TraversingPrimitivePreprocessor() {
3233

34+
constructor(
35+
report: Boolean = false,
36+
createClient: () -> SecretsManagerClient = { SecretsManagerClient.create() },
37+
) : this(report, Json.Default, createClient)
38+
3339
private val client by lazy { createClient() }
3440
private val regex1 = "\\$\\{awssecret:(.+?)}".toRegex()
3541
private val regex2 = "secretsmanager://(.+?)".toRegex()
@@ -82,7 +88,7 @@ class AwsSecretsManagerPreprocessor(
8288
.withMeta(CommonMetadata.RemoteLookup, "AWS '$key'")
8389
.valid()
8490
} else {
85-
val map = runCatching { Json.Default.decodeFromString<Map<String, String>>(secret) }.getOrElse { emptyMap() }
91+
val map = runCatching { json.decodeFromString<Map<String, String>>(secret) }.getOrElse { emptyMap() }
8692
val indexedValue = map[index]
8793
if (indexedValue == null)
8894
ConfigFailure.PreprocessorWarning(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.sksamuel.hoplite.aws2
2+
3+
import com.sksamuel.hoplite.ConfigLoaderBuilder
4+
import com.sksamuel.hoplite.parsers.PropsPropertySource
5+
import io.kotest.core.extensions.install
6+
import io.kotest.core.spec.style.FunSpec
7+
import io.kotest.extensions.testcontainers.ContainerExtension
8+
import io.kotest.matchers.shouldBe
9+
import kotlinx.serialization.json.Json
10+
import org.testcontainers.containers.localstack.LocalStackContainer
11+
import org.testcontainers.utility.DockerImageName
12+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
13+
import software.amazon.awssdk.regions.Region
14+
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient
15+
import java.util.Properties
16+
17+
class AwsSecretsManagerPreprocessorTest : FunSpec() {
18+
private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:3.6.0"))
19+
.withServices(LocalStackContainer.Service.SECRETSMANAGER)
20+
.withEnv("SKIP_SSL_CERT_DOWNLOAD", "true")
21+
22+
init {
23+
24+
install(ContainerExtension(localstack))
25+
26+
val client = SecretsManagerClient.builder()
27+
.region(Region.US_EAST_1)
28+
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.SECRETSMANAGER))
29+
.credentialsProvider { AwsBasicCredentials.create(localstack.accessKey, localstack.secretKey) }
30+
.build()
31+
32+
test("should support secret with unquoted number when isLenient is set") {
33+
client.createSecret {
34+
it.name("unquoted")
35+
it.secretString("""{"port": 5432}""")
36+
}
37+
val props = Properties()
38+
props["port"] = "awssm://unquoted[port]"
39+
40+
val json = Json { isLenient = true }
41+
ConfigLoaderBuilder.default()
42+
.addPreprocessor(AwsSecretsManagerPreprocessor(json = json) { client })
43+
.addPropertySource(PropsPropertySource(props))
44+
.build()
45+
.loadConfigOrThrow<PortHolder>()
46+
.port.shouldBe(5432)
47+
}
48+
49+
test("should support secret with quoted number for default Json serde") {
50+
client.createSecret {
51+
it.name("quoted")
52+
it.secretString("""{"port": "5432"}""")
53+
}
54+
val props = Properties()
55+
props["port"] = "awssm://quoted[port]"
56+
ConfigLoaderBuilder.default()
57+
.addPreprocessor(AwsSecretsManagerPreprocessor { client })
58+
.addPropertySource(PropsPropertySource(props))
59+
.build()
60+
.loadConfigOrThrow<PortHolder>()
61+
.port.shouldBe(5432)
62+
}
63+
}
64+
65+
data class PortHolder(val port: Int)
66+
}

0 commit comments

Comments
 (0)