Skip to content

Commit

Permalink
Support custom Json serde for SecretsManager (#423)
Browse files Browse the repository at this point in the history
Co-authored-by: Sam <[email protected]>
  • Loading branch information
samu-developments and sksamuel authored Sep 16, 2024
1 parent 5967b2f commit 40182da
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -84,7 +90,7 @@ class AwsSecretsManagerPreprocessor(
.withMeta(CommonMetadata.RemoteLookup, "AWS '$key'")
.valid()
} else {
val map = runCatching { Json.Default.decodeFromString<Map<String, String>>(secret) }.getOrElse { emptyMap() }
val map = runCatching { json.decodeFromString<Map<String, String>>(secret) }.getOrElse { emptyMap() }
val indexedValue = map[index]
if (indexedValue == null)
ConfigFailure.PreprocessorWarning(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -83,10 +83,10 @@ class AwsSecretsManagerContextResolverTest : FunSpec() {
.shouldBeInstanceOf<Validated.Invalid<ConfigFailure>>().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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -95,8 +95,8 @@ class AwsSecretsManagerPreprocessorTest : FunSpec() {
).shouldBeInstanceOf<Validated.Invalid<ConfigFailure>>().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",
Expand Down
2 changes: 2 additions & 0 deletions hoplite-aws2/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -82,7 +88,7 @@ class AwsSecretsManagerPreprocessor(
.withMeta(CommonMetadata.RemoteLookup, "AWS '$key'")
.valid()
} else {
val map = runCatching { Json.Default.decodeFromString<Map<String, String>>(secret) }.getOrElse { emptyMap() }
val map = runCatching { json.decodeFromString<Map<String, String>>(secret) }.getOrElse { emptyMap() }
val indexedValue = map[index]
if (indexedValue == null)
ConfigFailure.PreprocessorWarning(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PortHolder>()
.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<PortHolder>()
.port.shouldBe(5432)
}
}

data class PortHolder(val port: Int)
}

0 comments on commit 40182da

Please sign in to comment.