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

Support custom Json serde for SecretsManager #423

Merged
merged 8 commits into from
Sep 16, 2024
2 changes: 1 addition & 1 deletion hoplite-aws/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dependencies {
api(projects.hopliteCore)
api(libs.aws.java.sdk.secretsmanager)
api(libs.aws.java.sdk.ssm)
implementation(libs.kotlinx.serialization.json)
api(libs.kotlinx.serialization.json)
testApi("io.kotest.extensions:kotest-extensions-testcontainers:2.0.2")
testApi(libs.testcontainers.localstack)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import kotlinx.serialization.json.Json
*/
class AwsSecretsManagerPreprocessor(
private val report: Boolean = false,
private val json: Json = Json.Default,
samu-developments marked this conversation as resolved.
Show resolved Hide resolved
private val createClient: () -> AWSSecretsManager = { AWSSecretsManagerClientBuilder.standard().build() }
) : TraversingPrimitivePreprocessor() {

Expand Down Expand Up @@ -84,7 +85,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
4 changes: 3 additions & 1 deletion hoplite-aws2/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ dependencies {
api(projects.hopliteCore)
api(libs.regions)
api(libs.secretsmanager)
implementation(libs.kotlinx.serialization.json)
api(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,6 +27,7 @@ import software.amazon.awssdk.services.secretsmanager.model.SecretsManagerExcept
*/
class AwsSecretsManagerPreprocessor(
private val report: Boolean = false,
private val json: Json = Json.Default,
private val createClient: () -> SecretsManagerClient = { SecretsManagerClient.create() }
) : TraversingPrimitivePreprocessor() {

Expand Down Expand Up @@ -82,7 +83,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,64 @@
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.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()
.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)
}
Loading