Skip to content

Commit

Permalink
Add separator replacement support for SSM
Browse files Browse the repository at this point in the history
  • Loading branch information
sndl committed Jun 19, 2019
1 parent 619e7c9 commit 33c40d8
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.1.4

* SSM Backend now supports automatic separator replacement with `separator-to-replace` option.
I.e. if key you set separator to `.`, and upload key `section.key1` then it will be automatically uploaded to SSM as `prefix/section/key1`.
This will keeping compatibility between different config backends (i.e. Toml) and will allow using SSM features like search by-path

## 0.1.3

* `list` command on SSM backend now returns all parameters with recursive call
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Example:
profile = sandbox
prefix = /sndl/parnas/test
kms-key-id = 111a1aa1-a11a-1a1a-1aa1-1111111111a1
separator-to-replace = . # Can be absent, if set then all "." in uploaded keys will be replaced with "/"
```

## Usage
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile
import tanvd.kosogor.proxy.shadowJar

group = "sndl.parnas"
version = "0.1.3"
version = "0.1.4"
description = "PARameter Naming And Storing"

plugins {
Expand Down
26 changes: 18 additions & 8 deletions src/main/kotlin/sndl/parnas/backend/impl/SSM.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ import sndl.parnas.backend.ConfigOption
import sndl.parnas.utils.*

class SSM(name: String, private val ssmClient: AWSSimpleSystemsManagement,
private var prefix: String, private val keyId: String) : Backend(name) {
var prefix: String, private val keyId: String, private val separatorToReplace: String? = null) : Backend(name) {

constructor(name: String, region: String?, profileName: String?,
prefix: String, keyId: String) :
prefix: String, keyId: String, separatorToReplace: String? = null) :
this(
name = name,
ssmClient = AWSSimpleSystemsManagementClientBuilder.standard()
.withRegion(region)
.withCredentials(ProfileCredentialsProvider(profileName))
.build(),
prefix = prefix,
keyId = keyId
keyId = keyId,
separatorToReplace = separatorToReplace
)

constructor(name: String, config: Map<String, String>) :
Expand All @@ -29,7 +30,8 @@ class SSM(name: String, private val ssmClient: AWSSimpleSystemsManagement,
region = getConfigParameter("region", config),
profileName = getConfigParameter("profile", config),
prefix = getConfigParameter("prefix", config),
keyId = getConfigParameter("kms-key-id", config, true)
keyId = getConfigParameter("kms-key-id", config, true),
separatorToReplace = config["separator-to-replace"]
)

override val isInitialized: Boolean = true
Expand All @@ -56,15 +58,15 @@ class SSM(name: String, private val ssmClient: AWSSimpleSystemsManagement,
val nextToken = result.nextToken

result.parameters.forEach {
add(ConfigOption(it.name.removePrefix(prefix), it.value))
add(ConfigOption(it.name.removePrefix(prefix).convertFromSSMFormat(), it.value))
}

request.nextToken = nextToken
} while (nextToken != null)
}

override fun get(key: String): ConfigOption? {
val fullKey = prefix + key
val fullKey = prefix + key.convertToSSMFormat()
val request = GetParameterRequest()
.withName(fullKey)
.withWithDecryption(true)
Expand All @@ -77,7 +79,7 @@ class SSM(name: String, private val ssmClient: AWSSimpleSystemsManagement,
}

override fun set(key: String, value: String): ConfigOption {
val fullKey = prefix + key
val fullKey = prefix + key.convertToSSMFormat()
val request = PutParameterRequest()
.withName(fullKey)
.withValue(value)
Expand All @@ -91,7 +93,7 @@ class SSM(name: String, private val ssmClient: AWSSimpleSystemsManagement,
}

override fun delete(key: String) {
val fullKey = prefix + key
val fullKey = prefix + key.convertToSSMFormat()
val request = DeleteParameterRequest()
.withName(fullKey)

Expand All @@ -101,4 +103,12 @@ class SSM(name: String, private val ssmClient: AWSSimpleSystemsManagement,
System.err.println("ERROR: Parameter Not Found - \"$key\"")
}
}

private fun String.convertToSSMFormat() = separatorToReplace?.let {
this.replace(separatorToReplace, "/")
} ?: this

private fun String.convertFromSSMFormat() = separatorToReplace?.let {
this.replace("/", separatorToReplace)
} ?: this
}
4 changes: 0 additions & 4 deletions src/test/kotlin/sndl/parnas/SSMTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,10 @@ import com.amazonaws.client.builder.AwsClientBuilder
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder
import org.junit.ClassRule
import org.junit.jupiter.api.*
import org.testcontainers.containers.GenericContainer
import sndl.parnas.backend.ConfigOption
import sndl.parnas.backend.impl.SSM
import java.util.UUID.randomUUID

// See: https://github.com/testcontainers/testcontainers-java/issues/318
class KGenericContainer(imageName: String) : GenericContainer<KGenericContainer>(imageName)

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SSMTest {
companion object {
Expand Down
141 changes: 141 additions & 0 deletions src/test/kotlin/sndl/parnas/SSMWithSeparatorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package sndl.parnas

import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.client.builder.AwsClientBuilder
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder
import com.amazonaws.services.simplesystemsmanagement.model.GetParameterRequest
import org.junit.ClassRule
import org.junit.jupiter.api.*
import sndl.parnas.backend.ConfigOption
import sndl.parnas.backend.impl.SSM
import java.util.UUID.randomUUID

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SSMWithSeparatorTest {
companion object {
private const val containerPort = 4583
private const val awsRegion = "eu-west-1"
}

@ClassRule
private val localstack = KGenericContainer("localstack/localstack:latest")
.withExposedPorts(containerPort)
.withEnv("SERVICES", "ssm").also { it.start() }

private val ssmClient = AWSSimpleSystemsManagementClientBuilder.standard()
.withEndpointConfiguration(AwsClientBuilder
.EndpointConfiguration(
"http://${localstack.containerIpAddress}:${localstack.getMappedPort(containerPort)}",
awsRegion))
.withCredentials(AWSStaticCredentialsProvider(BasicAWSCredentials("dummy", "dummy")))
.build()
private val backend
get() = SSM("ssm-test", ssmClient, "/${randomUUID()}/", "1111", ".").also {
it["FIRST_ENTRY"] = "first-entry"
it["SECOND_ENTRY"] = "second-entry"
it["ENTRY_PREFIX.THIRD_ENTRY"] = "third-entry"
it["ENTRY_PREFIX.ENTRY_2ND_PREFIX.FOURTH_ENTRY"] = "fourth-entry"
}

@Test
fun list_backendIsNotEmpty_gotNotEmptyList() {
Assertions.assertTrue(backend.list().size > 0)
}

@Test
fun get_entryExists_gotEntry() {
Assertions.assertEquals(ConfigOption("ENTRY_PREFIX.THIRD_ENTRY", "third-entry"), backend["ENTRY_PREFIX.THIRD_ENTRY"])
}

@Test
fun list_backendIsNotEmpty_gotExpectedListOfValues() {
val list = backend.list()
val expectedList = setOf(
ConfigOption("FIRST_ENTRY", "first-entry"),
ConfigOption("SECOND_ENTRY", "second-entry"),
ConfigOption("ENTRY_PREFIX.THIRD_ENTRY", "third-entry"),
ConfigOption("ENTRY_PREFIX.ENTRY_2ND_PREFIX.FOURTH_ENTRY", "fourth-entry")
)

println(list)

Assertions.assertEquals(expectedList, list)
}

@Test
fun get_entryDoesNotExist_gotNull() {
Assertions.assertNull(backend["ENTRY_PREFIX.FOURTH_ENTRY"])
}

@Test
fun set_entryDoesNotExist_entryExists() {
val testBackend = backend

testBackend["THIRD_ENTRY"] = "third-entry"

val expectedEntry = ConfigOption("THIRD_ENTRY", "third-entry")
val entry = testBackend["THIRD_ENTRY"]

Assertions.assertEquals(expectedEntry, entry)
}

@Test
fun set_entryDoesNotExist_entryExistsInCorrectFormInSSM() {
val testBackend = backend

testBackend["ENTRY_PREFIX.FIFTH_ENTRY"] = "fifth-entry"

val expectedEntry = ConfigOption("ENTRY_PREFIX.FIFTH_ENTRY", "fifth-entry")
val entry = ConfigOption("ENTRY_PREFIX.FIFTH_ENTRY",
ssmClient.getParameter(GetParameterRequest()
.withName("${testBackend.prefix}ENTRY_PREFIX/FIFTH_ENTRY")
.withWithDecryption(true)
).parameter.value)

Assertions.assertEquals(expectedEntry, entry)
}

@Test
fun set_entryDoesNotExist_exactlyOneEntryIsCreated() {
val testBackend = backend
val beforeSize = testBackend.list().size

testBackend["THIRD_ENTRY"] = "third-entry"

val afterSize = testBackend.list().size

Assertions.assertTrue(afterSize - beforeSize == 1)
}

@Test
fun set_entryExists_entryUpdated() {
val testBackend = backend

testBackend["FIRST_ENTRY"] = "updated-first-entry"

val expectedEntry = ConfigOption("FIRST_ENTRY", "updated-first-entry")
val entry = testBackend["FIRST_ENTRY"]

Assertions.assertEquals(expectedEntry, entry)
}

@Test
fun delete_entryExists_entryDoesNotExist() {
val testBackend = backend
testBackend.delete("ENTRY_PREFIX.THIRD_ENTRY")
Assertions.assertNull(testBackend["ENTRY_PREFIX.THIRD_ENTRY"])
}

@Test
fun delete_entryExists_exactlyOneEntryIsRemoved() {
val testBackend = backend
val sizeBefore = testBackend.list().size

testBackend.delete("ENTRY_PREFIX.THIRD_ENTRY")

val sizeAfter = testBackend.list().size

Assertions.assertTrue(sizeBefore - sizeAfter == 1)
}
}
6 changes: 6 additions & 0 deletions src/test/kotlin/sndl/parnas/Utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package sndl.parnas

import org.testcontainers.containers.GenericContainer

// See: https://github.com/testcontainers/testcontainers-java/issues/318
class KGenericContainer(imageName: String) : GenericContainer<KGenericContainer>(imageName)

0 comments on commit 33c40d8

Please sign in to comment.