diff --git a/hoplite-aws/src/main/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessor.kt b/hoplite-aws/src/main/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessor.kt index 13461d0e..bbd1617f 100644 --- a/hoplite-aws/src/main/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessor.kt +++ b/hoplite-aws/src/main/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessor.kt @@ -29,9 +29,15 @@ import kotlinx.serialization.json.Json */ class AwsSecretsManagerPreprocessor( private val report: Boolean = false, + private val json: Json, private val createClient: () -> AWSSecretsManager = { AWSSecretsManagerClientBuilder.standard().build() } ) : TraversingPrimitivePreprocessor() { + constructor( + report: Boolean = false, + createClient: () -> AWSSecretsManager = { AWSSecretsManagerClientBuilder.standard().build() } + ) : this(report, Json.Default, createClient) + private val client by lazy { createClient() } private val regex1 = "\\$\\{awssecret:(.+?)}".toRegex() private val regex2 = "secretsmanager://(.+?)".toRegex() @@ -84,7 +90,7 @@ class AwsSecretsManagerPreprocessor( .withMeta(CommonMetadata.RemoteLookup, "AWS '$key'") .valid() } else { - val map = runCatching { Json.Default.decodeFromString>(secret) }.getOrElse { emptyMap() } + val map = runCatching { json.decodeFromString>(secret) }.getOrElse { emptyMap() } val indexedValue = map[index] if (indexedValue == null) ConfigFailure.PreprocessorWarning( diff --git a/hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerContextResolverTest.kt b/hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerContextResolverTest.kt index 7c64a90e..1e4973ad 100644 --- a/hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerContextResolverTest.kt +++ b/hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerContextResolverTest.kt @@ -26,7 +26,7 @@ import java.util.Properties @OptIn(ExperimentalHoplite::class) class AwsSecretsManagerContextResolverTest : FunSpec() { - private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:1.3.1")) + private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:3.6.0")) .withServices(LocalStackContainer.Service.SECRETSMANAGER) .withEnv("SKIP_SSL_CERT_DOWNLOAD", "true") @@ -83,10 +83,10 @@ class AwsSecretsManagerContextResolverTest : FunSpec() { .shouldBeInstanceOf>().error.description().shouldContain("qwerty") } - test("empty secret should return error and include empty secret message") { + test("blank secret should return error and include empty secret message") { val props = Properties() props["a"] = "\${{ aws-secrets-manager:bibblebobble }}" - client.createSecret(CreateSecretRequest().withName("bibblebobble").withSecretString("")) + client.createSecret(CreateSecretRequest().withName("bibblebobble").withSecretString(" ")) ConfigLoaderBuilder.newBuilder() .addResolver(AwsSecretsManagerContextResolver { client }) .addPropertySource(PropsPropertySource(props)) diff --git a/hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessorTest.kt b/hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessorTest.kt index da2b62ab..bec4dc61 100644 --- a/hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessorTest.kt +++ b/hoplite-aws/src/test/kotlin/com/sksamuel/hoplite/aws/AwsSecretsManagerPreprocessorTest.kt @@ -31,7 +31,7 @@ import java.util.Properties class AwsSecretsManagerPreprocessorTest : FunSpec() { - private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:1.3.1")) + private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:3.6.0")) .withServices(LocalStackContainer.Service.SECRETSMANAGER) .withEnv("SKIP_SSL_CERT_DOWNLOAD", "true") @@ -95,8 +95,8 @@ class AwsSecretsManagerPreprocessorTest : FunSpec() { ).shouldBeInstanceOf>().error.description().shouldContain("unkunk") } - test("empty secret should return error and include key") { - client.createSecret(CreateSecretRequest().withName("bibblebobble").withSecretString("")) + test("blank secret should return error and include key") { + client.createSecret(CreateSecretRequest().withName("bibblebobble").withSecretString(" ")) AwsSecretsManagerPreprocessor { client }.process( StringNode( "secretsmanager://bibblebobble", diff --git a/hoplite-aws2/build.gradle.kts b/hoplite-aws2/build.gradle.kts index 8bee5287..f11373d7 100644 --- a/hoplite-aws2/build.gradle.kts +++ b/hoplite-aws2/build.gradle.kts @@ -7,6 +7,8 @@ dependencies { api(libs.regions) api(libs.secretsmanager) implementation(libs.kotlinx.serialization.json) + testApi("io.kotest.extensions:kotest-extensions-testcontainers:2.0.2") + testApi(libs.testcontainers.localstack) } apply("../publish.gradle.kts") diff --git a/hoplite-aws2/src/main/kotlin/com/sksamuel/hoplite/aws2/AwsSecretsManagerPreprocessor.kt b/hoplite-aws2/src/main/kotlin/com/sksamuel/hoplite/aws2/AwsSecretsManagerPreprocessor.kt index 9ee3c6b3..990f227b 100644 --- a/hoplite-aws2/src/main/kotlin/com/sksamuel/hoplite/aws2/AwsSecretsManagerPreprocessor.kt +++ b/hoplite-aws2/src/main/kotlin/com/sksamuel/hoplite/aws2/AwsSecretsManagerPreprocessor.kt @@ -27,9 +27,15 @@ import software.amazon.awssdk.services.secretsmanager.model.SecretsManagerExcept */ class AwsSecretsManagerPreprocessor( private val report: Boolean = false, - private val createClient: () -> SecretsManagerClient = { SecretsManagerClient.create() } + private val json: Json, + private val createClient: () -> SecretsManagerClient = { SecretsManagerClient.create() }, ) : TraversingPrimitivePreprocessor() { + constructor( + report: Boolean = false, + createClient: () -> SecretsManagerClient = { SecretsManagerClient.create() }, + ) : this(report, Json.Default, createClient) + private val client by lazy { createClient() } private val regex1 = "\\$\\{awssecret:(.+?)}".toRegex() private val regex2 = "secretsmanager://(.+?)".toRegex() @@ -82,7 +88,7 @@ class AwsSecretsManagerPreprocessor( .withMeta(CommonMetadata.RemoteLookup, "AWS '$key'") .valid() } else { - val map = runCatching { Json.Default.decodeFromString>(secret) }.getOrElse { emptyMap() } + val map = runCatching { json.decodeFromString>(secret) }.getOrElse { emptyMap() } val indexedValue = map[index] if (indexedValue == null) ConfigFailure.PreprocessorWarning( diff --git a/hoplite-aws2/src/test/kotlin/com/sksamuel/hoplite/aws2/AwsSecretsManagerPreprocessorTest.kt b/hoplite-aws2/src/test/kotlin/com/sksamuel/hoplite/aws2/AwsSecretsManagerPreprocessorTest.kt new file mode 100644 index 00000000..46850c50 --- /dev/null +++ b/hoplite-aws2/src/test/kotlin/com/sksamuel/hoplite/aws2/AwsSecretsManagerPreprocessorTest.kt @@ -0,0 +1,66 @@ +package com.sksamuel.hoplite.aws2 + +import com.sksamuel.hoplite.ConfigLoaderBuilder +import com.sksamuel.hoplite.parsers.PropsPropertySource +import io.kotest.core.extensions.install +import io.kotest.core.spec.style.FunSpec +import io.kotest.extensions.testcontainers.ContainerExtension +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.Json +import org.testcontainers.containers.localstack.LocalStackContainer +import org.testcontainers.utility.DockerImageName +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient +import java.util.Properties + +class AwsSecretsManagerPreprocessorTest : FunSpec() { + private val localstack = LocalStackContainer(DockerImageName.parse("localstack/localstack:3.6.0")) + .withServices(LocalStackContainer.Service.SECRETSMANAGER) + .withEnv("SKIP_SSL_CERT_DOWNLOAD", "true") + + init { + + install(ContainerExtension(localstack)) + + val client = SecretsManagerClient.builder() + .region(Region.US_EAST_1) + .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.SECRETSMANAGER)) + .credentialsProvider { AwsBasicCredentials.create(localstack.accessKey, localstack.secretKey) } + .build() + + test("should support secret with unquoted number when isLenient is set") { + client.createSecret { + it.name("unquoted") + it.secretString("""{"port": 5432}""") + } + val props = Properties() + props["port"] = "awssm://unquoted[port]" + + val json = Json { isLenient = true } + ConfigLoaderBuilder.default() + .addPreprocessor(AwsSecretsManagerPreprocessor(json = json) { client }) + .addPropertySource(PropsPropertySource(props)) + .build() + .loadConfigOrThrow() + .port.shouldBe(5432) + } + + test("should support secret with quoted number for default Json serde") { + client.createSecret { + it.name("quoted") + it.secretString("""{"port": "5432"}""") + } + val props = Properties() + props["port"] = "awssm://quoted[port]" + ConfigLoaderBuilder.default() + .addPreprocessor(AwsSecretsManagerPreprocessor { client }) + .addPropertySource(PropsPropertySource(props)) + .build() + .loadConfigOrThrow() + .port.shouldBe(5432) + } + } + + data class PortHolder(val port: Int) +}