Skip to content

Commit

Permalink
feat: add (hacky) support for config and secrets
Browse files Browse the repository at this point in the history
This currently loads from environment variables
`FTL_{CONFIG,SECRET}_<MODULE>_<KEY>`, which will eventually be delivered
by the Runner.
  • Loading branch information
alecthomas committed Jan 14, 2024
1 parent 3ae98d0 commit 7e6f705
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 15 deletions.
5 changes: 3 additions & 2 deletions go-runtime/compile/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down
62 changes: 62 additions & 0 deletions go-runtime/sdk/config.go
Original file line number Diff line number Diff line change
@@ -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]
}
13 changes: 13 additions & 0 deletions go-runtime/sdk/config_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
42 changes: 42 additions & 0 deletions go-runtime/sdk/secrets.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions kotlin-runtime/ftl-runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>2.1.0</version>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package xyz.block.ftl.config

import xyz.block.ftl.serializer.makeGson

class Secret<T>(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 <reified T> 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)
}
}
Original file line number Diff line number Diff line change
@@ -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<String>("test")
assertEquals("testingtesting", secret.get())
}
}
Original file line number Diff line number Diff line change
@@ -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<T>(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 <reified T> 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)
}
}
Original file line number Diff line number Diff line change
@@ -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<String>("test")
assertEquals("testingtesting", secret.get())
}
}
8 changes: 6 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
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">

<modelVersion>4.0.0</modelVersion>

Expand Down Expand Up @@ -195,6 +195,10 @@
<include>Test*</include>
<include>*Test</include>
</includes>
<argLine>
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
<plugin>
Expand Down

0 comments on commit 7e6f705

Please sign in to comment.