diff --git a/README.md b/README.md index 1d9c89f..e583cef 100644 --- a/README.md +++ b/README.md @@ -17,20 +17,24 @@ Usage example in you build script: ```kotlin import com.liftric.vault.vault +import com.liftric.vault.GetVaultSecretTask plugins { id("com.liftric.vault-client-plugin") version ("") } vault { - vaultAddress = "http://localhost:8200" - vaultToken = "myroottoken" // don't do that in production code! - vaultTokenFilePath = "${System.getProperty("user.home")}/.vault-token" // from file is prefered over vaultToken - maxRetries = 2 - retryIntervalMilliseconds = 200 + vaultAddress.set("http://localhost:8200") + vaultToken.set("myroottoken") // don't do that in production code! + vaultTokenFilePath.set("${System.getProperty("user.home")}/.vault-token") // from file is prefered over vaultToken + maxRetries.set(2) + retryIntervalMilliseconds.set(200) } tasks { - val needsSecrets by creating { - val secrets: Map = project.vault("secret/example") + val needsSecrets by creating(GetVaultSecretTask::class) { + secretPath.set("secret/example") + doLast { + val secrets: Map = secret.get() + } } } ``` @@ -68,10 +72,17 @@ import com.liftric.vault.vault import org.gradle.api.Project object Configs { - fun Project.secretStuff(): String { + fun Project.secretStuff(): Any { val secrets = project.vault("secret/example") [...] // use the secrets } + fun Project.secretStuff(): Any { + val needsSecrets: GetVaultSecretTask = tasks.getByName("needsSecrets").apply { + execute() + } + val secret = needsSecrets.secret.get() + [...] // use the secrets + } } ``` This can be used in your build scripts like: diff --git a/build.gradle.kts b/build.gradle.kts index d132e74..1278262 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,9 @@ import net.nemerosa.versioning.tasks.VersionDisplayTask plugins { - kotlin("jvm") version "1.3.61" - `java-gradle-plugin` - id("org.gradle.kotlin.kotlin-dsl") version "1.3.3" + `kotlin-dsl` `maven-publish` - id("com.gradle.plugin-publish") version "0.10.1" + id("com.gradle.plugin-publish") version "0.11.0" id("net.nemerosa.versioning") version "2.12.0" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6254d2d..6623300 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/integration-token/build.gradle.kts b/integration-token/build.gradle.kts index bf3ce45..12bc301 100644 --- a/integration-token/build.gradle.kts +++ b/integration-token/build.gradle.kts @@ -1,22 +1,24 @@ import com.liftric.vault.vault +import com.liftric.vault.GetVaultSecretTask plugins { java id("com.liftric.vault-client-plugin") // version known from buildSrc } vault { - vaultAddress = "http://localhost:8200" - vaultToken = "myroottoken" // don't do that in production code! - maxRetries = 2 - retryIntervalMilliseconds = 200 + vaultAddress.set("http://localhost:8200") + vaultToken.set("myroottoken") // don't do that in production code! + maxRetries.set(2) + retryIntervalMilliseconds.set(200) } tasks { - val needsSecrets by creating { + val needsSecrets by creating(GetVaultSecretTask::class) { + secretPath.set("secret/example") doLast { - val secrets: Map = project.vault("secret/example") - if (secrets["examplestring"] != "helloworld") throw kotlin.IllegalStateException("examplestring couldn't be read") - if (secrets["exampleint"]?.toInt() != 1337) throw kotlin.IllegalStateException("exampleint couldn't be read") - println("getting secrets succeeded!") + val secret = secret.get() + if (secret["examplestring"] != "helloworld") throw kotlin.IllegalStateException("examplestring couldn't be read") + if (secret["exampleint"]?.toInt() != 1337) throw kotlin.IllegalStateException("exampleint couldn't be read") + println("getting secret succeeded!") } } val fromBuildSrc by creating { diff --git a/integration-token/buildSrc/src/main/kotlin/Configs.kt b/integration-token/buildSrc/src/main/kotlin/Configs.kt index 4ce6cce..1661434 100644 --- a/integration-token/buildSrc/src/main/kotlin/Configs.kt +++ b/integration-token/buildSrc/src/main/kotlin/Configs.kt @@ -1,9 +1,13 @@ -import com.liftric.vault.vault +import com.liftric.vault.GetVaultSecretTask import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByName object Configs { fun Project.secretStuff(): String { - val secrets = project.vault("secret/example") - return "${secrets["examplestring"]}:${secrets["exampleint"]}" + val needsSecrets: GetVaultSecretTask = tasks.getByName("needsSecrets").apply { + execute() + } + val secret = needsSecrets.secret.get() + return "${secret["examplestring"]}:${secret["exampleint"]}" } -} \ No newline at end of file +} diff --git a/integration-tokenfile/build.gradle.kts b/integration-tokenfile/build.gradle.kts index f2c4e1f..04dfdab 100644 --- a/integration-tokenfile/build.gradle.kts +++ b/integration-tokenfile/build.gradle.kts @@ -1,23 +1,24 @@ import com.liftric.vault.vault +import com.liftric.vault.GetVaultSecretTask plugins { java - id("com.liftric.vault-client-plugin") version "1.0.0-SNAPSHOT" + id("com.liftric.vault-client-plugin") } vault { - vaultAddress = "http://localhost:8200" - vaultTokenFilePath = "${projectDir.path}/.vault-token" // don't put the token file on the repo itself like here! - maxRetries = 2 - retryIntervalMilliseconds = 200 + vaultAddress.set("http://localhost:8200") + vaultTokenFilePath.set("${projectDir.path}/.vault-token") // don't put the token file on the repo itself like here! + maxRetries.set(2) + retryIntervalMilliseconds.set(200) } tasks { - val needsSecrets by creating { - val secrets = project.objects.mapProperty() + val needsSecrets by creating(GetVaultSecretTask::class) { + secretPath.set("secret/example") doLast { - secrets.set(project.vault("secret/example")) - if (secrets.get()["examplestring"] != "helloworld") throw kotlin.IllegalStateException("examplestring couldn't be read") - if (secrets.get()["exampleint"]?.toInt() != 1337) throw kotlin.IllegalStateException("exampleint couldn't be read") - println("getting secrets succeeded!") + val secret = secret.get() + if (secret["examplestring"] != "helloworld") throw kotlin.IllegalStateException("examplestring couldn't be read") + if (secret["exampleint"]?.toInt() != 1337) throw kotlin.IllegalStateException("exampleint couldn't be read") + println("getting secret succeeded!") } } val build by existing { diff --git a/src/main/kotlin/com/liftric/vault/Defaults.kt b/src/main/kotlin/com/liftric/vault/Defaults.kt new file mode 100644 index 0000000..5a8123c --- /dev/null +++ b/src/main/kotlin/com/liftric/vault/Defaults.kt @@ -0,0 +1,12 @@ +package com.liftric.vault + +object Defaults { + // values + const val MAX_RETRIES = 5 + const val RETRY_INTERVAL_MILLI = 1000 + + // env vars + const val VAULT_TOKEN_ENV = "VAULT_TOKEN" + const val VAULT_TOKEN_FILE_PATH_ENV = "VAULT_TOKEN_FILE_PATH" + const val VAULT_ADDR_ENV = "VAULT_ADDR" +} diff --git a/src/main/kotlin/com/liftric/vault/GetVaultSecretTask.kt b/src/main/kotlin/com/liftric/vault/GetVaultSecretTask.kt new file mode 100644 index 0000000..8292a11 --- /dev/null +++ b/src/main/kotlin/com/liftric/vault/GetVaultSecretTask.kt @@ -0,0 +1,91 @@ +package com.liftric.vault + +import com.liftric.vault.Defaults.MAX_RETRIES +import com.liftric.vault.Defaults.RETRY_INTERVAL_MILLI +import com.liftric.vault.Defaults.VAULT_ADDR_ENV +import com.liftric.vault.Defaults.VAULT_TOKEN_ENV +import com.liftric.vault.Defaults.VAULT_TOKEN_FILE_PATH_ENV +import org.gradle.api.DefaultTask +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.mapProperty +import org.gradle.kotlin.dsl.property +import java.io.File + +/** + * See extension for property documentation + */ +open class GetVaultSecretTask : DefaultTask() { + init { + group = "com.liftric.vault" + description = "Fetches a secret from vault." + outputs.upToDateWhen { false } + } + + @Input + val secretPath: Property = project.objects.property() + + @Input + @Optional + val vaultAddress: Property = project.objects.property() + + @Input + @Optional + val vaultToken: Property = project.objects.property() + + @Input + @Optional + val vaultTokenFilePath: Property = project.objects.property() + + @Input + @Optional + val maxRetries: Property = project.objects.property() + + @Input + @Optional + val retryIntervalMilliseconds: Property = project.objects.property() + + @Internal + // actually used as output... + val secret: MapProperty = project.objects.mapProperty() + + @TaskAction + fun execute() { + val token = determineToken(vaultToken = vaultToken.orNull, vaultTokenFilePath = vaultTokenFilePath.orNull) + val address = determinAddress(vaultAddress = vaultAddress.orNull) + val maxRetries = maxRetries.getOrElse(MAX_RETRIES) + val retryIntervalMilliseconds = retryIntervalMilliseconds.getOrElse(RETRY_INTERVAL_MILLI) + val path = secretPath.get() + println("[vault] getting `$path` from $address") + secret.set( + VaultClient( + token = token, + vaultAddress = address, + maxRetries = maxRetries, + retryIntervalMilliseconds = retryIntervalMilliseconds + ).get(path) + ) + } + + companion object { + fun determineToken(vaultToken: String?, vaultTokenFilePath: String?): String { + val finalVaultToken = vaultToken ?: System.getenv()[VAULT_TOKEN_ENV]?.trim() + val finalVaultTokenFilePath = vaultTokenFilePath ?: System.getenv()[VAULT_TOKEN_FILE_PATH_ENV]?.trim() + return when { + finalVaultToken != null -> finalVaultToken.trim() + finalVaultTokenFilePath != null -> File(finalVaultTokenFilePath).apply { + if (exists().not()) error("vault token file doesn't exist!") + }.let { + return@let it.readText().trim() + } + else -> error("neither `vaultToken` nor `vaultTokenFilePath` nor `$VAULT_TOKEN_FILE_PATH_ENV` env var nor `$VAULT_TOKEN_ENV` env var provided!") + } + } + + fun determinAddress(vaultAddress: String?): String { + val finalVaultAddress = vaultAddress ?: System.getenv()[VAULT_ADDR_ENV] + return finalVaultAddress?.trim() ?: error("neither `vaultAddress` nor `$VAULT_ADDR_ENV` env var provided!") + } + } +} diff --git a/src/main/kotlin/com/liftric/vault/VaultClient.kt b/src/main/kotlin/com/liftric/vault/VaultClient.kt index e601800..99d2a86 100644 --- a/src/main/kotlin/com/liftric/vault/VaultClient.kt +++ b/src/main/kotlin/com/liftric/vault/VaultClient.kt @@ -2,32 +2,70 @@ package com.liftric.vault import com.bettercloud.vault.Vault import com.bettercloud.vault.VaultConfig +import com.bettercloud.vault.VaultException /** * Actual client connecting to the configured vault server */ class VaultClient( - private val extension: VaultClientExtension, - token: String + token: String, + private val vaultAddress: String, + private val maxRetries: Int, + private val retryIntervalMilliseconds: Int ) { private val config by lazy { - VaultConfig() - .address(extension.vaultAddress) - .token(token) - .build() + try { + VaultConfig() + .address(vaultAddress) + .token(token) + .build() + } catch (e: VaultException) { + println("[vault] exception while creating vault client for $vaultAddress: ${e.message}") + throw e + } + } + private val vault by lazy { + try { + Vault(config) + } catch (e: VaultException) { + println( + "[vault] exception while preparing vault client config for $vaultAddress: ${e.message} - token valid?" + .replace('\n', '#') + ) + throw e + } + } + + fun get(secretPath: String): Map { + verifyTokenValid() + return try { + vault.withRetries(maxRetries, retryIntervalMilliseconds) + .logical() + .read(secretPath) + .data.also { + if (it.isEmpty()) error("[vault] secret response contains no data - secret exists? token has correct rights to access it?") + } + } catch (e: VaultException) { + println( + "[vault] exception while calling vault at $vaultAddress: ${e.message} - secret exists? token has correct rights to access it?" + .replace('\n', '#') + ) + if (vaultAddress.startsWith("https").not()) { + println("[vault] is your vault address correct? It doesn't start with https!") + } + throw e + } } - private val vault by lazy { Vault(config) } - fun get(secretPath: String): Map = try { - vault.withRetries(extension.maxRetries, extension.retryIntervalMilliseconds) - .logical() - .read(secretPath) - .data - } catch (e: Exception) { - println("[vault] exception while calling vault at ${extension.vaultAddress}: ${e.message}") - if (extension.vaultAddress?.startsWith("https") == false) { - println("[vault] is your vault address correct? It doesn't start with https!") + private fun verifyTokenValid() { + try { + vault.auth().lookupSelf() + } catch (e: VaultException) { + println( + "[vault] exception while verifying vault token validity for $vaultAddress: ${e.message}" + .replace('\n', '#') + ) + throw e } - throw e } } diff --git a/src/main/kotlin/com/liftric/vault/VaultClientExtension.kt b/src/main/kotlin/com/liftric/vault/VaultClientExtension.kt index cce8419..dad8b74 100644 --- a/src/main/kotlin/com/liftric/vault/VaultClientExtension.kt +++ b/src/main/kotlin/com/liftric/vault/VaultClientExtension.kt @@ -1,11 +1,44 @@ package com.liftric.vault import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.gradle.kotlin.dsl.property -open class VaultClientExtension(val project: Project) { - var vaultAddress: String? = System.getenv()["VAULT_ADDR"] - var vaultToken: String? = System.getenv()["VAULT_TOKEN"] - var vaultTokenFilePath: String? = System.getenv()["VAULT_TOKEN_FILE_PATH"] - var maxRetries: Int = 5 - var retryIntervalMilliseconds = 1000 +open class VaultClientExtension(project: Project) { + /** + * vault address to be used. will check env var `VAULT_ADDR` if unset + */ + @Input + @Optional + val vaultAddress: Property = project.objects.property() + + /** + * vault token to be used (it's recommend you don't set this in your build file). will check env var `VAULT_TOKEN` if unset + */ + @Input + @Optional + val vaultToken: Property = project.objects.property() + + /** + * vault token file path (if set has precedence over vaultToken). will check env var `VAULT_TOKEN_FILE_PATH` if unset + */ + @Input + @Optional + val vaultTokenFilePath: Property = project.objects.property() + + /** + * vault client max retry count + */ + @Input + @Optional + val maxRetries: Property = project.objects.property() + + /** + * time between vault request retries + */ + @Input + @Optional + val retryIntervalMilliseconds: Property = project.objects.property() } diff --git a/src/main/kotlin/com/liftric/vault/VaultClientPlugin.kt b/src/main/kotlin/com/liftric/vault/VaultClientPlugin.kt index a1f1159..48365e9 100644 --- a/src/main/kotlin/com/liftric/vault/VaultClientPlugin.kt +++ b/src/main/kotlin/com/liftric/vault/VaultClientPlugin.kt @@ -2,13 +2,21 @@ package com.liftric.vault import org.gradle.api.Plugin import org.gradle.api.Project -import java.io.File private const val extensionName = "vault" class VaultClientPlugin : Plugin { override fun apply(project: Project) { - project.extensions.create(extensionName, VaultClientExtension::class.java, project) + val extension = project.extensions.create(extensionName, VaultClientExtension::class.java, project) + project.tasks.withType(GetVaultSecretTask::class.java) { + project.afterEvaluate { + vaultAddress.set(extension.vaultAddress) + vaultToken.set(extension.vaultToken) + vaultTokenFilePath.set(extension.vaultTokenFilePath) + maxRetries.set(extension.maxRetries) + retryIntervalMilliseconds.set(extension.retryIntervalMilliseconds) + } + } } } @@ -16,36 +24,3 @@ fun Project.vault(): VaultClientExtension { return extensions.getByName(extensionName) as? VaultClientExtension ?: throw IllegalStateException("$extensionName is not of the correct type") } - -fun Project.vault(secretPath: String): Map { - println("[vault] loading $secretPath") - val vault = project.vault() - if (vault.vaultAddress == null) { - throw IllegalStateException("neither the env variable `VAULT_ADDR` nor the `vaultAddress` config set") - } - val tokenPresent = vault.vaultToken != null - val filePathPresent = vault.vaultTokenFilePath != null - - val token = when { - tokenPresent.not() && filePathPresent.not() - -> throw IllegalStateException("neither vaultToken (prefer ENV VAR) nor vaultTokenFilePath set") - tokenPresent && filePathPresent.not() - -> vault.vaultToken!! - filePathPresent && tokenPresent.not() - -> getTokenFileContent(vault.vaultTokenFilePath!!) - tokenPresent && filePathPresent - -> { - println("[warn] vault token and token file path set: will choose from file path!") - getTokenFileContent(vault.vaultTokenFilePath!!) - } - else -> throw IllegalStateException("unknown error during token reading") - } - val client = VaultClient(vault, token) - return client.get(secretPath) -} - -private fun getTokenFileContent(filePath: String) = File(filePath).let { - if (it.exists().not()) - throw IllegalStateException("no file found for given vaultTokenFilePath: ${it.absolutePath}") - it.readText().trim() -} diff --git a/src/test/kotlin/com/liftric/vault/VaultClientPluginTest.kt b/src/test/kotlin/com/liftric/vault/VaultClientPluginTest.kt index a283970..9bd5ce6 100644 --- a/src/test/kotlin/com/liftric/vault/VaultClientPluginTest.kt +++ b/src/test/kotlin/com/liftric/vault/VaultClientPluginTest.kt @@ -1,9 +1,6 @@ package com.liftric.vault -import com.bettercloud.vault.VaultException import junit.framework.TestCase.* -import org.gradle.kotlin.dsl.extra -import org.gradle.kotlin.dsl.provideDelegate import org.gradle.testfixtures.ProjectBuilder import org.junit.Rule import org.junit.Test @@ -28,22 +25,4 @@ class VaultClientPluginTest { assertNotNull(project.vault()) } - @Test - fun testExtensionDefaults() { - environmentVariables.clear("VAULT_ADDR", "VAULT_TOKEN") - VaultClientExtension(ProjectBuilder.builder().build()).apply { - assertNull(vaultAddress) - assertNull(vaultToken) - } - } - - @Test - fun testExtensionFromEnv() { - environmentVariables.set("VAULT_ADDR", "aabb") - environmentVariables.set("VAULT_TOKEN", "aacc") - VaultClientExtension(ProjectBuilder.builder().build()).apply { - assertEquals("aabb", vaultAddress) - assertEquals("aacc", vaultToken) - } - } }