Skip to content

Commit

Permalink
fix: require an ftl-project.toml file
Browse files Browse the repository at this point in the history
This is a global requirement across all commands, not scoped
specifically to `ftl serve` or `ftl dev`, because eg. `ftl secret`/`ftl
config`, etc. etc. all require this config file, and it's just
cleaner/safer to do it in one location.

Fixes #1669
  • Loading branch information
alecthomas committed Jun 13, 2024
1 parent 44163b4 commit 90040a2
Show file tree
Hide file tree
Showing 16 changed files with 343 additions and 27 deletions.
4 changes: 2 additions & 2 deletions backend/controller/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func configProviderKey(p *ftlv1.ConfigProvider) string {
// ConfigSet sets the configuration at the given ref to the provided value.
func (s *AdminService) ConfigSet(ctx context.Context, req *connect.Request[ftlv1.SetConfigRequest]) (*connect.Response[ftlv1.SetConfigResponse], error) {
pkey := configProviderKey(req.Msg.Provider)
err := s.cm.Set(ctx, pkey, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name), string(req.Msg.Value))
err := s.cm.SetJSON(ctx, pkey, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name), req.Msg.Value)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -185,7 +185,7 @@ func secretProviderKey(p *ftlv1.SecretProvider) string {
// SecretSet sets the secret at the given ref to the provided value.
func (s *AdminService) SecretSet(ctx context.Context, req *connect.Request[ftlv1.SetSecretRequest]) (*connect.Response[ftlv1.SetSecretResponse], error) {
pkey := secretProviderKey(req.Msg.Provider)
err := s.sm.Set(ctx, pkey, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name), string(req.Msg.Value))
err := s.sm.SetJSON(ctx, pkey, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name), req.Msg.Value)
if err != nil {
return nil, err
}
Expand Down
15 changes: 10 additions & 5 deletions cmd/ftl/cmd_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (s *configGetCmd) Run(ctx context.Context, adminClient admin.Client) error
Ref: configRefFromRef(s.Ref),
}))
if err != nil {
return err
return fmt.Errorf("failed to get config: %w", err)
}
fmt.Printf("%s\n", resp.Msg.Value)
return nil
Expand All @@ -117,18 +117,23 @@ func (s *configSetCmd) Run(ctx context.Context, scmd *configCmd, adminClient adm
}
}

var configValue []byte
var configJSON json.RawMessage
if s.JSON {
if err := json.Unmarshal(config, &configValue); err != nil {
var jsonValue any
if err := json.Unmarshal(config, &jsonValue); err != nil {
return fmt.Errorf("config is not valid JSON: %w", err)
}
configJSON = config
} else {
configValue = config
configJSON, err = json.Marshal(string(config))
if err != nil {
return fmt.Errorf("failed to encode config as JSON: %w", err)
}
}

req := &ftlv1.SetConfigRequest{
Ref: configRefFromRef(s.Ref),
Value: configValue,
Value: configJSON,
}
if provider, ok := scmd.provider().Get(); ok {
req.Provider = &provider
Expand Down
15 changes: 10 additions & 5 deletions cmd/ftl/cmd_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func (s *secretGetCmd) Run(ctx context.Context, adminClient admin.Client) error
Ref: configRefFromRef(s.Ref),
}))
if err != nil {
return err
return fmt.Errorf("failed to get secret: %w", err)
}
fmt.Printf("%s\n", resp.Msg.Value)
return nil
Expand Down Expand Up @@ -124,18 +124,23 @@ func (s *secretSetCmd) Run(ctx context.Context, scmd *secretCmd, adminClient adm
}
}

var secretValue []byte
var secretJSON json.RawMessage
if s.JSON {
if err := json.Unmarshal(secret, &secretValue); err != nil {
var jsonValue any
if err := json.Unmarshal(secret, &jsonValue); err != nil {
return fmt.Errorf("secret is not valid JSON: %w", err)
}
secretJSON = secret
} else {
secretValue = secret
secretJSON, err = json.Marshal(string(secret))
if err != nil {
return fmt.Errorf("failed to encode secret as JSON: %w", err)
}
}

req := &ftlv1.SetSecretRequest{
Ref: configRefFromRef(s.Ref),
Value: secretValue,
Value: secretJSON,
}
if provider, ok := scmd.provider().Get(); ok {
req.Provider = &provider
Expand Down
17 changes: 14 additions & 3 deletions cmd/ftl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,25 @@ func main() {
logger := log.Configure(os.Stderr, cli.LogConfig)
ctx = log.ContextWithLogger(ctx, logger)

config, err := projectconfig.Load(ctx, cli.ConfigFlag)
configPath := cli.ConfigFlag
if configPath == "" {
var ok bool
configPath, ok = projectconfig.DefaultConfigPath().Get()
if !ok {
kctx.Fatalf("could not determine default config path, either place an ftl-project.toml file in the root of your project, use --config=FILE, or set the FTL_CONFIG envar")
}
}

os.Setenv("FTL_CONFIG", configPath)

config, err := projectconfig.Load(ctx, configPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
kctx.Fatalf(err.Error())
}
kctx.Bind(config)

sr := cf.ProjectConfigResolver[cf.Secrets]{Config: cli.ConfigFlag}
cr := cf.ProjectConfigResolver[cf.Configuration]{Config: cli.ConfigFlag}
sr := cf.ProjectConfigResolver[cf.Secrets]{Config: configPath}
cr := cf.ProjectConfigResolver[cf.Configuration]{Config: configPath}
kctx.BindTo(sr, (*cf.Resolver[cf.Secrets])(nil))
kctx.BindTo(cr, (*cf.Resolver[cf.Configuration])(nil))

Expand Down
28 changes: 22 additions & 6 deletions common/configuration/manager.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package configuration

import (
"bytes"
"context"
"encoding/json"
"errors"
Expand Down Expand Up @@ -108,18 +109,26 @@ func (m *Manager[R]) availableProviderKeys() []string {
return keys
}

// Set a configuration value.
// Set a configuration value, encoding "value" as JSON before storing it.
func (m *Manager[R]) Set(ctx context.Context, pkey string, ref Ref, value any) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return m.SetJSON(ctx, pkey, ref, data)
}

// SetJSON sets a configuration value using raw JSON data.
func (m *Manager[R]) SetJSON(ctx context.Context, pkey string, ref Ref, value json.RawMessage) error {
if err := checkJSON(value); err != nil {
return fmt.Errorf("invalid value for %s, must be JSON: %w", m.resolver.Role(), err)
}
provider, ok := m.providers[pkey]
if !ok {
pkeys := strings.Join(m.availableProviderKeys(), ", ")
return fmt.Errorf("no provider for key %q, specify one of: %s", pkey, pkeys)
}
data, err := json.Marshal(value)
if err != nil {
return err
}
key, err := provider.Store(ctx, ref, data)
key, err := provider.Store(ctx, ref, value)
if err != nil {
return err
}
Expand Down Expand Up @@ -173,3 +182,10 @@ func (m *Manager[R]) Unset(ctx context.Context, pkey string, ref Ref) error {
func (m *Manager[R]) List(ctx context.Context) ([]Entry, error) {
return m.resolver.List(ctx)
}

func checkJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
var v any
return dec.Decode(&v)
}
32 changes: 32 additions & 0 deletions common/projectconfig/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/alecthomas/assert/v2"

in "github.com/TBD54566975/ftl/integration"
"github.com/TBD54566975/ftl/internal/exec"
"github.com/TBD54566975/ftl/internal/log"
)

func TestCmdsCreateProjectTomlFilesIfNonexistent(t *testing.T) {
Expand Down Expand Up @@ -50,3 +52,33 @@ func TestConfigCmdWithoutController(t *testing.T) {
in.ExecWithExpectedOutput("\"value\"\n", "ftl", "config", "get", "key"),
)
}

func TestFindConfig(t *testing.T) {
checkConfig := func(subdir string) in.Action {
return func(t testing.TB, ic in.TestContext) {
in.Infof("Running ftl config list --values")
cmd := exec.Command(ic, log.Debug, filepath.Join(ic.WorkingDir(), subdir), "ftl", "config", "list", "--values")
cmd.Stdout = nil
cmd.Stderr = nil
output, err := cmd.CombinedOutput()
assert.NoError(t, err, "%s", output)
assert.Equal(t, "test = \"test\"\n", string(output))
in.Infof("Running ftl secret list --values")
cmd = exec.Command(ic, log.Debug, filepath.Join(ic.WorkingDir(), subdir), "ftl", "secret", "list", "--values")
cmd.Stdout = nil
cmd.Stderr = nil
output, err = cmd.CombinedOutput()
assert.NoError(t, err, "%s", output)
assert.Equal(t, "test = \"test\"\n", string(output))
}
}
in.RunWithoutController(t, "",
in.CopyModule("findconfig"),
checkConfig("findconfig"),
checkConfig("findconfig/subdir"),
in.SetEnv("FTL_CONFIG", func(ic in.TestContext) string {
return filepath.Join(ic.WorkingDir(), "findconfig", "ftl-project.toml")
}),
checkConfig("."),
)
}
22 changes: 18 additions & 4 deletions common/projectconfig/projectconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/alecthomas/types/optional"

"github.com/TBD54566975/ftl"
"github.com/TBD54566975/ftl/internal"
"github.com/TBD54566975/ftl/internal/log"
)

Expand Down Expand Up @@ -76,11 +75,26 @@ func DefaultConfigPath() optional.Option[string] {
}
return optional.Some(absPath)
}
gitRoot, ok := internal.GitRoot("").Get()
if !ok {
dir, err := os.Getwd()
if err != nil {
return optional.None[string]()
}
return optional.Some(filepath.Join(gitRoot, "ftl-project.toml"))
// Find the first ftl-project.toml file in the parent directories.
for {
path := filepath.Join(dir, "ftl-project.toml")
_, err := os.Stat(path)
if err == nil {
return optional.Some(path)
}
if !errors.Is(err, os.ErrNotExist) {
return optional.None[string]()
}
dir = filepath.Dir(dir)
if dir == "/" || dir == "." {
break
}
}
return optional.Some(filepath.Join(dir, "ftl-project.toml"))
}

// MaybeCreateDefault creates the ftl-project.toml file in the Git root if it
Expand Down
21 changes: 21 additions & 0 deletions common/projectconfig/testdata/go/findconfig/findconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package findconfig

import (
"context"
"fmt"

"github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK.
)

type EchoRequest struct {
Name ftl.Option[string] `json:"name"`
}

type EchoResponse struct {
Message string `json:"message"`
}

//ftl:verb
func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) {
return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil
}
5 changes: 5 additions & 0 deletions common/projectconfig/testdata/go/findconfig/ftl-project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[global]
[global.configuration]
test = "inline://InRlc3Qi"
[global.secrets]
test = "inline://InRlc3Qi"
2 changes: 2 additions & 0 deletions common/projectconfig/testdata/go/findconfig/ftl.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module = "findconfig"
language = "go"
46 changes: 46 additions & 0 deletions common/projectconfig/testdata/go/findconfig/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module ftl/findconfig

go 1.22.2

toolchain go1.22.4

require github.com/TBD54566975/ftl v1.1.5

require (
connectrpc.com/connect v1.16.1 // indirect
connectrpc.com/grpcreflect v1.2.0 // indirect
connectrpc.com/otelconnect v0.7.0 // indirect
github.com/alecthomas/concurrency v0.0.2 // indirect
github.com/alecthomas/participle/v2 v2.1.1 // indirect
github.com/alecthomas/types v0.16.0 // indirect
github.com/alessio/shellescape v1.4.2 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect
github.com/swaggest/jsonschema-go v0.3.70 // indirect
github.com/swaggest/refl v1.3.0 // indirect
github.com/zalando/go-keyring v0.2.5 // indirect
go.opentelemetry.io/otel v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)

replace github.com/TBD54566975/ftl => /Users/alec/dev/ftl
Loading

0 comments on commit 90040a2

Please sign in to comment.