Skip to content

Commit

Permalink
feat(go-runtime): use new configuration/secret system
Browse files Browse the repository at this point in the history
This allows secrets/config to be dynamically updated in most cases
(the envar provider being a notable exception).

```
🐚 ~/dev/ftl $ ftl config set echo.default --inline "anonymous"
🐚 ~/dev/ftl $ ftl config get echo.default
"anonymous"
🐚 ~/dev/ftl $ ftl call echo.echo       aat/configuration-go-runtime
{"message":"Hello, anonymous!!! It is 2024-03-03 07:45:21.237088 +1000 AEST!"}
🐚 ~/dev/ftl $ ftl config set echo.default --inline "Anne"
🐚 ~/dev/ftl $ ftl config get echo.default
"Anne"
🐚 ~/dev/ftl $ ftl call echo.echo
{"message":"Hello, Anne!!! It is 2024-03-03 07:44:52.2176 +1000 AEST!"}
```

Also made a few tweaks to allow FTL to largely work offline:

- Propagate `replace` directives from Go modules into the generated main
  and external-module `go.mod` files.
- Add `--[no-]console` flag that allows building of the console to be
  skipped.
- Put Bit maven builds into offline mode (does not work fully, I think
  there's another flag to force Maven to use the local repository that
  needs to be used, but I'm offline and there's nothing in `--help`).
  • Loading branch information
alecthomas committed Mar 4, 2024
1 parent ab09744 commit f715921
Show file tree
Hide file tree
Showing 26 changed files with 322 additions and 170 deletions.
1 change: 1 addition & 0 deletions Bitfile
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ kotlin-runtime/external-module-template.zip: kotlin-runtime/external-module-temp
-clean

%{KT_RUNTIME_OUT}: %{KT_RUNTIME_IN} %{PROTO_IN}
# TODO: Figure out how to make Maven build completely offline. Bizarrely "-o" does not do this.
build:
mvn -B -N install
mvn -B -pl :ftl-runtime install
Expand Down
23 changes: 16 additions & 7 deletions backend/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import (

type Config struct {
Bind *url.URL `help:"Socket to bind to." default:"http://localhost:8892" env:"FTL_CONTROLLER_BIND"`
NoConsole bool `help:"Disable the console."`
Advertise *url.URL `help:"Endpoint the Controller should advertise (must be unique across the cluster, defaults to --bind if omitted)." env:"FTL_CONTROLLER_ADVERTISE"`
ConsoleURL *url.URL `help:"The public URL of the console (for CORS)." env:"FTL_CONTROLLER_CONSOLE_URL"`
AllowOrigins []*url.URL `help:"Allow CORS requests to ingress endpoints from these origins." env:"FTL_CONTROLLER_ALLOW_ORIGIN"`
Expand Down Expand Up @@ -78,9 +79,18 @@ func Start(ctx context.Context, config Config, runnerScaling scaling.RunnerScali
logger := log.FromContext(ctx)
logger.Debugf("Starting FTL controller")

c, err := frontend.Server(ctx, config.ContentTime, config.ConsoleURL)
if err != nil {
return err
var consoleHandler http.Handler
var err error
if config.NoConsole {
consoleHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
_, _ = w.Write([]byte("Console not installed."))
})
} else {
consoleHandler, err = frontend.Server(ctx, config.ContentTime, config.ConsoleURL)
if err != nil {
return err
}
}

// Bring up the DB connection and DAL.
Expand Down Expand Up @@ -111,7 +121,7 @@ func Start(ctx context.Context, config Config, runnerScaling scaling.RunnerScali
rpc.GRPC(ftlv1connect.NewControllerServiceHandler, svc),
rpc.GRPC(pbconsoleconnect.NewConsoleServiceHandler, console),
rpc.HTTP("/ingress/", ingressHandler),
rpc.HTTP("/", c),
rpc.HTTP("/", consoleHandler),
)
}

Expand Down Expand Up @@ -845,7 +855,6 @@ func (s *Service) reconcileDeployments(ctx context.Context) (time.Duration, erro
}
wg, ctx := concurrency.New(ctx, concurrency.WithConcurrencyLimit(4))
for _, reconcile := range reconciliation {
reconcile := reconcile
deploymentLogger := s.getDeploymentLogger(ctx, reconcile.Deployment)
deploymentLogger.Debugf("Reconciling %s", reconcile.Deployment)
deployment := model.Deployment{
Expand All @@ -860,7 +869,7 @@ func (s *Service) reconcileDeployments(ctx context.Context) (time.Duration, erro
if err := s.deploy(ctx, deployment); err != nil {
deploymentLogger.Debugf("Failed to increase deployment replicas: %s", err)
} else {
deploymentLogger.Debugf("Reconciled %s", reconcile.Deployment)
deploymentLogger.Infof("Reconciled %s to %d/%d replicas", reconcile.Deployment, reconcile.AssignedReplicas+1, reconcile.RequiredReplicas)
}
return nil
})
Expand All @@ -871,7 +880,7 @@ func (s *Service) reconcileDeployments(ctx context.Context) (time.Duration, erro
if err != nil {
deploymentLogger.Warnf("Failed to terminate runner: %s", err)
} else if ok {
deploymentLogger.Debugf("Reconciled %s", reconcile.Deployment)
deploymentLogger.Infof("Reconciled %s to %d/%d replicas", reconcile.Deployment, reconcile.AssignedReplicas-1, reconcile.RequiredReplicas)
} else {
deploymentLogger.Warnf("Failed to terminate runner: no runners found")
}
Expand Down
4 changes: 2 additions & 2 deletions backend/controller/scaling/localscaling/devel.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ var templateDirOnce sync.Once

func templateDir(ctx context.Context) string {
templateDirOnce.Do(func() {
cmd := exec.Command(ctx, log.Debug, internal.GitRoot(""), "bit", "build/template/ftl/jars/ftl-runtime.jar")
err := cmd.Run()
// TODO: Figure out how to make maven build offline
err := exec.Command(ctx, log.Debug, internal.GitRoot(""), "bit", "build/template/ftl/jars/ftl-runtime.jar").RunBuffered(ctx)
if err != nil {
panic(err)
}
Expand Down
2 changes: 2 additions & 0 deletions backend/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect"
"github.com/TBD54566975/ftl/backend/schema"
"github.com/TBD54566975/ftl/common/plugin"
"github.com/TBD54566975/ftl/internal"
"github.com/TBD54566975/ftl/internal/download"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/model"
Expand Down Expand Up @@ -218,6 +219,7 @@ func (s *Service) Deploy(ctx context.Context, req *connect.Request[ftlv1.DeployR
ftlv1connect.NewVerbServiceClient,
plugin.WithEnvars(
"FTL_ENDPOINT="+s.config.ControllerEndpoint.String(),
"FTL_CONFIG="+filepath.Join(internal.GitRoot(""), "ftl-project.toml"),
"FTL_OBSERVABILITY_ENDPOINT="+s.config.ControllerEndpoint.String(),
),
)
Expand Down
31 changes: 5 additions & 26 deletions cmd/ftl/cmd_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,18 @@ import (
"io"
"os"

"github.com/alecthomas/kong"

"github.com/TBD54566975/ftl/common/configuration"
)

type mutableConfigProviderMixin struct {
configuration.InlineProvider
configuration.EnvarProvider[configuration.EnvarTypeConfig]
}

func (s *mutableConfigProviderMixin) newConfigManager(ctx context.Context, resolver configuration.Resolver) (*configuration.Manager, error) {
return configuration.New(ctx, resolver, []configuration.Provider{s.InlineProvider, s.EnvarProvider})
}

type configCmd struct {
configuration.ProjectConfigResolver[configuration.FromConfig]
configuration.DefaultConfigMixin

List configListCmd `cmd:"" help:"List configuration."`
Get configGetCmd `cmd:"" help:"Get a configuration value."`
Set configSetCmd `cmd:"" help:"Set a configuration value."`
Unset configUnsetCmd `cmd:"" help:"Unset a configuration value."`
}

func (s *configCmd) newConfigManager(ctx context.Context) (*configuration.Manager, error) {
mp := mutableConfigProviderMixin{}
_ = kong.ApplyDefaults(&mp)
return mp.newConfigManager(ctx, s.ProjectConfigResolver)
}

func (s *configCmd) Help() string {
return `
Configuration values are used to store non-sensitive information such as URLs,
Expand All @@ -49,7 +32,7 @@ type configListCmd struct {
}

func (s *configListCmd) Run(ctx context.Context, scmd *configCmd) error {
sm, err := scmd.newConfigManager(ctx)
sm, err := scmd.NewConfigurationManager(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -95,7 +78,7 @@ Returns a JSON-encoded configuration value.
}

func (s *configGetCmd) Run(ctx context.Context, scmd *configCmd) error {
sm, err := scmd.newConfigManager(ctx)
sm, err := scmd.NewConfigurationManager(ctx)
if err != nil {
return err
}
Expand All @@ -115,15 +98,13 @@ func (s *configGetCmd) Run(ctx context.Context, scmd *configCmd) error {
}

type configSetCmd struct {
mutableConfigProviderMixin

JSON bool `help:"Assume input value is JSON."`
Ref configuration.Ref `arg:"" help:"Configuration reference in the form [<module>.]<name>."`
Value *string `arg:"" placeholder:"VALUE" help:"Configuration value (read from stdin if omitted)." optional:""`
}

func (s *configSetCmd) Run(ctx context.Context, scmd *configCmd) error {
sm, err := s.newConfigManager(ctx, scmd.ProjectConfigResolver)
sm, err := scmd.NewConfigurationManager(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -154,13 +135,11 @@ func (s *configSetCmd) Run(ctx context.Context, scmd *configCmd) error {
}

type configUnsetCmd struct {
mutableConfigProviderMixin

Ref configuration.Ref `arg:"" help:"Configuration reference in the form [<module>.]<name>."`
}

func (s *configUnsetCmd) Run(ctx context.Context, scmd *configCmd) error {
sm, err := s.newConfigManager(ctx, scmd.ProjectConfigResolver)
sm, err := scmd.NewConfigurationManager(ctx)
if err != nil {
return err
}
Expand Down
34 changes: 5 additions & 29 deletions cmd/ftl/cmd_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,21 @@ import (
"io"
"os"

"github.com/alecthomas/kong"
"github.com/mattn/go-isatty"
"golang.org/x/term"

"github.com/TBD54566975/ftl/common/configuration"
)

type mutableSecretProviderMixin struct {
configuration.InlineProvider
configuration.KeychainProvider
configuration.EnvarProvider[configuration.EnvarTypeSecrets]
configuration.OnePasswordProvider
}

func (s *mutableSecretProviderMixin) newSecretsManager(ctx context.Context, resolver configuration.Resolver) (*configuration.Manager, error) {
return configuration.New(ctx, resolver, []configuration.Provider{
s.InlineProvider, s.KeychainProvider, s.EnvarProvider, s.OnePasswordProvider,
})
}

type secretCmd struct {
configuration.ProjectConfigResolver[configuration.FromSecrets]
configuration.DefaultSecretsMixin

List secretListCmd `cmd:"" help:"List secrets."`
Get secretGetCmd `cmd:"" help:"Get a secret."`
Set secretSetCmd `cmd:"" help:"Set a secret."`
Unset secretUnsetCmd `cmd:"" help:"Unset a secret."`
}

func (s *secretCmd) newSecretsManager(ctx context.Context) (*configuration.Manager, error) {
mp := mutableSecretProviderMixin{}
_ = kong.ApplyDefaults(&mp)
return mp.newSecretsManager(ctx, s.ProjectConfigResolver)
}

func (s *secretCmd) Help() string {
return `
Secrets are used to store sensitive information such as passwords, tokens, and
Expand All @@ -58,7 +38,7 @@ type secretListCmd struct {
}

func (s *secretListCmd) Run(ctx context.Context, scmd *secretCmd) error {
sm, err := scmd.newSecretsManager(ctx)
sm, err := scmd.NewSecretsManager(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -104,7 +84,7 @@ Returns a JSON-encoded secret value.
}

func (s *secretGetCmd) Run(ctx context.Context, scmd *secretCmd) error {
sm, err := scmd.newSecretsManager(ctx)
sm, err := scmd.NewSecretsManager(ctx)
if err != nil {
return err
}
Expand All @@ -124,14 +104,12 @@ func (s *secretGetCmd) Run(ctx context.Context, scmd *secretCmd) error {
}

type secretSetCmd struct {
mutableSecretProviderMixin

JSON bool `help:"Assume input value is JSON."`
Ref configuration.Ref `arg:"" help:"Secret reference in the form [<module>.]<name>."`
}

func (s *secretSetCmd) Run(ctx context.Context, scmd *secretCmd) error {
sm, err := s.newSecretsManager(ctx, scmd.ProjectConfigResolver)
sm, err := scmd.NewSecretsManager(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -168,13 +146,11 @@ func (s *secretSetCmd) Run(ctx context.Context, scmd *secretCmd) error {
}

type secretUnsetCmd struct {
mutableSecretProviderMixin

Ref configuration.Ref `arg:"" help:"Secret reference in the form [<module>.]<name>."`
}

func (s *secretUnsetCmd) Run(ctx context.Context, scmd *secretCmd) error {
sm, err := s.newSecretsManager(ctx, scmd.ProjectConfigResolver)
sm, err := scmd.NewSecretsManager(ctx)
if err != nil {
return err
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/ftl/cmd_serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
type serveCmd struct {
Bind *url.URL `help:"Starting endpoint to bind to and advertise to. Each controller and runner will increment the port by 1" default:"http://localhost:8892"`
AllowOrigins []*url.URL `help:"Allow CORS requests to ingress endpoints from these origins." env:"FTL_CONTROLLER_ALLOW_ORIGIN"`
NoConsole bool `help:"Disable the console."`
DBPort int `help:"Port to use for the database." default:"54320"`
Recreate bool `help:"Recreate the database even if it already exists." default:"false"`
Controllers int `short:"c" help:"Number of controllers to start." default:"1"`
Expand Down Expand Up @@ -98,6 +99,7 @@ func (s *serveCmd) Run(ctx context.Context) error {
Bind: controllerAddresses[i],
DSN: dsn,
AllowOrigins: s.AllowOrigins,
NoConsole: s.NoConsole,
}
if err := kong.ApplyDefaults(&config); err != nil {
return err
Expand Down
37 changes: 37 additions & 0 deletions common/configuration/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package configuration

import "context"

type contextKeySecrets struct{}

type contextKeyConfig struct{}

// ContextWithSecrets adds a secrets manager to the given context.
func ContextWithSecrets(ctx context.Context, secretsManager *Manager) context.Context {
return context.WithValue(ctx, contextKeySecrets{}, secretsManager)
}

// SecretsFromContext retrieves the secrets configuration.Manager previously
// added to the context with [ContextWithConfig].
func SecretsFromContext(ctx context.Context) *Manager {
s, ok := ctx.Value(contextKeySecrets{}).(*Manager)
if !ok {
panic("no secrets manager in context")
}
return s
}

// ContextWithConfig adds a configuration manager to the given context.
func ContextWithConfig(ctx context.Context, configManager *Manager) context.Context {
return context.WithValue(ctx, contextKeyConfig{}, configManager)
}

// ConfigFromContext retrieves the configuration.Manager previously added to the
// context with [ContextWithConfig].
func ConfigFromContext(ctx context.Context) *Manager {
m, ok := ctx.Value(contextKeyConfig{}).(*Manager)
if !ok {
panic("no configuration manager in context")
}
return m
}
63 changes: 63 additions & 0 deletions common/configuration/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package configuration

import (
"context"

"github.com/alecthomas/kong"
)

// NewConfigurationManager constructs a new [Manager] with the default providers for configuration.
func NewConfigurationManager(ctx context.Context, configPath string) (*Manager, error) {
conf := DefaultConfigMixin{
ProjectConfigResolver: ProjectConfigResolver[FromConfig]{
Config: configPath,
},
}
_ = kong.ApplyDefaults(&conf)
return conf.NewConfigurationManager(ctx)
}

// DefaultConfigMixin is a Kong mixin that provides the default configuration manager.
type DefaultConfigMixin struct {
ProjectConfigResolver[FromConfig]
InlineProvider
EnvarProvider[EnvarTypeConfig]
}

// NewConfigurationManager creates a new configuration manager with the default configuration providers.
func (d DefaultConfigMixin) NewConfigurationManager(ctx context.Context) (*Manager, error) {
return New(ctx, &d.ProjectConfigResolver, []Provider{
d.InlineProvider,
d.EnvarProvider,
})
}

// NewSecretsManager constructs a new [Manager] with the default providers for secrets.
func NewSecretsManager(ctx context.Context, configPath string) (*Manager, error) {
conf := DefaultSecretsMixin{
ProjectConfigResolver: ProjectConfigResolver[FromSecrets]{
Config: configPath,
},
}
_ = kong.ApplyDefaults(&conf)
return conf.NewSecretsManager(ctx)
}

// DefaultSecretsMixin is a Kong mixin that provides the default secrets manager.
type DefaultSecretsMixin struct {
ProjectConfigResolver[FromSecrets]
InlineProvider
EnvarProvider[EnvarTypeSecrets]
KeychainProvider
OnePasswordProvider
}

// NewSecretsManager creates a new secrets manager with the default secret providers.
func (d DefaultSecretsMixin) NewSecretsManager(ctx context.Context) (*Manager, error) {
return New(ctx, &d.ProjectConfigResolver, []Provider{
d.InlineProvider,
d.EnvarProvider,
d.KeychainProvider,
d.OnePasswordProvider,
})
}
Loading

0 comments on commit f715921

Please sign in to comment.