diff --git a/cmd/cmd.go b/cmd/cmd.go index f96619a51..ffceed2bc 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -17,9 +17,12 @@ import ( "syscall" "time" + "github.com/adrg/xdg" + "github.com/choria-io/appbuilder/builder" "github.com/choria-io/go-choria/protocol" - "github.com/choria-io/go-choria/providers/appbuilder" - + "github.com/choria-io/go-choria/providers/appbuilder/discover" + "github.com/choria-io/go-choria/providers/appbuilder/kv" + "github.com/choria-io/go-choria/providers/appbuilder/rpc" log "github.com/sirupsen/logrus" "gopkg.in/alecthomas/kingpin.v2" @@ -62,7 +65,15 @@ func ParseCLI() (err error) { // // TODO: too janky, need to do a better job here, looking at the name is not enough if !strings.Contains(os.Args[0], "choria") { - builder := appbuilder.NewAppBuilder(ctx, filepath.Base(os.Args[0])) + kv.MustRegister() + rpc.MustRegister() + discover.MustRegister() + + builder, err := builder.New(ctx, filepath.Base(os.Args[0]), + builder.WithConfigPaths(filepath.Join(xdg.ConfigHome, "choria", "appbuilder"), filepath.Join("etc", "choria", "appbuilder"))) + if err != nil { + panic(fmt.Sprintf("app builder setup failed: %v", err)) + } if builder.HasDefinition() { builder.RunCommand() os.Exit(0) diff --git a/go.mod b/go.mod index f1c43af21..ab18f8e81 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( github.com/brutella/dnssd v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cheekybits/genny v1.0.0 // indirect + github.com/choria-io/appbuilder v0.0.4 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/emicklei/dot v0.16.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect diff --git a/go.sum b/go.sum index 515ed13fa..71eb3246a 100644 --- a/go.sum +++ b/go.sum @@ -194,6 +194,8 @@ github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d8 github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/choria-io/appbuilder v0.0.4 h1:u6RkHgSJlnNhnqoWbqXZUY//8L1XCNbREPU5uDn/DA0= +github.com/choria-io/appbuilder v0.0.4/go.mod h1:1S3nj4fALNNpkrmB7vPLCf5L9khohm8H/JD05OXrUZk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= diff --git a/internal/fs/schemas/builder.json b/internal/fs/schemas/builder.json deleted file mode 100644 index c5c1a35fc..000000000 --- a/internal/fs/schemas/builder.json +++ /dev/null @@ -1,536 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "id": "https://choria.io/schemas/choria/builder/v1/application.json", - "title": "io.choria.builder.v1.application", - "description": "Choria Builder Application Specification", - "type": "object", - "required": [ - "name", - "description", - "version", - "author", - "commands" - ], - "definitions": { - "shortname": { - "type": "string", - "minLength": 1, - "pattern": "^[a-z0-9_-]*$" - }, - "semver": { - "type": "string", - "description": "Semantic Versioning 2.0.0 version string", - "minLength": 5, - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" - }, - "rpc_filter": { - "type": "object", - "description": "Standard Choria RPC discovery options", - "additionalItems": false, - "properties": { - "collective": { - "type": "string", - "description": "Target nodes in a specific sub-collective, defaults to configured main collective" - }, - "facts": { - "type": "array", - "description": "List of fact filters, like operatingsystem=CentOS, as typed in -F filters on the CLI", - "items": { - "type": "string" - } - }, - "agents": { - "type": "array", - "description": "List of agents to require, as typed in -A filters on the CLI", - "items": { - "type": "string" - } - }, - "classes": { - "type": "array", - "description": "List of classes to match on, as typed in -C filters on the CLI", - "items": { - "type": "string" - } - }, - "identities": { - "type": "array", - "description": "List of identities to target, as typed in -I filters on the CLI", - "items": { - "type": "string" - } - }, - "combined": { - "type": "array", - "description": "List of combined filters to match on targets, as typed in -W filters on the CLI", - "items": { - "type": "string" - } - }, - "compound": { - "type": "string", - "description": "Compound filter to use, as typed in -S filters on the CLI" - }, - "discovery_method": { - "type": "string", - "description": "The discovery method to use", - "enum": [ - "mc", - "broadcast", - "choria", - "puppetdb", - "external", - "flatfile", - "file", - "inventory" - ] - }, - "discovery_timeout": { - "type": "integer", - "description": "Number of seconds to allow for discovery to complete", - "minimum": 0 - }, - "dynamic_discovery_timeout": { - "type": "boolean", - "description": "Enables windowed dynamic discovery timeout" - }, - "nodes_file": { - "type": "string", - "description": "Path to a file listing node names in text or JSON format" - }, - "discovery_options": { - "type": "object", - "description": "Discovery options as a map of string values", - "additionalItems": { - "type": "string" - } - } - } - }, - "standard_command": { - "type": "object", - "required": [ - "name", - "description", - "type" - ], - "properties": { - "name": { - "type": "string", - "description": "A unique name for this command", - "$ref": "#/definitions/shortname" - }, - "description": { - "$ref": "#/definitions/description" - }, - "confirm_prompt": { - "type": "string", - "description": "When set always confirms the operation using the value as prompt" - } - } - }, - "description": { - "type": "string", - "description": "A human friendly description of what an item does", - "minLength": 1 - }, - "generic_flag": { - "type": "object", - "required": [ - "name", - "description" - ], - "properties": { - "name": { - "description": "A unique name for this flag", - "$ref": "#/definitions/shortname" - }, - "description": { - "$ref": "#/definitions/description" - }, - "required": { - "type": "boolean", - "description": "Indicates this flag must be passed", - "default": false - }, - "placeholder": { - "type": "string", - "description": "String to show as value place holder in help output" - } - } - }, - "generic_transform": { - "description": "Transform the data using the GOJQ implementation of the JQ language", - "type": "object", - "required": ["query"], - "properties": { - "query": { - "type": "string", - "description": "A JQ query to pass the data through" - } - } - }, - "generic_argument": { - "type": "object", - "required": [ - "name", - "description" - ], - "properties": { - "name": { - "description": "A unique name for this argument", - "$ref": "#/definitions/shortname" - }, - "description": { - "$ref": "#/definitions/description" - }, - "required": { - "type": "boolean", - "description": "Indicates that this flag must be passed", - "default": false - } - } - }, - "commands": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/rpc_command" - }, - { - "$ref": "#/definitions/parent_command" - }, - { - "$ref": "#/definitions/kv_command" - }, - { - "$ref": "#/definitions/exec_command" - }, - { - "$ref": "#/definitions/discover_command" - } - ] - } - }, - "parent_command": { - "type": "object", - "description": "A command that does not do anything but serves as a anchor for sub commands", - "additionalItems": false, - "allOf": [ - { - "$ref": "#/definitions/standard_command" - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "const": "parent" - }, - "commands": { - "description": "Additional CLI commands to add", - "$ref": "#/definitions/commands", - "minItems": 1 - } - } - } - ] - }, - "discover_command": { - "type": "object", - "description": "A command that does Choria based node discovery", - "additionalItems": false, - "allOf": [ - { - "$ref": "#/definitions/standard_command" - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "const": "discover" - }, - "filter": { - "description": "Optional filters to apply to the request, will be merged with standard options if set", - "$ref": "#/definitions/rpc_filter" - }, - "std_filters": { - "type": "boolean", - "description": "Enables standard RPC filters like -C, -I etc", - "default": false - }, - "arguments": { - "type": "array", - "description": "List or arguments to accept after the command name", - "items": { - "$ref": "#/definitions/generic_argument" - } - }, - "flags": { - "type": "array", - "description": "List of flags to add to the command", - "items": { - "allOf": [ - { - "$ref": "#/definitions/generic_flag" - } - ] - } - } - } - } - ] - }, - "exec_command": { - "type": "object", - "description": "A command that calls an external script", - "additionalItems": false, - "allOf": [ - { - "$ref": "#/definitions/standard_command" - }, - { - "type": "object", - "required": [ - "type", - "command" - ], - "properties": { - "type": { - "type": "string", - "const": "exec" - }, - "commands": { - "description": "Additional CLI commands to add", - "$ref": "#/definitions/commands" - }, - "command": { - "type": "string", - "description": "The command to execute, supports template interpolation", - "minLength": 1 - } - } - } - ] - }, - "kv_command": { - "type": "object", - "description": "A command that interact with the Choria Key-Value store", - "additionalItems": false, - "allOf": [ - { - "$ref": "#/definitions/standard_command" - }, - { - "type": "object", - "required": [ - "type", - "action", - "bucket", - "key" - ], - "properties": { - "type": { - "type": "string", - "const": "kv" - }, - "commands": { - "description": "Additional CLI commands to add", - "$ref": "#/definitions/commands" - }, - "bucket": { - "type": "string", - "description": "The name of the Key-Value store bucket", - "pattern": "\\A[a-zA-Z0-9_-]+\\z", - "minLength": 1 - }, - "key": { - "type": "string", - "description": "The key to act on", - "minLength": 1 - }, - "value": { - "type": "string", - "description": "The value to store for the put operation" - }, - "action": { - "type": "string", - "description": "The action to perform against the bucket and key", - "enum": [ - "get", - "put", - "del", - "history" - ] - }, - "json": { - "type": "boolean", - "description": "Renders the result in JSON format for get and history actions", - "default": false - } - } - } - ] - }, - "rpc_command": { - "type": "object", - "additionalItems": false, - "allOf": [ - { - "$ref": "#/definitions/standard_command" - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "const": "rpc" - }, - "commands": { - "description": "Additional CLI commands to add", - "$ref": "#/definitions/commands" - }, - "transform": { - "$ref": "#/definitions/generic_transform" - }, - "request": { - "type": "object", - "description": "Details of the RPC request", - "properties": { - "agent": { - "type": "string", - "description": "The agent to call", - "$ref": "#/definitions/shortname" - }, - "action": { - "type": "string", - "description": "The action to call", - "$ref": "#/definitions/shortname" - }, - "inputs": { - "type": "object", - "description": "Free form list of input arguments as a hash, values support template interpolation", - "additionalProperties": { - "type": "string" - } - }, - "filter": { - "description": "Optional filters to apply to the request, will be merged with standard options if set", - "$ref": "#/definitions/rpc_filter" - } - } - }, - "std_filters": { - "type": "boolean", - "description": "Enables standard RPC filters like -C, -I etc", - "default": false - }, - "output_format_flags": { - "type": "boolean", - "description": "Enable flags to adjust the output format like --json, --table etc", - "default": false - }, - "output_format": { - "type": "string", - "description": "Sets a specific output format, not compatible with output_format_flags", - "enum": [ - "senders", - "json", - "table" - ] - }, - "display_flag": { - "type": "boolean", - "description": "Enables the --display flag, not compatible with display", - "default": false - }, - "display": { - "type": "string", - "description": "Force a specific display format to be used, not compatible with display_flags", - "enum": [ - "ok", - "failed", - "all", - "none" - ] - }, - "no_progress": { - "type": "boolean", - "description": "Disables the progress bar" - }, - "batch": { - "type": "integer", - "description": "Batch size to use when executing the RPC request, not compatible with batch_flags" - }, - "batch_sleep": { - "type": "integer", - "description": "When batch is given this is the seconds to sleep between batches, not compatible with batch_flags" - }, - "batch_flags": { - "type": "boolean", - "description": "Enables the --batch and --batch-sleep flags", - "default": false - }, - "all_nodes_confirm_prompt": { - "type": "string", - "description": "Prompts for confirmation when no filter is active resulting on an action against all nodes" - }, - "flags": { - "type": "array", - "description": "List of flags to add to the command", - "items": { - "allOf": [ - { - "$ref": "#/definitions/generic_flag" - }, - { - "type": "object", - "properties": { - "reply_filter": { - "type": "string", - "description": "Choria reply filter" - } - } - } - ] - } - } - } - } - ] - } - }, - "properties": { - "name": { - "description": "A unique name for this application", - "$ref": "#/definitions/shortname" - }, - "description": { - "$ref": "#/definitions/description" - }, - "version": { - "$ref": "#/definitions/semver" - }, - "author": { - "type": "string", - "description": "Contact details for the author", - "minLength": 1 - }, - "commands": { - "description": "A list of commands that make up the application", - "minItems": 1, - "$ref": "#/definitions/commands" - } - } -} diff --git a/providers/appbuilder/builder.go b/providers/appbuilder/builder.go deleted file mode 100644 index cc61cbfe2..000000000 --- a/providers/appbuilder/builder.go +++ /dev/null @@ -1,504 +0,0 @@ -// Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package appbuilder - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "text/template" - - "github.com/AlecAivazis/survey/v2" - "github.com/adrg/xdg" - "github.com/choria-io/go-choria/choria" - "github.com/choria-io/go-choria/client/discovery" - "github.com/choria-io/go-choria/inter" - "github.com/choria-io/go-choria/internal/fs" - "github.com/choria-io/go-choria/internal/util" - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" - "github.com/tidwall/gjson" - "github.com/xeipuuv/gojsonschema" - "gopkg.in/alecthomas/kingpin.v2" - "gopkg.in/alessio/shellescape.v1" -) - -type StandardCommand struct { - Name string `json:"name"` - Description string `json:"description"` - Aliases []string `json:"aliases"` - Type string `json:"type"` - Arguments []GenericArgument `json:"arguments"` - Flags []GenericFlag `json:"flags"` - ConfirmPrompt string `json:"confirm_prompt"` -} - -type StandardSubCommands struct { - Commands []json.RawMessage `json:"commands"` -} - -type Definition struct { - Name string `json:"name"` - Description string `json:"description"` - Version string `json:"version"` - Author string `json:"author"` - - StandardSubCommands - - commands []command -} - -type GenericArgument struct { - Name string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` -} - -type GenericFlag struct { - Name string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` - PlaceHolder string `json:"placeholder"` -} - -type GenericTransform struct { - Query string `json:"query"` -} - -type templateState struct { - Arguments interface{} - Flags interface{} - Config interface{} -} - -type command interface { - CreateCommand(app inter.FlagApp) (*kingpin.CmdClause, error) - SubCommands() []json.RawMessage -} - -type AppBuilder struct { - ctx context.Context - def *Definition - name string - cfg map[string]interface{} - log *logrus.Entry -} - -var ( - errDefinitionNotfound = errors.New("definition not found") - appDefPattern = "%s-app.yaml" - descriptionFmt = `%s - -Contact: %s -` -) - -func NewAppBuilder(ctx context.Context, name string) *AppBuilder { - builder := &AppBuilder{ - ctx: ctx, - name: name, - } - - return builder -} - -func (b *AppBuilder) RunCommand() { - err := b.runCLI() - if err != nil { - fmt.Fprintf(os.Stderr, "Choria application %s: %v\n", b.name, err) - os.Exit(1) - } -} - -func (b *AppBuilder) runCLI() error { - logger := logrus.New() - b.log = logrus.NewEntry(logger) - logger.SetLevel(logrus.WarnLevel) - if os.Getenv("BUILDER_DEBUG") != "" { - logger.SetLevel(logrus.DebugLevel) - } - - var err error - - b.def, err = b.loadDefinition(b.name) - if err != nil { - return err - } - - err = b.loadConfig() - if err != nil { - return err - } - - cmd := kingpin.New(b.name, fmt.Sprintf(descriptionFmt, b.def.Description, b.def.Author)) - cmd.Version(b.def.Version) - cmd.Author(b.def.Author) - cmd.VersionFlag.Hidden() - - err = b.registerCommands(cmd, b.def.commands...) - if err != nil { - return err - } - - _, err = cmd.Parse(os.Args[1:]) - return err -} - -func (b *AppBuilder) registerCommands(cli inter.FlagApp, cmds ...command) error { - for _, c := range cmds { - cmd, err := c.CreateCommand(cli) - if err != nil { - return err - } - - subs := c.SubCommands() - if len(subs) > 0 { - for _, sub := range subs { - subCommand, err := b.createCommand(sub) - if err != nil { - return err - } - - err = b.registerCommands(cmd, subCommand) - if err != nil { - return err - } - } - } - } - - return nil -} - -func (b *AppBuilder) HasDefinition() bool { - source, _ := b.findConfigFile(fmt.Sprintf(appDefPattern, b.name), "BUILDER_APP") - if source == "" { - return false - } - - return util.FileExist(source) -} - -func (b *AppBuilder) loadDefinition(name string) (*Definition, error) { - source, err := b.findConfigFile(fmt.Sprintf(appDefPattern, name), "BUILDER_APP") - if err != nil { - return nil, errDefinitionNotfound - } - - if b.log != nil { - b.log.Infof("Loading application definition %v", source) - } - - cfg, err := os.ReadFile(source) - if err != nil { - return nil, err - } - - d := &Definition{} - cfgj, err := yaml.YAMLToJSON(cfg) - if err != nil { - return nil, err - } - - if os.Getenv("BUILDER_NOVALIDATE") == "" { - schema, err := fs.FS.ReadFile("schemas/builder.json") - if err != nil { - return nil, fmt.Errorf("could not load schema: %v", err) - } - - sloader := gojsonschema.NewBytesLoader(schema) - dloader := gojsonschema.NewBytesLoader(cfgj) - result, err := gojsonschema.Validate(sloader, dloader) - if err != nil { - return nil, fmt.Errorf("schema validation failed: %s", err) - } - - if !result.Valid() { - fmt.Printf("The Builder Application %s does not pass validation against https://choria.io/schemas/choria/builder/v1/application.json:\n\n", source) - for _, err := range result.Errors() { - fmt.Printf(" - %s\n", err) - } - - return nil, fmt.Errorf("validation failed") - } - } - - err = json.Unmarshal(cfgj, d) - if err != nil { - return nil, err - } - - return d, b.createCommands(d, d.Commands) -} - -func (b *AppBuilder) createCommands(d *Definition, defs []json.RawMessage) error { - for _, c := range defs { - cmd, err := b.createCommand(c) - if err != nil { - return err - } - - d.commands = append(d.commands, cmd) - } - - return nil -} - -func (b *AppBuilder) createCommand(def json.RawMessage) (command, error) { - t := gjson.GetBytes(def, "type") - if !t.Exists() { - return nil, fmt.Errorf("command does not have a type\n%s", string(def)) - } - - switch t.String() { - case "rpc": - return NewRPCCommand(b, def, b.log) - case "parent": - return NewParentCommand(b, def, b.log) - case "kv": - return NewKVCommand(b, def, b.log) - case "exec": - return NewExecCommand(b, def, b.log) - case "discover": - return NewDiscoverCommand(b, def, b.log) - default: - return nil, fmt.Errorf("unknown command type %q", t.String()) - } -} - -func (b *AppBuilder) runWrapper(cmd StandardCommand, handler kingpin.Action) kingpin.Action { - return func(pc *kingpin.ParseContext) error { - if cmd.ConfirmPrompt != "" { - ans := false - err := survey.AskOne(&survey.Confirm{Message: cmd.ConfirmPrompt, Default: false}, &ans) - if err != nil { - return err - } - if !ans { - return fmt.Errorf("aborted") - } - } - - return handler(pc) - } -} - -func (b *AppBuilder) findConfigFile(name string, env string) (string, error) { - sources := []string{ - filepath.Join(xdg.ConfigHome, "choria", "builder"), - "/etc/choria/builder", - } - - cur, err := filepath.Abs(".") - if err == nil { - sources = append([]string{cur}, sources...) - } - - if b.log != nil { - b.log.Debugf("Searching for app definition %s in %v", name, sources) - } - - source := os.Getenv(env) - - if source == "" { - for _, s := range sources { - path := filepath.Join(s, name) - if choria.FileExist(path) { - source = path - break - } - } - } - - if source == "" { - return "", fmt.Errorf("could not find configuration %s in %s", name, strings.Join(sources, ", ")) - } - - return source, nil -} - -func (b *AppBuilder) loadConfig() error { - source, err := b.findConfigFile("applications.yaml", "BUILDER_CONFIG") - if err != nil { - return nil - } - - b.log.Debugf("Loading configuration file %s", source) - - cfgb, err := os.ReadFile(source) - if err != nil { - return err - } - - cfgj, err := yaml.YAMLToJSON(cfgb) - if err != nil { - return err - } - - b.cfg = map[string]interface{}{} - - return json.Unmarshal(cfgj, &b.cfg) -} - -func parseStateTemplate(body string, args interface{}, flags interface{}, cfg interface{}) (string, error) { - state := templateState{ - Arguments: args, - Flags: flags, - Config: cfg, - } - - funcs := map[string]interface{}{ - "require": func(v interface{}, reason string) (interface{}, error) { - err := errors.New("value required") - if reason != "" { - err = errors.New(reason) - } - - switch val := v.(type) { - case string: - if val == "" { - return "", err - } - default: - if v == nil { - return "", err - } - } - - return v, nil - }, - "escape": func(v string) string { - return shellescape.Quote(v) - }, - "read_file": func(v string) (string, error) { - b, err := os.ReadFile(v) - if err != nil { - return "", err - } - - return string(b), nil - }, - "default": func(v string, dflt string) string { - if v != "" { - return v - } - - return dflt - }, - } - - temp, err := template.New("choria").Funcs(funcs).Parse(body) - if err != nil { - return "", err - } - - var b bytes.Buffer - err = temp.Execute(&b, state) - if err != nil { - return "", err - } - - return b.String(), nil -} - -func createStandardCommand(app inter.FlagApp, b *AppBuilder, sc *StandardCommand, arguments map[string]*string, flags map[string]*string, cb kingpin.Action) *kingpin.CmdClause { - cmd := app.Command(sc.Name, sc.Description).Action(b.runWrapper(*sc, cb)) - for _, a := range sc.Aliases { - cmd.Alias(a) - } - - if arguments != nil { - for _, a := range sc.Arguments { - arg := cmd.Arg(a.Name, a.Description) - if a.Required { - arg.Required() - } - - arguments[a.Name] = arg.String() - } - } - - if flags != nil { - for _, f := range sc.Flags { - flag := cmd.Flag(f.Name, f.Description) - if f.Required { - flag.Required() - } - if f.PlaceHolder != "" { - flag.PlaceHolder(f.PlaceHolder) - } - flags[f.Name] = flag.String() - } - } - - return cmd -} - -func processStdDiscoveryOptions(f *discovery.StandardOptions, arguments interface{}, flags interface{}, config interface{}) error { - var err error - - if f.Collective != "" { - f.Collective, err = parseStateTemplate(f.Collective, arguments, flags, config) - if err != nil { - return err - } - } - - if f.NodesFile != "" { - f.NodesFile, err = parseStateTemplate(f.NodesFile, arguments, flags, config) - if err != nil { - return err - } - } - - if f.CompoundFilter != "" { - f.CompoundFilter, err = parseStateTemplate(f.CompoundFilter, arguments, flags, config) - if err != nil { - return err - } - } - - for i, item := range f.CombinedFilter { - f.CombinedFilter[i], err = parseStateTemplate(item, arguments, flags, config) - if err != nil { - return err - } - } - - for i, item := range f.IdentityFilter { - f.IdentityFilter[i], err = parseStateTemplate(item, arguments, flags, config) - if err != nil { - return err - } - } - - for i, item := range f.AgentFilter { - f.AgentFilter[i], err = parseStateTemplate(item, arguments, flags, config) - if err != nil { - return err - } - } - - for i, item := range f.ClassFilter { - f.ClassFilter[i], err = parseStateTemplate(item, arguments, flags, config) - if err != nil { - return err - } - } - - for i, item := range f.FactFilter { - f.FactFilter[i], err = parseStateTemplate(item, arguments, flags, config) - if err != nil { - return err - } - } - - return nil -} diff --git a/providers/appbuilder/discover.go b/providers/appbuilder/discover/discover.go similarity index 64% rename from providers/appbuilder/discover.go rename to providers/appbuilder/discover/discover.go index 43fed049e..4e17b8896 100644 --- a/providers/appbuilder/discover.go +++ b/providers/appbuilder/discover/discover.go @@ -2,48 +2,50 @@ // // SPDX-License-Identifier: Apache-2.0 -package appbuilder +package discover import ( "context" "encoding/json" "fmt" + + "github.com/choria-io/appbuilder/builder" "github.com/choria-io/go-choria/config" + "github.com/choria-io/go-choria/providers/appbuilder" "github.com/sirupsen/logrus" "github.com/choria-io/go-choria/choria" "github.com/choria-io/go-choria/client/discovery" - "github.com/choria-io/go-choria/inter" "gopkg.in/alecthomas/kingpin.v2" ) -type DiscoverCommand struct { +type Command struct { StandardFilter bool `json:"std_filters"` Filter *discovery.StandardOptions `json:"filter"` - StandardCommand - StandardSubCommands + builder.GenericCommand + builder.GenericSubCommands } type Discover struct { - b *AppBuilder + b *builder.AppBuilder cmd *kingpin.CmdClause fo *discovery.StandardOptions - def *DiscoverCommand + def *Command cfg interface{} arguments map[string]*string flags map[string]*string json bool - log *logrus.Entry + log builder.Logger ctx context.Context } -func NewDiscoverCommand(b *AppBuilder, j json.RawMessage, log *logrus.Entry) (*Discover, error) { +func NewDiscoverCommand(b *builder.AppBuilder, j json.RawMessage, log builder.Logger) (builder.Command, error) { find := &Discover{ arguments: map[string]*string{}, flags: map[string]*string{}, - def: &DiscoverCommand{}, - cfg: b.cfg, - ctx: b.ctx, + def: &Command{}, + cfg: b.Configuration(), + ctx: b.Context(), log: log, b: b, } @@ -56,12 +58,20 @@ func NewDiscoverCommand(b *AppBuilder, j json.RawMessage, log *logrus.Entry) (*D return find, nil } +func MustRegister() { + builder.MustRegisterCommand("discover", NewDiscoverCommand) +} + +func (r *Discover) Validate(log builder.Logger) error { return nil } + +func (r *Discover) String() string { return fmt.Sprintf("%s (discover)", r.def.Name) } + func (r *Discover) SubCommands() []json.RawMessage { return r.def.Commands } -func (r *Discover) CreateCommand(app inter.FlagApp) (*kingpin.CmdClause, error) { - r.cmd = createStandardCommand(app, r.b, &r.def.StandardCommand, r.arguments, r.flags, r.runCommand) +func (r *Discover) CreateCommand(app builder.KingpinCommand) (*kingpin.CmdClause, error) { + r.cmd = builder.CreateGenericCommand(app, &r.def.GenericCommand, r.arguments, r.flags, r.runCommand) r.fo = discovery.NewStandardOptions() @@ -81,7 +91,11 @@ func (r *Discover) runCommand(_ *kingpin.ParseContext) error { if err != nil { return err } - cfg.CustomLogger = r.log.Logger + + logger, ok := interface{}(r.log).(*logrus.Logger) + if ok { + cfg.CustomLogger = logger + } fw, err := choria.NewWithConfig(cfg) if err != nil { @@ -91,7 +105,7 @@ func (r *Discover) runCommand(_ *kingpin.ParseContext) error { log := fw.Logger("find") if r.def.Filter != nil { - err = processStdDiscoveryOptions(r.def.Filter, r.arguments, r.flags, r.cfg) + err = appbuilder.ProcessStdDiscoveryOptions(r.def.Filter, r.arguments, r.flags, r.cfg) if err != nil { return err } diff --git a/providers/appbuilder/exec.go b/providers/appbuilder/exec.go deleted file mode 100644 index c792117ac..000000000 --- a/providers/appbuilder/exec.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package appbuilder - -import ( - "context" - "encoding/json" - "fmt" - "github.com/sirupsen/logrus" - "os" - "os/exec" - - "github.com/choria-io/go-choria/inter" - "github.com/kballard/go-shellquote" - "gopkg.in/alecthomas/kingpin.v2" -) - -type ExecCommand struct { - Command string `json:"command"` - - StandardSubCommands - StandardCommand -} - -type Exec struct { - Arguments map[string]*string - Flags map[string]*string - cmd *kingpin.CmdClause - def *ExecCommand - cfg interface{} - ctx context.Context - b *AppBuilder -} - -func NewExecCommand(b *AppBuilder, j json.RawMessage, _ *logrus.Entry) (*Exec, error) { - exec := &Exec{ - def: &ExecCommand{}, - cfg: b.cfg, - ctx: b.ctx, - b: b, - Arguments: map[string]*string{}, - Flags: map[string]*string{}, - } - - err := json.Unmarshal(j, exec.def) - if err != nil { - return nil, err - } - - return exec, nil -} - -func (r *Exec) SubCommands() []json.RawMessage { - return r.def.Commands -} - -func (r *Exec) CreateCommand(app inter.FlagApp) (*kingpin.CmdClause, error) { - r.cmd = createStandardCommand(app, r.b, &r.def.StandardCommand, r.Arguments, r.Flags, r.runCommand) - - return r.cmd, nil -} - -func (r *Exec) runCommand(_ *kingpin.ParseContext) error { - cmd, err := parseStateTemplate(r.def.Command, r.Arguments, r.Flags, r.cfg) - if err != nil { - return err - } - - parts, err := shellquote.Split(cmd) - if err != nil { - return err - } - if len(parts) == 0 { - return fmt.Errorf("invalid command") - } - - run := exec.CommandContext(r.ctx, parts[0], parts[1:]...) - run.Env = os.Environ() - run.Stdin = os.Stdin - run.Stdout = os.Stdout - run.Stderr = os.Stderr - - err = run.Run() - if err != nil { - return err - } - - return nil -} diff --git a/providers/appbuilder/kv.go b/providers/appbuilder/kv/kv.go similarity index 79% rename from providers/appbuilder/kv.go rename to providers/appbuilder/kv/kv.go index b929c92f8..f5b5f1b81 100644 --- a/providers/appbuilder/kv.go +++ b/providers/appbuilder/kv/kv.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package appbuilder +package kv import ( "context" @@ -10,43 +10,43 @@ import ( "fmt" "time" + "github.com/choria-io/appbuilder/builder" "github.com/choria-io/go-choria/config" "github.com/sirupsen/logrus" "github.com/choria-io/go-choria/choria" - "github.com/choria-io/go-choria/inter" "github.com/choria-io/go-choria/internal/util" "github.com/nats-io/nats.go" "gopkg.in/alecthomas/kingpin.v2" ) -type KVCommand struct { +type Command struct { Action string `json:"action"` Bucket string `json:"bucket"` Key string `json:"key"` Value string `json:"value"` RenderJSON bool `json:"json"` - StandardSubCommands - StandardCommand + builder.GenericCommand + builder.GenericSubCommands } type KV struct { - b *AppBuilder + b *builder.AppBuilder Arguments map[string]*string Flags map[string]*string cmd *kingpin.CmdClause - def *KVCommand + def *Command cfg interface{} - log *logrus.Entry + log builder.Logger ctx context.Context } -func NewKVCommand(b *AppBuilder, j json.RawMessage, log *logrus.Entry) (*KV, error) { +func NewKVCommand(b *builder.AppBuilder, j json.RawMessage, log builder.Logger) (builder.Command, error) { kv := &KV{ - def: &KVCommand{}, - cfg: b.cfg, - ctx: b.ctx, + def: &Command{}, + cfg: b.Configuration(), + ctx: b.Context(), b: b, log: log, Arguments: map[string]*string{}, @@ -61,12 +61,19 @@ func NewKVCommand(b *AppBuilder, j json.RawMessage, log *logrus.Entry) (*KV, err return kv, nil } +func MustRegister() { + builder.MustRegisterCommand("kv", NewKVCommand) +} + +func (r *KV) Validate(log builder.Logger) error { return nil } +func (r *KV) String() string { return fmt.Sprintf("%s (kv)", r.def.Name) } + func (r *KV) SubCommands() []json.RawMessage { return r.def.Commands } -func (r *KV) CreateCommand(app inter.FlagApp) (*kingpin.CmdClause, error) { - r.cmd = createStandardCommand(app, r.b, &r.def.StandardCommand, r.Arguments, r.Flags, r.runCommand) +func (r *KV) CreateCommand(app builder.KingpinCommand) (*kingpin.CmdClause, error) { + r.cmd = builder.CreateGenericCommand(app, &r.def.GenericCommand, r.Arguments, r.Flags, r.runCommand) if r.def.Action == "get" || r.def.Action == "history" && !r.def.RenderJSON { r.cmd.Flag("json", "Renders results in JSON format").BoolVar(&r.def.RenderJSON) @@ -96,7 +103,7 @@ func (r *KV) getAction(kv nats.KeyValue) error { } func (r *KV) putAction(kv nats.KeyValue) error { - v, err := parseStateTemplate(r.def.Value, r.Arguments, r.Flags, r.cfg) + v, err := builder.ParseStateTemplate(r.def.Value, r.Arguments, r.Flags, r.cfg) if err != nil { return err } @@ -198,7 +205,11 @@ func (r *KV) runCommand(_ *kingpin.ParseContext) error { if err != nil { return err } - cfg.CustomLogger = r.log.Logger + + logger, ok := interface{}(r.log).(*logrus.Logger) + if ok { + cfg.CustomLogger = logger + } fw, err := choria.NewWithConfig(cfg) if err != nil { diff --git a/providers/appbuilder/parent.go b/providers/appbuilder/parent.go deleted file mode 100644 index 00ef446ff..000000000 --- a/providers/appbuilder/parent.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package appbuilder - -import ( - "encoding/json" - "github.com/sirupsen/logrus" - - "github.com/choria-io/go-choria/inter" - "gopkg.in/alecthomas/kingpin.v2" -) - -type ParentCommand struct { - StandardSubCommands - StandardCommand -} - -type Parent struct { - cmd *kingpin.CmdClause - def *ParentCommand -} - -func NewParentCommand(_ *AppBuilder, j json.RawMessage, _ *logrus.Entry) (*Parent, error) { - parent := &Parent{ - def: &ParentCommand{}, - } - - err := json.Unmarshal(j, parent.def) - if err != nil { - return nil, err - } - - return parent, nil -} - -func (p *Parent) SubCommands() []json.RawMessage { - return p.def.Commands -} - -func (p *Parent) CreateCommand(app inter.FlagApp) (*kingpin.CmdClause, error) { - p.cmd = app.Command(p.def.Name, p.def.Description) - for _, a := range p.def.Aliases { - p.cmd.Alias(a) - } - - return p.cmd, nil -} diff --git a/providers/appbuilder/rpc.go b/providers/appbuilder/rpc/rpc.go similarity index 82% rename from providers/appbuilder/rpc.go rename to providers/appbuilder/rpc/rpc.go index fd39d730d..0cc2e806b 100644 --- a/providers/appbuilder/rpc.go +++ b/providers/appbuilder/rpc/rpc.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package appbuilder +package rpc import ( "bytes" @@ -14,7 +14,9 @@ import ( "sync" "time" + "github.com/choria-io/appbuilder/builder" "github.com/choria-io/go-choria/config" + "github.com/choria-io/go-choria/providers/appbuilder" "github.com/AlecAivazis/survey/v2" "github.com/choria-io/go-choria/choria" @@ -30,42 +32,42 @@ import ( "gopkg.in/alecthomas/kingpin.v2" ) -type RPCFlag struct { - GenericFlag +type Flag struct { + builder.GenericFlag ReplyFilter string `json:"reply_filter"` } -type RPCRequest struct { +type Request struct { Agent string `json:"agent"` Action string `json:"action"` Params map[string]string `json:"inputs"` Filter *discovery.StandardOptions `json:"filter"` } -type RPCCommand struct { - StandardFilter bool `json:"std_filters"` - OutputFormatFlags bool `json:"output_format_flags"` - OutputFormat string `json:"output_format"` - Display string `json:"display"` - DisplayFlag bool `json:"display_flag"` - BatchFlags bool `json:"batch_flags"` - BatchSize int `json:"batch"` - BatchSleep int `json:"batch_sleep"` - NoProgress bool `json:"no_progress"` - AllNodesConfirmPrompt string `json:"all_nodes_confirm_prompt"` - Flags []RPCFlag `json:"flags"` - Request RPCRequest `json:"request"` - Transform *GenericTransform `json:"transform"` - - StandardCommand - StandardSubCommands +type Command struct { + StandardFilter bool `json:"std_filters"` + OutputFormatFlags bool `json:"output_format_flags"` + OutputFormat string `json:"output_format"` + Display string `json:"display"` + DisplayFlag bool `json:"display_flag"` + BatchFlags bool `json:"batch_flags"` + BatchSize int `json:"batch"` + BatchSleep int `json:"batch_sleep"` + NoProgress bool `json:"no_progress"` + AllNodesConfirmPrompt string `json:"all_nodes_confirm_prompt"` + Flags []Flag `json:"flags"` + Request Request `json:"request"` + Transform *builder.GenericTransform `json:"transform"` + + builder.GenericCommand + builder.GenericSubCommands } type RPC struct { - b *AppBuilder + b *builder.AppBuilder cmd *kingpin.CmdClause fo *discovery.StandardOptions - def *RPCCommand + def *Command cfg interface{} arguments map[string]*string flags map[string]*string @@ -77,17 +79,17 @@ type RPC struct { batchSleep int jqQuery *gojq.Query progressBar *uiprogress.Bar - log *logrus.Entry + log builder.Logger ctx context.Context } -func NewRPCCommand(b *AppBuilder, j json.RawMessage, log *logrus.Entry) (*RPC, error) { +func NewRPCCommand(b *builder.AppBuilder, j json.RawMessage, log builder.Logger) (builder.Command, error) { rpc := &RPC{ arguments: map[string]*string{}, flags: map[string]*string{}, - def: &RPCCommand{}, - cfg: b.cfg, - ctx: b.ctx, + def: &Command{}, + cfg: b.Configuration(), + ctx: b.Context(), b: b, log: log, } @@ -107,12 +109,20 @@ func NewRPCCommand(b *AppBuilder, j json.RawMessage, log *logrus.Entry) (*RPC, e return rpc, nil } +func MustRegister() { + builder.MustRegisterCommand("rpc", NewRPCCommand) +} + +func (r *RPC) String() string { return fmt.Sprintf("%s (rpc)", r.def.Name) } + +func (r *RPC) Validate(log builder.Logger) error { return nil } + func (r *RPC) SubCommands() []json.RawMessage { return r.def.Commands } -func (r *RPC) CreateCommand(app inter.FlagApp) (*kingpin.CmdClause, error) { - r.cmd = createStandardCommand(app, r.b, &r.def.StandardCommand, r.arguments, nil, r.runCommand) +func (r *RPC) CreateCommand(app builder.KingpinCommand) (*kingpin.CmdClause, error) { + r.cmd = builder.CreateGenericCommand(app, &r.def.GenericCommand, r.arguments, nil, r.runCommand) switch { case r.def.OutputFormatFlags && r.def.OutputFormat != "": @@ -216,7 +226,7 @@ func (r *RPC) setupFilter(fw inter.Framework) error { } if r.def.Request.Filter != nil { - err = processStdDiscoveryOptions(r.def.Request.Filter, r.arguments, r.flags, r.cfg) + err = appbuilder.ProcessStdDiscoveryOptions(r.def.Request.Filter, r.arguments, r.flags, r.cfg) if err != nil { return err } @@ -258,7 +268,11 @@ func (r *RPC) runCommand(_ *kingpin.ParseContext) error { if err != nil { return err } - cfg.CustomLogger = r.log.Logger + + logger, ok := interface{}(r.log).(*logrus.Logger) + if ok { + cfg.CustomLogger = logger + } fw, err := choria.NewWithConfig(cfg) if err != nil { @@ -466,5 +480,5 @@ func (r *RPC) reqOptions(action *agent.Action) (inputs map[string]string, rpcInp } func (r *RPC) parseStateTemplate(body string) (string, error) { - return parseStateTemplate(body, r.arguments, r.flags, r.cfg) + return builder.ParseStateTemplate(body, r.arguments, r.flags, r.cfg) } diff --git a/providers/appbuilder/util.go b/providers/appbuilder/util.go new file mode 100644 index 000000000..759a8d652 --- /dev/null +++ b/providers/appbuilder/util.go @@ -0,0 +1,72 @@ +// Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package appbuilder + +import ( + "github.com/choria-io/appbuilder/builder" + "github.com/choria-io/go-choria/client/discovery" +) + +func ProcessStdDiscoveryOptions(f *discovery.StandardOptions, arguments interface{}, flags interface{}, config interface{}) error { + var err error + + if f.Collective != "" { + f.Collective, err = builder.ParseStateTemplate(f.Collective, arguments, flags, config) + if err != nil { + return err + } + } + + if f.NodesFile != "" { + f.NodesFile, err = builder.ParseStateTemplate(f.NodesFile, arguments, flags, config) + if err != nil { + return err + } + } + + if f.CompoundFilter != "" { + f.CompoundFilter, err = builder.ParseStateTemplate(f.CompoundFilter, arguments, flags, config) + if err != nil { + return err + } + } + + for i, item := range f.CombinedFilter { + f.CombinedFilter[i], err = builder.ParseStateTemplate(item, arguments, flags, config) + if err != nil { + return err + } + } + + for i, item := range f.IdentityFilter { + f.IdentityFilter[i], err = builder.ParseStateTemplate(item, arguments, flags, config) + if err != nil { + return err + } + } + + for i, item := range f.AgentFilter { + f.AgentFilter[i], err = builder.ParseStateTemplate(item, arguments, flags, config) + if err != nil { + return err + } + } + + for i, item := range f.ClassFilter { + f.ClassFilter[i], err = builder.ParseStateTemplate(item, arguments, flags, config) + if err != nil { + return err + } + } + + for i, item := range f.FactFilter { + f.FactFilter[i], err = builder.ParseStateTemplate(item, arguments, flags, config) + if err != nil { + return err + } + } + + return nil +}