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 bc5d490
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 22 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
14 changes: 9 additions & 5 deletions cmd/ftl/cmd_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ func (s *secretSetCmd) Run(ctx context.Context, scmd *secretCmd, adminClient adm
var err error
var secret []byte
if isatty.IsTerminal(0) {
fmt.Print("Secret: ")
secret, err = term.ReadPassword(0)
fmt.Println()
if err != nil {
Expand All @@ -124,18 +123,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, place an ftl-project.toml file in your git root directory, 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)
}
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
2 changes: 1 addition & 1 deletion examples/go/echo/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22.2

replace github.com/TBD54566975/ftl => ../../..

require github.com/TBD54566975/ftl v0.241.2
require github.com/TBD54566975/ftl v0.248.0

require (
connectrpc.com/connect v1.16.1 // indirect
Expand Down
2 changes: 1 addition & 1 deletion integration/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func DebugShell() Action {
// Exec runs a command from the test working directory.
func Exec(cmd string, args ...string) Action {
return func(t testing.TB, ic TestContext) {
Infof("Executing: %s %s", cmd, shellquote.Join(args...))
Infof("Executing (in %s): %s %s", ic.workDir, cmd, shellquote.Join(args...))
err := ftlexec.Command(ic, log.Debug, ic.workDir, cmd, args...).RunBuffered(ic)
assert.NoError(t, err)
}
Expand Down
3 changes: 3 additions & 0 deletions integration/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ func run(t *testing.T, ftlConfigPath string, startController bool, actions ...Ac
// can't be loaded until the module is copied over, and the config itself
// is used by FTL during startup.
t.Setenv("FTL_CONFIG", filepath.Join(cwd, "testdata", "go", ftlConfigPath))
} else {
err = os.WriteFile(filepath.Join(tmpDir, "ftl-project.toml"), []byte{}, 0644)
assert.NoError(t, err)
}

// Build FTL binary
Expand Down

0 comments on commit bc5d490

Please sign in to comment.