Skip to content

Commit

Permalink
feat: add "ftl profile ..." command tree (#2862)
Browse files Browse the repository at this point in the history
Also refactor profiles package a bit to be more flexible.


```
🐚 ~/dev/ftl $ ftl profile init ftl
Project initialized in /Users/alec/dev/ftl.
🐚 ~/dev/ftl $ ftl profile new --local test
🐚 ~/dev/ftl $ ftl profile list
local (local, default)
test (local)
🐚 ~/dev/ftl $ ftl profile switch test
🐚 ~/dev/ftl $ ftl profile list
local (local, default)
test (local, active)
```

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
alecthomas and github-actions[bot] authored Sep 27, 2024
1 parent 6bd2cc9 commit 8888126
Show file tree
Hide file tree
Showing 7 changed files with 458 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/.ftl-project/
.hermit/
.vscode/*
!/.vscode/settings.json
Expand Down Expand Up @@ -42,3 +41,4 @@ junit*.xml
/docs/public
.ftl.lock
docker-build/
**/.ftl
57 changes: 32 additions & 25 deletions frontend/cli/cmd_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,31 @@ import (
"strings"

"github.com/TBD54566975/ftl"
"github.com/TBD54566975/ftl/internal"
"github.com/TBD54566975/ftl/internal/configuration"
"github.com/TBD54566975/ftl/internal/configuration/providers"
"github.com/TBD54566975/ftl/internal/exec"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/profiles"
"github.com/TBD54566975/ftl/internal/projectconfig"
"github.com/TBD54566975/ftl/internal/projectinit"
)

type initCmd struct {
Name string `arg:"" help:"Name of the project."`
Hermit bool `help:"Include Hermit language-specific toolchain binaries." negatable:""`
Dir string `arg:"" help:"Directory to initialize the project in."`
ModuleDirs []string `help:"Child directories of existing modules."`
NoGit bool `help:"Don't add files to the git repository."`
Startup string `help:"Command to run on startup."`
Name string `arg:"" help:"Name of the project."`
Hermit bool `help:"Include Hermit language-specific toolchain binaries." negatable:""`
Dir string `arg:"" help:"Directory to initialize the project in." default:"${gitroot}" required:""`
ModuleDirs []string `help:"Child directories of existing modules."`
ModuleRoots []string `help:"Root directories of existing modules."`
NoGit bool `help:"Don't add files to the git repository."`
Startup string `help:"Command to run on startup."`
}

func (i initCmd) Run(ctx context.Context) error {
if i.Dir == "" {
return fmt.Errorf("directory is required")
}

logger := log.FromContext(ctx)
func (i initCmd) Run(
ctx context.Context,
logger *log.Logger,
configRegistry *providers.Registry[configuration.Configuration],
secretsRegistry *providers.Registry[configuration.Secrets],
) error {
logger.Debugf("Initializing FTL project in %s", i.Dir)
if err := scaffold(ctx, i.Hermit, projectinit.Files(), i.Dir, i); err != nil {
return err
Expand All @@ -50,20 +53,24 @@ func (i initCmd) Run(ctx context.Context) error {
return err
}

gitRoot, ok := internal.GitRoot(i.Dir).Get()
if !i.NoGit && ok {
_, err := profiles.Init(profiles.ProjectConfig{
Realm: i.Name,
FTLMinVersion: ftl.Version,
ModuleRoots: i.ModuleRoots,
NoGit: i.NoGit,
Root: i.Dir,
}, secretsRegistry, configRegistry)
if err != nil {
return fmt.Errorf("initialize project: %w", err)
}

if !i.NoGit {
logger.Debugf("Updating .gitignore")
if err := updateGitIgnore(ctx, gitRoot); err != nil {
return err
}
logger.Debugf("Adding files to git")
if i.Hermit {
if err := maybeGitAdd(ctx, i.Dir, "bin/*"); err != nil {
return err
}
if err := updateGitIgnore(ctx, i.Dir); err != nil {
return fmt.Errorf("update .gitignore: %w", err)
}
if err := maybeGitAdd(ctx, i.Dir, "ftl-project.toml"); err != nil {
return err
if err := maybeGitAdd(ctx, i.Dir, ".ftl-project"); err != nil {
return fmt.Errorf("git add .ftl-project: %w", err)
}
}
return nil
Expand Down
133 changes: 133 additions & 0 deletions frontend/cli/cmd_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package main

import (
"fmt"
"net/url"
"strings"

"github.com/alecthomas/types/either"

"github.com/TBD54566975/ftl"
"github.com/TBD54566975/ftl/internal/configuration"
"github.com/TBD54566975/ftl/internal/configuration/providers"
"github.com/TBD54566975/ftl/internal/profiles"
)

type profileCmd struct {
Init profileInitCmd `cmd:"" help:"Initialize a new project."`
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."`
}

type profileInitCmd struct {
Project string `arg:"" help:"Name of the project."`
Dir string `arg:"" help:"Directory to initialize the project in." default:"${gitroot}" required:""`
ModuleRoots []string `help:"Root directories of existing modules."`
NoGit bool `help:"Don't add files to the git repository."`
}

func (p profileInitCmd) Run(
configRegistry *providers.Registry[configuration.Configuration],
secretsRegistry *providers.Registry[configuration.Secrets],
) error {
_, err := profiles.Init(profiles.ProjectConfig{
Realm: p.Project,
FTLMinVersion: ftl.Version,
ModuleRoots: p.ModuleRoots,
NoGit: p.NoGit,
Root: p.Dir,
}, secretsRegistry, configRegistry)
if err != nil {
return fmt.Errorf("init project: %w", err)
}
fmt.Printf("Project initialized in %s.\n", p.Dir)
return nil
}

type profileListCmd struct{}

func (profileListCmd) Run(project *profiles.Project) error {
active, err := project.ActiveProfile()
if err != nil {
return fmt.Errorf("active profile: %w", err)
}
p, err := project.List()
if err != nil {
return fmt.Errorf("list profiles: %w", err)
}
for _, profile := range p {
attrs := []string{}
switch profile.Config.(type) {
case either.Left[profiles.LocalProfileConfig, profiles.RemoteProfileConfig]:
attrs = append(attrs, "local")
case either.Right[profiles.LocalProfileConfig, profiles.RemoteProfileConfig]:
attrs = append(attrs, "remote")
}
if project.DefaultProfile() == profile.Name {
attrs = append(attrs, "default")
}
if active == profile.Name {
attrs = append(attrs, "active")
}
fmt.Printf("%s (%s)\n", profile, strings.Join(attrs, ", "))
}
return nil
}

type profileDefaultCmd struct {
Profile string `arg:"" help:"Profile name."`
}

func (p profileDefaultCmd) Run(project *profiles.Project) error {
err := project.SetDefault(p.Profile)
if err != nil {
return fmt.Errorf("set default profile: %w", err)
}
return nil
}

type profileSwitchCmd struct {
Profile string `arg:"" help:"Profile name."`
}

func (p profileSwitchCmd) Run(project *profiles.Project) error {
err := project.Switch(p.Profile)
if err != nil {
return fmt.Errorf("switch profile: %w", err)
}
return nil
}

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"`
Name string `arg:"" help:"Profile name."`
}

func (p profileNewCmd) Run(project *profiles.Project) error {
var config either.Either[profiles.LocalProfileConfig, profiles.RemoteProfileConfig]
switch {
case p.Local:
config = either.LeftOf[profiles.RemoteProfileConfig](profiles.LocalProfileConfig{
SecretsProvider: p.Secrets,
ConfigProvider: p.Configuration,
})

case p.Remote != nil:
config = either.RightOf[profiles.LocalProfileConfig](profiles.RemoteProfileConfig{
Endpoint: p.Remote,
})
}
err := project.New(profiles.ProfileConfig{
Name: p.Name,
Config: config,
})
if err != nil {
return fmt.Errorf("new profile: %w", err)
}
return nil
}
25 changes: 23 additions & 2 deletions frontend/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/url"
"os"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"syscall"
Expand All @@ -16,8 +17,12 @@ import (

"github.com/TBD54566975/ftl"
"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect"
"github.com/TBD54566975/ftl/internal"
_ "github.com/TBD54566975/ftl/internal/automaxprocs" // Set GOMAXPROCS to match Linux container CPU quota.
"github.com/TBD54566975/ftl/internal/configuration"
"github.com/TBD54566975/ftl/internal/configuration/providers"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/profiles"
"github.com/TBD54566975/ftl/internal/projectconfig"
"github.com/TBD54566975/ftl/internal/rpc"
"github.com/TBD54566975/ftl/internal/terminal"
Expand All @@ -30,6 +35,7 @@ type InteractiveCLI struct {
Ping pingCmd `cmd:"" help:"Ping the FTL cluster."`
Status statusCmd `cmd:"" help:"Show FTL status."`
Init initCmd `cmd:"" help:"Initialize a new FTL project."`
Profile profileCmd `cmd:"" help:"Manage profiles."`
New newCmd `cmd:"" help:"Create a new FTL module."`
PS psCmd `cmd:"" help:"List deployments."`
Call callCmd `cmd:"" help:"Call an FTL function."`
Expand Down Expand Up @@ -121,14 +127,15 @@ func main() {
if err != nil && !errors.Is(err, os.ErrNotExist) {
kctx.FatalIfErrorf(err)
}
bindContext := makeBindContext(config, cancel)
bindContext := makeBindContext(config, logger, cancel)
ctx = bindContext(ctx, kctx)

err = kctx.Run(ctx)
kctx.FatalIfErrorf(err)
}

func createKongApplication(cli any) *kong.Kong {
gitRoot, _ := internal.GitRoot(".").Get()
app := kong.Must(cli,
kong.Description(`FTL - Towards a 𝝺-calculus for large-scale systems`),
kong.Configuration(kongtoml.Loader, ".ftl.toml", "~/.ftl.toml"),
Expand All @@ -146,25 +153,39 @@ func createKongApplication(cli any) *kong.Kong {
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"numcpu": strconv.Itoa(runtime.NumCPU()),
"gitroot": gitRoot,
},
)
return app
}

func makeBindContext(projectConfig projectconfig.Config, cancel context.CancelFunc) terminal.KongContextBinder {
func makeBindContext(projectConfig projectconfig.Config, logger *log.Logger, cancel context.CancelFunc) terminal.KongContextBinder {
var bindContext terminal.KongContextBinder
bindContext = func(ctx context.Context, kctx *kong.Context) context.Context {
kctx.Bind(projectConfig)
kctx.Bind(logger)

controllerServiceClient := rpc.Dial(ftlv1connect.NewControllerServiceClient, cli.Endpoint.String(), log.Error)
ctx = rpc.ContextWithClient(ctx, controllerServiceClient)
kctx.BindTo(controllerServiceClient, (*ftlv1connect.ControllerServiceClient)(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]())
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)
kctx.BindTo(verbServiceClient, (*ftlv1connect.VerbServiceClient)(nil))
project, err := profiles.Open(filepath.Dir(projectConfig.Path), secretsRegistry, configRegistry)
kctx.FatalIfErrorf(err)
kctx.Bind(project)

kctx.Bind(cli.Endpoint)
kctx.BindTo(ctx, (*context.Context)(nil))
Expand Down
Loading

0 comments on commit 8888126

Please sign in to comment.