From 7e6f705f78109fca328c9b890fce10ecb110ec71 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 14 Jan 2024 19:47:06 +1100 Subject: [PATCH] feat: add (hacky) support for config and secrets This currently loads from environment variables `FTL_{CONFIG,SECRET}__`, which will eventually be delivered by the Runner. --- go-runtime/compile/build.go | 5 +- go-runtime/sdk/config.go | 62 +++++++++++++++++++ go-runtime/sdk/config_test.go | 13 ++++ go-runtime/sdk/secrets.go | 42 +++++++++++++ kotlin-runtime/ftl-runtime/pom.xml | 7 +++ .../kotlin/xyz/block/ftl/config/Config.kt | 21 +++++++ .../kotlin/xyz/block/ftl/config/ConfigTest.kt | 14 +++++ .../kotlin/xyz/block/ftl/secrets/Secrets.kt | 26 ++++---- .../xyz/block/ftl/secrets/SecretTest.kt | 14 +++++ pom.xml | 8 ++- 10 files changed, 197 insertions(+), 15 deletions(-) create mode 100644 go-runtime/sdk/config.go create mode 100644 go-runtime/sdk/config_test.go create mode 100644 go-runtime/sdk/secrets.go create mode 100644 kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/Config.kt create mode 100644 kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/ConfigTest.kt create mode 100644 kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/secrets/SecretTest.kt diff --git a/go-runtime/compile/build.go b/go-runtime/compile/build.go index c98a9553e6..e906f70a12 100644 --- a/go-runtime/compile/build.go +++ b/go-runtime/compile/build.go @@ -10,10 +10,11 @@ import ( "reflect" "strings" - "github.com/TBD54566975/scaffolder" "github.com/iancoleman/strcase" "google.golang.org/protobuf/proto" + "github.com/TBD54566975/scaffolder" + "github.com/TBD54566975/ftl/backend/common/exec" "github.com/TBD54566975/ftl/backend/common/log" "github.com/TBD54566975/ftl/backend/common/moduleconfig" @@ -88,7 +89,7 @@ func Build(ctx context.Context, moduleDir string, sch *schema.Schema) error { if err := exec.Command(ctx, log.Debug, mainDir, "go", "mod", "tidy").Run(); err != nil { return fmt.Errorf("failed to tidy go.mod: %w", err) } - return exec.Command(ctx, log.Debug, mainDir, "go", "build", "-o", "../../main", ".").Run() + return exec.Command(ctx, log.Info, mainDir, "go", "build", "-o", "../../main", ".").Run() } var scaffoldFuncs = scaffolder.FuncMap{ diff --git a/go-runtime/sdk/config.go b/go-runtime/sdk/config.go new file mode 100644 index 0000000000..b75c8cf26a --- /dev/null +++ b/go-runtime/sdk/config.go @@ -0,0 +1,62 @@ +package sdk + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "strings" +) + +// ConfigType is a type that can be used as a configuration value. +// +// Supported types are currently limited, but will eventually be extended to +// allow any type that FTL supports, including structs. +type ConfigType interface { + string | int | float64 | bool | + []string | []int | []float64 | []bool | + map[string]string | map[string]int | map[string]float64 | map[string]bool +} + +// Config declares a typed configuration key for the current module. +func Config[Type ConfigType](name string) ConfigValue[Type] { + module := callerModule() + return ConfigValue[Type]{module, name} +} + +// ConfigValue is a typed configuration key for the current module. +type ConfigValue[Type ConfigType] struct { + module string + name string +} + +// Get returns the value of the configuration key from FTL. +func (c *ConfigValue[Type]) Get() (out Type) { + value, ok := os.LookupEnv(fmt.Sprintf("FTL_CONFIG_%s_%s", strings.ToUpper(c.module), strings.ToUpper(c.name))) + if !ok { + return out + } + if err := json.Unmarshal([]byte(value), &out); err != nil { + panic(fmt.Errorf("failed to parse config value %s: %w", value, err)) + } + return +} + +func callerModule() string { + pc, _, _, ok := runtime.Caller(2) + if !ok { + panic("failed to get caller") + } + details := runtime.FuncForPC(pc) + if details == nil { + panic("failed to get caller") + } + module := details.Name() + if strings.HasPrefix(module, "github.com/TBD54566975/ftl/go-runtime/sdk") { + return "testing" + } + if !strings.HasPrefix(module, "ftl/") { + panic(fmt.Sprintf("must be called from an FTL module not %s", module)) + } + return strings.Split(strings.Split(module, "/")[1], ".")[0] +} diff --git a/go-runtime/sdk/config_test.go b/go-runtime/sdk/config_test.go new file mode 100644 index 0000000000..08140899d3 --- /dev/null +++ b/go-runtime/sdk/config_test.go @@ -0,0 +1,13 @@ +package sdk + +import ( + "testing" + + "github.com/alecthomas/assert/v2" +) + +func TestConfig(t *testing.T) { + t.Setenv("FTL_CONFIG_TESTING_TEST", `["one", "two", "three"]`) + config := Config[[]string]("test") + assert.Equal(t, []string{"one", "two", "three"}, config.Get()) +} diff --git a/go-runtime/sdk/secrets.go b/go-runtime/sdk/secrets.go new file mode 100644 index 0000000000..c7850d69a5 --- /dev/null +++ b/go-runtime/sdk/secrets.go @@ -0,0 +1,42 @@ +package sdk + +import ( + "encoding/json" + "fmt" + "os" + "strings" +) + +// SecretType is a type that can be used as a secret value. +// +// Supported types are currently limited, but will eventually be extended to +// allow any type that FTL supports, including structs. +type SecretType interface { + string | int | float64 | bool | + []string | []int | []float64 | []bool | + map[string]string | map[string]int | map[string]float64 | map[string]bool +} + +// Secret declares a typed secret for the current module. +func Secret[Type SecretType](name string) SecretValue[Type] { + module := callerModule() + return SecretValue[Type]{module, name} +} + +// SecretValue is a typed secret for the current module. +type SecretValue[Type SecretType] struct { + module string + name string +} + +// Get returns the value of the secret from FTL. +func (c *SecretValue[Type]) Get() (out Type) { + value, ok := os.LookupEnv(fmt.Sprintf("FTL_SECRET_%s_%s", strings.ToUpper(c.module), strings.ToUpper(c.name))) + if !ok { + return out + } + if err := json.Unmarshal([]byte(value), &out); err != nil { + panic(fmt.Errorf("failed to parse secret %s.%s: %w", c.module, c.name, err)) + } + return +} diff --git a/kotlin-runtime/ftl-runtime/pom.xml b/kotlin-runtime/ftl-runtime/pom.xml index 6970e10ed8..53ded133cf 100644 --- a/kotlin-runtime/ftl-runtime/pom.xml +++ b/kotlin-runtime/ftl-runtime/pom.xml @@ -109,6 +109,13 @@ test + + org.junit-pioneer + junit-pioneer + 2.1.0 + test + + diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/Config.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/Config.kt new file mode 100644 index 0000000000..5aa90467e9 --- /dev/null +++ b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/Config.kt @@ -0,0 +1,21 @@ +package xyz.block.ftl.config + +import xyz.block.ftl.serializer.makeGson + +class Secret(val name: String) { + val _module: String + val _gson = makeGson() + + init { + val caller = Thread.currentThread().stackTrace[2].className + require(caller.startsWith("ftl.") || caller.startsWith("xyz.block.ftl.config.")) { "Config must be defined in an FTL module not $caller" } + val parts = caller.split(".") + _module = parts[parts.size - 2] + } + + inline fun get(): T { + val key = "FTL_CONFIG_${_module.uppercase()}_${name.uppercase()}" + val value = System.getenv(key) ?: throw Exception("Config key ${_module}.${name} not found") + return _gson.fromJson(value, T::class.java) + } +} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/ConfigTest.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/ConfigTest.kt new file mode 100644 index 0000000000..2faeb242ef --- /dev/null +++ b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/ConfigTest.kt @@ -0,0 +1,14 @@ +package xyz.block.ftl.config + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junitpioneer.jupiter.SetEnvironmentVariable + +class ConfigTest { + @Test + @SetEnvironmentVariable(key = "FTL_SECRET_SECRETS_TEST", value = "testingtesting") + fun testSecret() { + val secret = Secret("test") + assertEquals("testingtesting", secret.get()) + } +} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/secrets/Secrets.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/secrets/Secrets.kt index 1e6f4e0a1b..bfe1baf244 100644 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/secrets/Secrets.kt +++ b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/secrets/Secrets.kt @@ -1,17 +1,21 @@ package xyz.block.ftl.secrets -object Secrets { - private const val FTL_SECRETS_ENV_VAR_PREFIX = "FTL_SECRET_" +import xyz.block.ftl.serializer.makeGson - fun get(name: String): String { - if (!name.startsWith(FTL_SECRETS_ENV_VAR_PREFIX)) { - throw Exception("Invalid secret name; must start with $FTL_SECRETS_ENV_VAR_PREFIX") - } +class Secret(val name: String) { + val module: String + val gson = makeGson() - return try { - System.getenv(name) - } catch (e: Exception) { - throw Exception("Secret $name not found") - } + init { + val caller = Thread.currentThread().getStackTrace()[2].className + require(caller.startsWith("ftl.") || caller.startsWith("xyz.block.ftl.secrets.")) { "Secrets must be defined in an FTL module not ${caller}" } + val parts = caller.split(".") + module = parts[parts.size - 2] + } + + inline fun get(): T { + val key = "FTL_SECRET_${module.uppercase()}_${name.uppercase()}" + val value = System.getenv(key) ?: throw Exception("Secret ${module}.${name} not found") + return gson.fromJson(value, T::class.java) } } diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/secrets/SecretTest.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/secrets/SecretTest.kt new file mode 100644 index 0000000000..ba3a0fd440 --- /dev/null +++ b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/secrets/SecretTest.kt @@ -0,0 +1,14 @@ +package xyz.block.ftl.secrets + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junitpioneer.jupiter.SetEnvironmentVariable + +class SecretTest { + @Test + @SetEnvironmentVariable(key = "FTL_SECRET_SECRETS_TEST", value = "testingtesting") + fun testSecret() { + val secret = Secret("test") + assertEquals("testingtesting", secret.get()) + } +} diff --git a/pom.xml b/pom.xml index 964db394dd..576444a43c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 @@ -195,6 +195,10 @@ Test* *Test + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED +