Skip to content

Commit

Permalink
chore: improve "ftl profile new" help (#3209)
Browse files Browse the repository at this point in the history
  • Loading branch information
alecthomas authored Oct 28, 2024
1 parent 2bb7dd7 commit 8c6f740
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 20 deletions.
31 changes: 27 additions & 4 deletions frontend/cli/cmd_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
44 changes: 28 additions & 16 deletions frontend/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"

"github.com/alecthomas/kong"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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"),
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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]
}
2 changes: 2 additions & 0 deletions internal/configuration/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions internal/configuration/providers/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package providers
import (
"context"
"fmt"
"maps"
"slices"

"github.com/TBD54566975/ftl/internal/configuration"
)
Expand All @@ -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
}
Expand Down

0 comments on commit 8c6f740

Please sign in to comment.