Skip to content

Commit

Permalink
feat: add external plugin to build engine (#2994)
Browse files Browse the repository at this point in the history
Adds `languageplugin.externalPlugin` for working with plugins over gRPC.
This PR does not include any non-test implementations.

Part of #2452
  • Loading branch information
matt2e authored Oct 11, 2024
1 parent e518502 commit 3ee5b10
Show file tree
Hide file tree
Showing 17 changed files with 1,183 additions and 93 deletions.
2 changes: 1 addition & 1 deletion backend/controller/admin/local_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (s *diskSchemaRetriever) GetActiveSchema(ctx context.Context, bAllocator op
if err != nil {
moduleSchemas <- either.RightOf[*schema.Module](fmt.Errorf("could not load plugin for %s: %w", m.Module, err))
}
defer plugin.Kill(ctx) // nolint:errcheck
defer plugin.Kill() // nolint:errcheck

customDefaults, err := plugin.ModuleConfigDefaults(ctx, m.Dir)
if err != nil {
Expand Down
14 changes: 14 additions & 0 deletions backend/protos/xyz/block/ftl/v1/language/mixins.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/TBD54566975/ftl/internal/builderrors"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/slices"
)

Expand Down Expand Up @@ -72,3 +73,16 @@ func PosFromProto(pos *Position) builderrors.Position {
Filename: pos.Filename,
}
}

func LogLevelFromProto(level LogMessage_LogLevel) log.Level {
switch level {
case LogMessage_INFO:
return log.Info
case LogMessage_WARN:
return log.Warn
case LogMessage_ERROR:
return log.Error
default:
panic(fmt.Sprintf("unhandled Log_Level %v", level))
}
}
2 changes: 2 additions & 0 deletions frontend/cli/cmd_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type newCmd struct {
// - help text (ftl new go --help)
// - default values
// - environment variable overrides
//
// Language plugins take time to launch, so we return the one we created so it can be reused in Run().
func prepareNewCmd(ctx context.Context, k *kong.Kong, args []string) (optionalPlugin optional.Option[languageplugin.LanguagePlugin], err error) {
if len(args) < 2 {
return optionalPlugin, nil
Expand Down
2 changes: 1 addition & 1 deletion frontend/cli/cmd_schema_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func localSchema(ctx context.Context, projectConfig projectconfig.Config, bindAl
if err != nil {
moduleSchemas <- either.RightOf[*schema.Module](err)
}
defer plugin.Kill(ctx) // nolint:errcheck
defer plugin.Kill() // nolint:errcheck

customDefaults, err := plugin.ModuleConfigDefaults(ctx, m.Dir)
if err != nil {
Expand Down
6 changes: 4 additions & 2 deletions frontend/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,16 @@ func main() {
csm := &currentStatusManager{}

app := createKongApplication(&cli, csm)
languagePlugin, err := prepareNewCmd(ctx, app, os.Args[1:])

// Dynamically update the kong app with language specific flags for the "ftl new" command.
languagePlugin, err := prepareNewCmd(log.ContextWithNewDefaultLogger(ctx), app, os.Args[1:])
app.FatalIfErrorf(err)

kctx, err := app.Parse(os.Args[1:])
app.FatalIfErrorf(err)

if plugin, ok := languagePlugin.Get(); ok {
// for "ftl new" command, we only need to create the language plugin once
// Plugins take time to launch, so we bind the "ftl new" plugin to the kong context.
kctx.BindTo(plugin, (*languageplugin.LanguagePlugin)(nil))
}

Expand Down
32 changes: 16 additions & 16 deletions internal/buildengine/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"os"
"time"

"github.com/alecthomas/types/either"
"github.com/alecthomas/types/result"
"google.golang.org/protobuf/proto"

"github.com/TBD54566975/ftl/internal/buildengine/languageplugin"
Expand All @@ -17,33 +17,33 @@ import (
"github.com/TBD54566975/ftl/internal/schema"
)

var errInvalidateDependencies = errors.New("dependencies need to be updated")

// Build a module in the given directory given the schema and module config.
//
// A lock file is used to ensure that only one build is running at a time.
func build(ctx context.Context, plugin languageplugin.LanguagePlugin, projectRootDir string, sch *schema.Schema, config moduleconfig.ModuleConfig, buildEnv []string, devMode bool) (moduleSchema *schema.Module, deploy []string, err error) {
logger := log.FromContext(ctx).Module(config.Module).Scope("build")
//
// Returns invalidateDependenciesError if the build failed due to a change in dependencies.
func build(ctx context.Context, plugin languageplugin.LanguagePlugin, projectRootDir string, bctx languageplugin.BuildContext, buildEnv []string, devMode bool) (moduleSchema *schema.Module, deploy []string, err error) {
logger := log.FromContext(ctx).Module(bctx.Config.Module).Scope("build")
ctx = log.ContextWithLogger(ctx, logger)

logger.Infof("Building module")

result, err := plugin.Build(ctx, projectRootDir, config, sch, buildEnv, devMode)
if err != nil {
return handleBuildResult(ctx, config, either.RightOf[languageplugin.BuildResult](err))
}
return handleBuildResult(ctx, config, either.LeftOf[error](result))
return handleBuildResult(ctx, bctx.Config, result.From(plugin.Build(ctx, projectRootDir, bctx, buildEnv, devMode)))
}

// handleBuildResult processes the result of a build
func handleBuildResult(ctx context.Context, c moduleconfig.ModuleConfig, eitherResult either.Either[languageplugin.BuildResult, error]) (moduleSchema *schema.Module, deploy []string, err error) {
func handleBuildResult(ctx context.Context, c moduleconfig.ModuleConfig, eitherResult result.Result[languageplugin.BuildResult]) (moduleSchema *schema.Module, deploy []string, err error) {
logger := log.FromContext(ctx)
config := c.Abs()

var result languageplugin.BuildResult
switch eitherResult := eitherResult.(type) {
case either.Right[languageplugin.BuildResult, error]:
return nil, nil, fmt.Errorf("failed to build module: %w", eitherResult.Get())
case either.Left[languageplugin.BuildResult, error]:
result = eitherResult.Get()
result, err := eitherResult.Result()
if err != nil {
return nil, nil, fmt.Errorf("failed to build module: %w", err)
}

if result.InvalidateDependencies {
return nil, nil, errInvalidateDependencies
}

var errs []error
Expand Down
10 changes: 7 additions & 3 deletions internal/buildengine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration
}
if meta, ok := e.moduleMetas.Load(event.Config.Module); ok {
meta.plugin.Updates().Unsubscribe(meta.events)
err := meta.plugin.Kill(ctx)
err := meta.plugin.Kill()
if err != nil {
didError = true
e.reportBuildFailed(err)
Expand Down Expand Up @@ -633,7 +633,6 @@ func (e *Engine) BuildAndDeploy(ctx context.Context, replicas int32, waitForDepl
type buildCallback func(ctx context.Context, module Module) error

func (e *Engine) buildWithCallback(ctx context.Context, callback buildCallback, moduleNames ...string) error {

if len(moduleNames) == 0 {
e.moduleMetas.Range(func(name string, meta moduleMeta) bool {
moduleNames = append(moduleNames, name)
Expand Down Expand Up @@ -813,9 +812,14 @@ func (e *Engine) build(ctx context.Context, moduleName string, builtModules map[
e.listener.OnBuildStarted(meta.module)
}

moduleSchema, deploy, err := build(ctx, meta.plugin, e.projectRoot, sch, meta.module.Config, e.buildEnv, e.devMode)
moduleSchema, deploy, err := build(ctx, meta.plugin, e.projectRoot, languageplugin.BuildContext{
Config: meta.module.Config,
Schema: sch,
Dependencies: meta.module.Dependencies,
}, e.buildEnv, e.devMode)
if err != nil {
terminal.UpdateModuleState(ctx, moduleName, terminal.BuildStateFailed)
// TODO: handle errInvalidateDependencies
return err
}
// update files to deploy
Expand Down
Loading

0 comments on commit 3ee5b10

Please sign in to comment.