From ee157580cfe9e6ba883abae2c6f09b43c7b339f9 Mon Sep 17 00:00:00 2001 From: "R.I.Pienaar" Date: Thu, 31 Aug 2023 14:02:27 +0200 Subject: [PATCH 1/2] (#145) Allow binaries to be build that embed a app Signed-off-by: R.I.Pienaar --- builder/builder.go | 26 +++++++++++++++----------- builder/cli.go | 18 +++++++++++++++++- builder/options.go | 9 +++++++++ commands/commands.go | 17 +++++++++++++++++ main.go | 10 +++------- 5 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 commands/commands.go diff --git a/builder/builder.go b/builder/builder.go index 1682562..0d5331c 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -324,16 +324,7 @@ func (b *AppBuilder) HasDefinition() bool { return fileExist(source) } -func (b *AppBuilder) loadDefinition(source string) (*Definition, error) { - if b.log != nil { - b.log.Debugf("Loading application definition %v", source) - } - - cfg, err := os.ReadFile(source) - if err != nil { - return nil, err - } - +func (b *AppBuilder) loadDefinitionBytes(cfg []byte, path string) (*Definition, error) { d := &Definition{} cfgj, err := yaml.YAMLToJSON(cfg) if err != nil { @@ -350,11 +341,24 @@ func (b *AppBuilder) loadDefinition(source string) (*Definition, error) { return nil, err } - b.definitionPath = source + b.definitionPath = path return d, nil } +func (b *AppBuilder) loadDefinition(source string) (*Definition, error) { + if b.log != nil { + b.log.Debugf("Loading application definition %v", source) + } + + cfg, err := os.ReadFile(source) + if err != nil { + return nil, err + } + + return b.loadDefinitionBytes(cfg, source) +} + // LoadDefinition loads the definition for the name from file, creates the command structure and validates everything func (b *AppBuilder) LoadDefinition() (*Definition, error) { name := appDefPattern diff --git a/builder/cli.go b/builder/cli.go index 489d95e..f79f5ef 100644 --- a/builder/cli.go +++ b/builder/cli.go @@ -8,13 +8,14 @@ import ( "context" "errors" "fmt" - "github.com/choria-io/fisk" "os" "os/signal" "strings" "syscall" "time" + "github.com/choria-io/fisk" + "github.com/sirupsen/logrus" ) @@ -71,6 +72,21 @@ func RunBuilderCLI(ctx context.Context, watchInterrupts bool, opts ...Option) er return bldr.RunBuilderCLI() } +// MountAsCommand takes the given definition and mounts it on app using name +func MountAsCommand(ctx context.Context, app KingpinCommand, definition []byte, log Logger) error { + bldr, err := createBuilder(ctx, "builder", log, WithAppDefinitionBytes(definition)) + if err != nil { + return err + } + + bldr.cfg, err = bldr.LoadConfig() + if err != nil && !errors.Is(err, ErrConfigNotFound) { + return err + } + + return bldr.registerCommands(app, bldr.def.commands...) +} + // RunStandardCLI runs a standard command line instance with shutdown watchers etc. If log is nil a logger will be created func RunStandardCLI(ctx context.Context, name string, watchInterrupts bool, log Logger, opts ...Option) error { ctx, cancel := context.WithCancel(ctx) diff --git a/builder/options.go b/builder/options.go index 5f49b41..5b53576 100644 --- a/builder/options.go +++ b/builder/options.go @@ -27,6 +27,15 @@ func WithLogger(logger Logger) Option { } } +// WithAppDefinitionBytes uses a provided app definition rather than load one from disk +func WithAppDefinitionBytes(def []byte) Option { + return func(b *AppBuilder) (err error) { + b.def, err = b.loadDefinitionBytes(def, "embedded") + + return err + } +} + // WithAppDefinitionFile sets a file where the definition should be loaded from func WithAppDefinitionFile(f string) Option { return func(b *AppBuilder) error { diff --git a/commands/commands.go b/commands/commands.go new file mode 100644 index 0000000..bf2d10a --- /dev/null +++ b/commands/commands.go @@ -0,0 +1,17 @@ +// Copyright (c) 2023, R.I. Pienaar and the Choria Project contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package commands + +import ( + "github.com/choria-io/appbuilder/commands/exec" + "github.com/choria-io/appbuilder/commands/parent" + "github.com/choria-io/appbuilder/commands/scaffold" +) + +func MustRegisterStandardCommands() { + parent.MustRegister() + exec.MustRegister() + scaffold.MustRegister() +} diff --git a/main.go b/main.go index f59d5d3..bec30de 100644 --- a/main.go +++ b/main.go @@ -13,20 +13,16 @@ import ( "strings" "github.com/choria-io/appbuilder/builder" - "github.com/choria-io/appbuilder/commands/exec" - "github.com/choria-io/appbuilder/commands/parent" - "github.com/choria-io/appbuilder/commands/scaffold" + "github.com/choria-io/appbuilder/commands" ) func main() { - parent.MustRegister() - exec.MustRegister() - scaffold.MustRegister() - name := filepath.Base(os.Args[0]) var err error + commands.MustRegisterStandardCommands() + if strings.HasPrefix(name, "appbuilder") { err = builder.RunBuilderCLI(context.Background(), true, builder.WithContextualUsageOnError()) } else if strings.HasPrefix(name, "abt") { From 86613e49d022b8eb75e3652d7b8747a6c43eac8e Mon Sep 17 00:00:00 2001 From: "R.I.Pienaar" Date: Thu, 31 Aug 2023 14:08:40 +0200 Subject: [PATCH 2/2] (#145) Add documentation Signed-off-by: R.I.Pienaar --- docs/content/experiments/_index.md | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/docs/content/experiments/_index.md b/docs/content/experiments/_index.md index a893a70..c01f7cf 100644 --- a/docs/content/experiments/_index.md +++ b/docs/content/experiments/_index.md @@ -71,3 +71,65 @@ An example can be found in the source repository for this project. Configuration is looked for in the local directory in the `.abtenv` file. At present this is not searched for in parent directories. + +## Compiled Applications + +It's nice that you do not need to compile App Builder apps into binaries as it allows for fast iteration, but sometimes +it might be desired. + +As of version `0.7.2` we support compiling binaries that contain an application. + +Given an application in `app.yaml` we can create a small go stub: + +```go +package main + +import ( + "context" + _ "embed" + "os" + + "github.com/choria-io/appbuilder/builder" + "github.com/choria-io/fisk" +) + +//go:embed app.yaml +var def []byte + +func main() { + builder.MustRegisterStandardCommands() + + cmd := fisk.Newf("myapp", "My compiled App Builder application") + + err := builder.MountAsCommand(context.TODO(), cmd, def, nil) + if err != nil { + panic(err) + } + + cmd.MustParseWithUsage(os.Args[1:]) +} +``` + +When you compile this as a normal Go application your binary will be an executable version of the app. + +Here we mount the application at the top level of the `myapp` binary, but you could also mount it later on - perhaps you +have other compiled in behaviours you wish to surface: + +```go +func main() { + builder.MustRegisterStandardCommands() + + cmd := fisk.Newf("myapp", "My compiled App Builder application") + embedded := cmd.Command("embedded","Embedded application goes here") + + err := builder.MountAsCommand(context.TODO(), embedded, def, nil) + if err != nil { + panic(err) + } + + cmd.MustParseWithUsage(os.Args[1:]) +} +``` + +Here we would end up with `myapp embedded [app commands]` - the command being mounted at a deeper level in the resulting +compiled application. This way you can plug a App Builder command into any level programmatically. \ No newline at end of file