From 8c6f74098cfbf0b173ffd87b6047530e5aeb63a8 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Mon, 28 Oct 2024 14:52:30 +1100 Subject: [PATCH] chore: improve "ftl profile new" help (#3209) --- frontend/cli/cmd_profile.go | 31 ++++++++++++-- frontend/cli/main.go | 44 +++++++++++++------- internal/configuration/api.go | 2 + internal/configuration/providers/registry.go | 7 ++++ 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/frontend/cli/cmd_profile.go b/frontend/cli/cmd_profile.go index 3be6613104..4eafa47f49 100644 --- a/frontend/cli/cmd_profile.go +++ b/frontend/cli/cmd_profile.go @@ -18,7 +18,7 @@ type profileCmd struct { List profileListCmd `cmd:"" help:"List all profiles."` Default profileDefaultCmd `cmd:"" help:"Set a profile as default."` Switch profileSwitchCmd `cmd:"" help:"Switch locally active profile."` - New profileNewCmd `cmd:"" help:"Create a new profile."` + New profileNewCmd `cmd:"" help:"Create a new local or remote profile."` } type profileInitCmd struct { @@ -102,12 +102,35 @@ func (p profileSwitchCmd) Run(project *profiles.Project) error { type profileNewCmd struct { Local bool `help:"Create a local profile." xor:"location" and:"providers"` - Remote *url.URL `help:"Create a remote profile." xor:"location"` - Secrets configuration.ProviderKey `help:"Secrets provider." placeholder:"PROVIDER" default:"inline" and:"providers"` - Configuration configuration.ProviderKey `help:"Configuration provider." placeholder:"PROVIDER" default:"inline" and:"providers"` + Remote *url.URL `help:"Create a remote profile." xor:"location" placeholder:"ENDPOINT"` + Secrets configuration.ProviderKey `help:"Secrets provider (one of ${enum})." enum:"${secretProviders}" default:"inline" and:"providers"` + Configuration configuration.ProviderKey `help:"Configuration provider (one of ${enum})." enum:"${configProviders}" default:"inline" and:"providers"` Name string `arg:"" help:"Profile name."` } +func (profileNewCmd) Help() string { + return ` +Specify either --local or --remote=ENDPOINT to create a new profile. + +A local profile (specified via --local) is used for local development and testing, and can be managed without a running +FTL cluster. In a local profile, secrets and configuration are stored in locally accessible secret stores, including +1Password (--secrets=op), Keychain (--secrets=keychain), and local files (--secrets=inline). + +A remote profile (specified via --remote=ENDPOINT) is used for persistent cloud deployments. In a remote profile, secrets +and configuration are managed by the FTL cluster. + +eg. + +Create a new local profile with secrets stored in the Keychain, and configuration stored inline: + + ftl profile new devel --local --secrets=keychain + +Create a new remote profile: + + ftl profile new staging --remote=https://ftl.example.com +` +} + func (p profileNewCmd) Run(project *profiles.Project) error { var config either.Either[profiles.LocalProfileConfig, profiles.RemoteProfileConfig] switch { diff --git a/frontend/cli/main.go b/frontend/cli/main.go index a68ed24d52..c72d78fdde 100644 --- a/frontend/cli/main.go +++ b/frontend/cli/main.go @@ -9,6 +9,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "syscall" "github.com/alecthomas/kong" @@ -29,6 +30,7 @@ import ( "github.com/TBD54566975/ftl/internal/profiles" "github.com/TBD54566975/ftl/internal/projectconfig" "github.com/TBD54566975/ftl/internal/rpc" + "github.com/TBD54566975/ftl/internal/slices" "github.com/TBD54566975/ftl/internal/terminal" ) @@ -155,6 +157,9 @@ func main() { func createKongApplication(cli any, csm *currentStatusManager) *kong.Kong { gitRoot, _ := internal.GitRoot(".").Get() + configRegistry, secretsRegistry := makeConfigurationRegistries() + configProviders := slices.Map(configRegistry.Providers(), func(key configuration.ProviderKey) string { return key.String() }) + secretProviders := slices.Map(secretsRegistry.Providers(), func(key configuration.ProviderKey) string { return key.String() }) app := kong.Must(cli, kong.Description(`FTL - Towards a 𝝺-calculus for large-scale systems`), kong.Configuration(kongtoml.Loader, ".ftl.toml", "~/.ftl.toml"), @@ -168,13 +173,15 @@ func createKongApplication(cli any, csm *currentStatusManager) *kong.Kong { return &kong.Group{Key: node.Name, Title: "Command flags:"} }), kong.Vars{ - "version": ftl.Version, - "os": runtime.GOOS, - "arch": runtime.GOARCH, - "numcpu": strconv.Itoa(runtime.NumCPU()), - "gitroot": gitRoot, - "dsn": dsn.DSN("ftl"), - "boxdsn": dsn.DSN("ftl", dsn.Port(5432)), + "version": ftl.Version, + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "numcpu": strconv.Itoa(runtime.NumCPU()), + "gitroot": gitRoot, + "dsn": dsn.DSN("ftl"), + "boxdsn": dsn.DSN("ftl", dsn.Port(5432)), + "configProviders": strings.Join(configProviders, ","), + "secretProviders": strings.Join(secretProviders, ","), }, kong.Exit(func(code int) { if sm, ok := csm.statusManager.Get(); ok { @@ -200,17 +207,10 @@ func makeBindContext(projectConfig projectconfig.Config, logger *log.Logger, can ctx = rpc.ContextWithClient(ctx, provisionerServiceClient) kctx.BindTo(provisionerServiceClient, (*provisionerconnect.ProvisionerServiceClient)(nil)) - // Initialise configuration registries. - configRegistry := providers.NewRegistry[configuration.Configuration]() - configRegistry.Register(providers.NewEnvarFactory[configuration.Configuration]()) - configRegistry.Register(providers.NewInlineFactory[configuration.Configuration]()) - kctx.Bind(configRegistry) - secretsRegistry := providers.NewRegistry[configuration.Secrets]() - secretsRegistry.Register(providers.NewEnvarFactory[configuration.Secrets]()) - secretsRegistry.Register(providers.NewInlineFactory[configuration.Secrets]()) + configRegistry, secretsRegistry := makeConfigurationRegistries() + kctx.Bind(configRegistry, secretsRegistry) kongcompletion.Register(kctx.Kong, kongcompletion.WithPredictors(terminal.Predictors(ctx, controllerServiceClient))) - kctx.Bind(secretsRegistry) verbServiceClient := rpc.Dial(ftlv1connect.NewVerbServiceClient, cli.Endpoint.String(), log.Error) ctx = rpc.ContextWithClient(ctx, verbServiceClient) @@ -228,6 +228,18 @@ func makeBindContext(projectConfig projectconfig.Config, logger *log.Logger, can return bindContext } +func makeConfigurationRegistries() (configRegistry *providers.Registry[configuration.Configuration], secretsRegistry *providers.Registry[configuration.Secrets]) { + configRegistry = providers.NewRegistry[configuration.Configuration]() + configRegistry.Register(providers.NewEnvarFactory[configuration.Configuration]()) + configRegistry.Register(providers.NewInlineFactory[configuration.Configuration]()) + secretsRegistry = providers.NewRegistry[configuration.Secrets]() + secretsRegistry.Register(providers.NewEnvarFactory[configuration.Secrets]()) + secretsRegistry.Register(providers.NewInlineFactory[configuration.Secrets]()) + // secretsRegistry.Register(providers.NewOnePasswordFactory()) + secretsRegistry.Register(providers.NewKeychainFactory()) + return +} + type currentStatusManager struct { statusManager optional.Option[terminal.StatusManager] } diff --git a/internal/configuration/api.go b/internal/configuration/api.go index f52e78ca94..71a5d3ca15 100644 --- a/internal/configuration/api.go +++ b/internal/configuration/api.go @@ -105,6 +105,8 @@ type Router[R Role] interface { type ProviderKey string +func (p ProviderKey) String() string { return string(p) } + // Provider is a generic interface for storing and retrieving configuration and secrets. type Provider[R Role] interface { Role() R diff --git a/internal/configuration/providers/registry.go b/internal/configuration/providers/registry.go index 9a59e366d5..f4a2f12226 100644 --- a/internal/configuration/providers/registry.go +++ b/internal/configuration/providers/registry.go @@ -3,6 +3,8 @@ package providers import ( "context" "fmt" + "maps" + "slices" "github.com/TBD54566975/ftl/internal/configuration" ) @@ -20,6 +22,11 @@ func NewRegistry[R configuration.Role]() *Registry[R] { } } +// Providers returns the list of registered provider keys. +func (r *Registry[R]) Providers() []configuration.ProviderKey { + return slices.Collect(maps.Keys(r.factories)) +} + func (r *Registry[R]) Register(name configuration.ProviderKey, factory Factory[R]) { r.factories[name] = factory }