From 5848048c17aae230353aee3221e24d9f2b727a7d Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Fri, 27 Mar 2020 15:43:37 +0100 Subject: [PATCH 01/19] - First attempt at using a new cli framework. WIP. --- README.md | 7 ++ cmd/centry/help.go | 1 + cmd/centry/options.go | 61 ++++++++++++++++++ cmd/centry/runtime.go | 146 +++++++++++++++++++++++++++++------------- cmd/centry/script.go | 57 +++++++++++------ cmd/centry/serve.go | 16 +++++ go.mod | 1 + go.sum | 11 ++++ 8 files changed, 236 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 88d9f4d..8aec0a8 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,10 @@ scripts/test ```bash scripts/run-centry ``` + + + + +https://github.com/kristofferahl/go-centry +https://github.com/urfave/cli/blob/master/docs/v2/manual.md#subcommands +https://github.com/spf13/cobra diff --git a/cmd/centry/help.go b/cmd/centry/help.go index 1bc5c5f..bc4d6fb 100644 --- a/cmd/centry/help.go +++ b/cmd/centry/help.go @@ -12,6 +12,7 @@ import ( "github.com/mitchellh/cli" ) +// TODO: Remove if unused??? func cliHelpFunc(manifest *config.Manifest, globalOptions *cmd.OptionsSet) cli.HelpFunc { return func(commands map[string]cli.CommandFactory) string { var buf bytes.Buffer diff --git a/cmd/centry/options.go b/cmd/centry/options.go index 7ceb6be..022e9dd 100644 --- a/cmd/centry/options.go +++ b/cmd/centry/options.go @@ -2,6 +2,7 @@ package main import ( "github.com/kristofferahl/go-centry/internal/pkg/cmd" + "github.com/urfave/cli/v2" ) // OptionSetGlobal is the name of the global OptionsSet @@ -75,3 +76,63 @@ func createGlobalOptions(context *Context) *cmd.OptionsSet { return options } + +func createGlobalFlags(context *Context) []cli.Flag { + options := make([]cli.Flag, 0) + manifest := context.manifest + + options = append(options, &cli.StringFlag{ + Name: "config.log.level", + Usage: "Overrides the log level", + Value: manifest.Config.Log.Level, + }) + + options = append(options, &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Disables logging", + Value: false, + }) + + // Adding global options specified by the manifest + for _, o := range manifest.Options { + o := o + + if context.optionEnabledFunc != nil && context.optionEnabledFunc(o) == false { + continue + } + + short := []string{o.Short} + if o.Short == "" { + short = nil + } + + //TODO: Handle EnvName?? + switch o.Type { + case cmd.SelectOption: + + options = append(options, &cli.BoolFlag{ + Name: o.Name, + Aliases: short, + Usage: o.Description, + Value: false, + }) + case cmd.BoolOption: + options = append(options, &cli.BoolFlag{ + Name: o.Name, + Aliases: short, + Usage: o.Description, + Value: false, + }) + case cmd.StringOption: + options = append(options, &cli.StringFlag{ + Name: o.Name, + Aliases: short, + Usage: o.Description, + Value: o.Default, + }) + } + } + + return options +} diff --git a/cmd/centry/runtime.go b/cmd/centry/runtime.go index 052295d..3716740 100644 --- a/cmd/centry/runtime.go +++ b/cmd/centry/runtime.go @@ -6,14 +6,15 @@ import ( "github.com/kristofferahl/go-centry/internal/pkg/config" "github.com/kristofferahl/go-centry/internal/pkg/log" "github.com/kristofferahl/go-centry/internal/pkg/shell" - "github.com/mitchellh/cli" "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" ) // Runtime defines the runtime type Runtime struct { context *Context - cli *cli.CLI + cli *cli.App + args []string } // NewRuntime builds a runtime based on the given arguments @@ -23,10 +24,10 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { // Args file := "" - args := []string{} + runtime.args = []string{} if len(inputArgs) >= 1 { file = inputArgs[0] - args = inputArgs[1:] + runtime.args = inputArgs[1:] } // Load manifest @@ -42,47 +43,47 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { // Create global options options := createGlobalOptions(context) + flags := createGlobalFlags(context) // Parse global options to get cli args - args, err = options.Parse(args, context.io) - if err != nil { - return nil, err - } + // args, err = options.Parse(args, context.io) + // if err != nil { + // return nil, err + // } // Initialize cli - c := &cli.CLI{ - Name: context.manifest.Config.Name, - Version: context.manifest.Config.Version, - - Commands: map[string]cli.CommandFactory{}, - Args: args, - HelpFunc: cliHelpFunc(context.manifest, options), - HelpWriter: context.io.Stderr, - - // Autocomplete: true, - // AutocompleteInstall: "install-autocomplete", - // AutocompleteUninstall: "uninstall-autocomplete", + app := &cli.App{ + Name: context.manifest.Config.Name, + HelpName: context.manifest.Config.Name, + Usage: "A tool for building declarative CLI's over bash scripts, written in go.", // TODO: Set from manifest config + UsageText: "", + Version: context.manifest.Config.Version, + HideHelpCommand: true, + + Commands: make([]*cli.Command, 0), + Flags: flags, } + // TODO: Fix log level from options // Override the current log level from options - logLevel := options.GetString("config.log.level") - if options.GetBool("quiet") { - logLevel = "panic" - } - context.log.TrySetLogLevel(logLevel) + // logLevel := options.GetString("config.log.level") + // if options.GetBool("quiet") { + // logLevel = "panic" + // } + //context.log.TrySetLogLevel(logLevel) + context.log.TrySetLogLevel("debug") logger := context.log.GetLogger() // Register builtin commands if context.executor == CLI { - c.Commands["serve"] = func() (cli.Command, error) { - return &ServeCommand{ - Manifest: context.manifest, - Log: logger.WithFields(logrus.Fields{ - "command": "serve", - }), - }, nil + serveCmd := &ServeCommand{ + Manifest: context.manifest, + Log: logger.WithFields(logrus.Fields{ + "command": "serve", + }), } + app.Commands = append(app.Commands, serveCmd.ToCLICommand()) } // Build commands @@ -100,7 +101,6 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { logger.WithFields(logrus.Fields{ "command": cmd.Name, }).Errorf("Failed to parse script functions. %v", err) - } else { for _, fn := range funcs { fn := fn @@ -111,24 +111,68 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { continue } + cmdDescription := cmd.Description if fn.Description != "" { cmd.Description = fn.Description } + cmdHelp := cmd.Help if fn.Help != "" { cmd.Help = fn.Help } cmdKey := strings.Replace(fn.Name, script.FunctionNameSplitChar(), " ", -1) - c.Commands[cmdKey] = func() (cli.Command, error) { - return &ScriptCommand{ - Context: context, - Log: logger.WithFields(logrus.Fields{}), - GlobalOptions: options, - Command: cmd, - Script: script, - Function: fn.Name, - }, nil + cmdKeyParts := strings.Split(cmdKey, " ") + + scriptCmd := &ScriptCommand{ + Context: context, + Log: logger.WithFields(logrus.Fields{}), + GlobalOptions: options, + Command: cmd, + Script: script, + Function: fn.Name, + } + + cliCmd := scriptCmd.ToCLICommand() + + var root *cli.Command + + for depth, cmdKeyPart := range cmdKeyParts { + if depth == 0 { + if getCommand(app.Commands, cmdKeyPart) == nil { + if depth == len(cmdKeyParts)-1 { + // add destination command + app.Commands = append(app.Commands, cliCmd) + } else { + // add placeholder + app.Commands = append(app.Commands, &cli.Command{ + Name: cmdKeyPart, + Usage: cmdDescription, + UsageText: cmdHelp, + HideHelpCommand: true, + Action: nil, + }) + } + } + root = getCommand(app.Commands, cmdKeyPart) + } else { + if getCommand(root.Subcommands, cmdKeyPart) == nil { + if depth == len(cmdKeyParts)-1 { + // add destination command + root.Subcommands = append(root.Subcommands, cliCmd) + } else { + // add placeholder + root.Subcommands = append(root.Subcommands, &cli.Command{ + Name: cmdKeyPart, + Usage: "...", + UsageText: "", + HideHelpCommand: true, + Action: nil, + }) + } + } + root = getCommand(root.Subcommands, cmdKeyPart) + } } logger.Debugf("Registered command \"%s\"", cmdKey) @@ -137,21 +181,23 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { } runtime.context = context - runtime.cli = c + runtime.cli = app return runtime, nil } // Execute runs the CLI and exits with a code func (runtime *Runtime) Execute() int { + args := append([]string{""}, runtime.args...) + // Run cli - exitCode, err := runtime.cli.Run() + err := runtime.cli.Run(args) if err != nil { logger := runtime.context.log.GetLogger() logger.Error(err) } - return exitCode + return 0 } func createScript(cmd config.Command, context *Context) shell.Script { @@ -163,3 +209,13 @@ func createScript(cmd config.Command, context *Context) shell.Script { }), } } + +func getCommand(commands []*cli.Command, name string) *cli.Command { + for _, c := range commands { + if c.HasName(name) { + return c + } + } + + return nil +} diff --git a/cmd/centry/script.go b/cmd/centry/script.go index 6999cfb..083514f 100644 --- a/cmd/centry/script.go +++ b/cmd/centry/script.go @@ -10,6 +10,7 @@ import ( "github.com/kristofferahl/go-centry/internal/pkg/config" "github.com/kristofferahl/go-centry/internal/pkg/shell" "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" ) // ScriptCommand is a Command implementation that applies stuff @@ -22,21 +23,39 @@ type ScriptCommand struct { Function string } +// ToCLICommand returns a CLI command +func (sc *ScriptCommand) ToCLICommand() *cli.Command { + cmdKeys := strings.Split(strings.Replace(sc.Function, sc.Script.FunctionNameSplitChar(), " ", -1), " ") + return &cli.Command{ + Name: cmdKeys[len(cmdKeys)-1], + Usage: sc.Synopsis(), + UsageText: sc.Help(), + HideHelpCommand: true, + Action: func(c *cli.Context) error { + ec := sc.Run(c.Args().Slice()) + if ec > 0 { + return cli.Exit("command exited with non zero exit code", ec) + } + return nil + }, + } +} + // Run builds the source and executes it -func (c *ScriptCommand) Run(args []string) int { - c.Log.Debugf("Executing command \"%v\"", c.Function) +func (sc *ScriptCommand) Run(args []string) int { + sc.Log.Debugf("Executing command \"%v\"", sc.Function) var source string - switch c.Script.Language() { + switch sc.Script.Language() { case "bash": - source = generateBashSource(c, args) - c.Log.Debugf("Generated bash source:\n%s\n", source) + source = generateBashSource(sc, args) + sc.Log.Debugf("Generated bash source:\n%s\n", source) default: - c.Log.Errorf("Unsupported script language %s", c.Script.Language()) + sc.Log.Errorf("Unsupported script language %s", sc.Script.Language()) return 1 } - err := c.Script.Executable().Run(c.Context.io, []string{"-c", source}) + err := sc.Script.Executable().Run(sc.Context.io, []string{"-c", source}) if err != nil { exitCode := 1 @@ -46,36 +65,36 @@ func (c *ScriptCommand) Run(args []string) int { } } - c.Log.Errorf("Command %v exited with error! %v", c.Function, err) + sc.Log.Errorf("Command %v exited with error! %v", sc.Function, err) return exitCode } - c.Log.Debugf("Finished executing command %v...", c.Function) + sc.Log.Debugf("Finished executing command %v...", sc.Function) return 0 } // Help returns the help text of the ScriptCommand -func (c *ScriptCommand) Help() string { - return c.Command.Help +func (sc *ScriptCommand) Help() string { + return sc.Command.Help } // Synopsis returns the synopsis of the ScriptCommand -func (c *ScriptCommand) Synopsis() string { - return c.Command.Description +func (sc *ScriptCommand) Synopsis() string { + return sc.Command.Description } -func generateBashSource(c *ScriptCommand, args []string) string { +func generateBashSource(sc *ScriptCommand, args []string) string { source := []string{} source = append(source, "#!/usr/bin/env bash") source = append(source, "") source = append(source, "# Set working directory") - source = append(source, fmt.Sprintf("cd %s || exit 1", c.Context.manifest.BasePath)) + source = append(source, fmt.Sprintf("cd %s || exit 1", sc.Context.manifest.BasePath)) source = append(source, "") source = append(source, "# Set exports from flags") - for _, v := range optionsSetToEnvVars(c.GlobalOptions) { + for _, v := range optionsSetToEnvVars(sc.GlobalOptions) { if v.Value != "" { value := v.Value if v.IsString() { @@ -87,17 +106,17 @@ func generateBashSource(c *ScriptCommand, args []string) string { source = append(source, "") source = append(source, "# Sourcing scripts") - for _, s := range c.Context.manifest.Scripts { + for _, s := range sc.Context.manifest.Scripts { source = append(source, fmt.Sprintf("source %s", s)) } source = append(source, "") source = append(source, "# Sourcing command") - source = append(source, fmt.Sprintf("source %s", c.Script.FullPath())) + source = append(source, fmt.Sprintf("source %s", sc.Script.FullPath())) source = append(source, "") source = append(source, "# Executing command") - source = append(source, fmt.Sprintf("%s %s", c.Function, strings.Join(args, " "))) + source = append(source, fmt.Sprintf("%s %s", sc.Function, strings.Join(args, " "))) return strings.Join(source, "\n") } diff --git a/cmd/centry/serve.go b/cmd/centry/serve.go index b99efee..f92e220 100644 --- a/cmd/centry/serve.go +++ b/cmd/centry/serve.go @@ -11,6 +11,7 @@ import ( "github.com/kristofferahl/go-centry/internal/pkg/config" "github.com/kristofferahl/go-centry/internal/pkg/io" "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" ) // ServeCommand is a Command implementation that applies stuff @@ -19,6 +20,21 @@ type ServeCommand struct { Log *logrus.Entry } +// ToCLICommand returns a CLI command +func (sc *ServeCommand) ToCLICommand() *cli.Command { + return &cli.Command{ + Name: "serve", + Usage: sc.Synopsis(), + Action: func(c *cli.Context) error { + ec := sc.Run(c.Args().Slice()) + if ec > 0 { + return cli.Exit("failed to start the server", ec) + } + return nil + }, + } +} + // Run starts an HTTP server and blocks func (sc *ServeCommand) Run(args []string) int { sc.Log.Debugf("Serving HTTP api") diff --git a/go.mod b/go.mod index c5666e4..1336cf1 100644 --- a/go.mod +++ b/go.mod @@ -9,5 +9,6 @@ require ( github.com/mitchellh/cli v1.0.0 github.com/santhosh-tekuri/jsonschema/v2 v2.1.0 github.com/sirupsen/logrus v1.4.2 + github.com/urfave/cli/v2 v2.2.0 gopkg.in/yaml.v2 v2.2.7 ) diff --git a/go.sum b/go.sum index 02a8fa0..ae3f2a8 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,10 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= @@ -28,17 +31,25 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v2 v2.1.0 h1:7KOtBzox6l1PbyZCuQfo923yIBpoMtGCDOD78P9lv9g= github.com/santhosh-tekuri/jsonschema/v2 v2.1.0/go.mod h1:yzJzKUGV4RbWqWIBBP4wSOBqavX5saE02yirLS0OTyg= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From ceaad4f2aabc1c180643b06c8716408b00116cbc Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Sun, 29 Mar 2020 15:08:29 +0200 Subject: [PATCH 02/19] - More work for using new cli. --- cmd/centry/env.go | 5 +- cmd/centry/help.go | 106 ++------- cmd/centry/options.go | 62 ++---- cmd/centry/runtime.go | 91 ++++---- cmd/centry/runtime_test.go | 63 +++--- cmd/centry/script.go | 39 ++-- cmd/centry/serve.go | 15 +- go.mod | 1 - go.sum | 20 -- internal/pkg/cmd/option.go | 142 +----------- internal/pkg/cmd/option_test.go | 371 -------------------------------- internal/pkg/config/manifest.go | 2 +- 12 files changed, 144 insertions(+), 773 deletions(-) diff --git a/cmd/centry/env.go b/cmd/centry/env.go index cb5a748..6a608a4 100644 --- a/cmd/centry/env.go +++ b/cmd/centry/env.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/kristofferahl/go-centry/internal/pkg/cmd" + "github.com/urfave/cli/v2" ) type envType string @@ -28,7 +29,7 @@ func (v envVar) IsBool() bool { return v.Type == envTypeBool } -func optionsSetToEnvVars(set *cmd.OptionsSet) []envVar { +func optionsSetToEnvVars(c *cli.Context, set *cmd.OptionsSet) []envVar { envVars := make([]envVar, 0) for _, o := range set.Sorted() { o := o @@ -40,7 +41,7 @@ func optionsSetToEnvVars(set *cmd.OptionsSet) []envVar { envName = strings.Replace(strings.ToUpper(envName), ".", "_", -1) envName = strings.Replace(strings.ToUpper(envName), "-", "_", -1) - value := set.GetValueString(o.Name) + value := c.String(o.Name) switch o.Type { case cmd.BoolOption: diff --git a/cmd/centry/help.go b/cmd/centry/help.go index bc4d6fb..a4bde27 100644 --- a/cmd/centry/help.go +++ b/cmd/centry/help.go @@ -1,94 +1,30 @@ package main -import ( - "bytes" - "fmt" - "log" - "sort" - "strings" +var cliHelpTemplate = `NAME: + {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} - "github.com/kristofferahl/go-centry/internal/pkg/cmd" - "github.com/kristofferahl/go-centry/internal/pkg/config" - "github.com/mitchellh/cli" -) +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} -// TODO: Remove if unused??? -func cliHelpFunc(manifest *config.Manifest, globalOptions *cmd.OptionsSet) cli.HelpFunc { - return func(commands map[string]cli.CommandFactory) string { - var buf bytes.Buffer - buf.WriteString(fmt.Sprintf("Usage: %s [] []\n\n", manifest.Config.Name)) +VERSION: + {{.Version}}{{end}}{{end}}{{if .Description}} - writeCommands(&buf, commands, manifest) - writeOptionsSet(&buf, globalOptions) +DESCRIPTION: + {{.Description}}{{end}}{{if len .Authors}} - return buf.String() - } -} +AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: + {{range $index, $author := .Authors}}{{if $index}} + {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} -func writeCommands(buf *bytes.Buffer, commands map[string]cli.CommandFactory, manifest *config.Manifest) { - buf.WriteString("Commands:\n") +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + {{.Name}}:{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} - // Get the list of keys so we can sort them, and also get the maximum - // key length so they can be aligned properly. - keys := make([]string, 0, len(commands)) - maxKeyLen := 0 - for key := range commands { - if len(key) > maxKeyLen { - maxKeyLen = len(key) - } +GLOBAL OPTIONS: + {{range $index, $option := .VisibleFlags}}{{if $index}} + {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} - keys = append(keys, key) - } - sort.Strings(keys) - - for _, key := range keys { - commandFunc, ok := commands[key] - if !ok { - // This should never happen since we JUST built the list of keys. - panic("command not found: " + key) - } - - command, err := commandFunc() - if err != nil { - log.Printf("[ERR] cli: Command '%s' failed to load: %s", key, err) - continue - } - - synopsis := command.Synopsis() - if synopsis == "" { - for _, mc := range manifest.Commands { - if mc.Name == key { - synopsis = mc.Description - } - } - } - key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key))) - buf.WriteString(fmt.Sprintf(" %s %s\n", key, synopsis)) - } -} - -func writeOptionsSet(buf *bytes.Buffer, set *cmd.OptionsSet) { - buf.WriteString(fmt.Sprintf("\n%s options:\n", set.Name)) - - sorted := set.Sorted() - - maxKeyLen := 0 - for _, o := range sorted { - key := fmt.Sprintf("--%s", o.Name) - if len(key) > maxKeyLen { - maxKeyLen = len(key) - } - } - - for _, o := range sorted { - l := fmt.Sprintf("--%s", o.Name) - - if o.Short != "" { - l = fmt.Sprintf("%s, -%s", l, o.Short) - } - - n := fmt.Sprintf("%s%s", l, strings.Repeat(" ", maxKeyLen-len(l))) - d := o.Description - buf.WriteString(fmt.Sprintf(" %s %s\n", n, d)) - } -} +COPYRIGHT: + {{.Copyright}}{{end}} +` diff --git a/cmd/centry/options.go b/cmd/centry/options.go index 022e9dd..a90fff9 100644 --- a/cmd/centry/options.go +++ b/cmd/centry/options.go @@ -13,9 +13,6 @@ func createGlobalOptions(context *Context) *cmd.OptionsSet { // Add global options options := cmd.NewOptionsSet(OptionSetGlobal) - options.ShortCircuitParseFunc = func(arg string) bool { - return arg == "-v" || arg == "--v" || arg == "-version" || arg == "--version" || arg == "-h" || arg == "--h" || arg == "-help" || arg == "--help" - } options.Add(&cmd.Option{ Type: cmd.StringOption, @@ -30,20 +27,8 @@ func createGlobalOptions(context *Context) *cmd.OptionsSet { Description: "Disables logging", Default: false, }) - options.Add(&cmd.Option{ - Type: cmd.BoolOption, - Name: "help", - Short: "h", - Description: "Displays help", - Default: false, - }) - options.Add(&cmd.Option{ - Type: cmd.BoolOption, - Name: "version", - Short: "v", - Description: "Displays the version of the cli", - Default: false, - }) + + // TODO: Override default version and help flags to get unified descriptions? // Adding global options specified by the manifest for _, o := range manifest.Options { @@ -77,62 +62,39 @@ func createGlobalOptions(context *Context) *cmd.OptionsSet { return options } -func createGlobalFlags(context *Context) []cli.Flag { - options := make([]cli.Flag, 0) - manifest := context.manifest - - options = append(options, &cli.StringFlag{ - Name: "config.log.level", - Usage: "Overrides the log level", - Value: manifest.Config.Log.Level, - }) - - options = append(options, &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Disables logging", - Value: false, - }) - - // Adding global options specified by the manifest - for _, o := range manifest.Options { - o := o - - if context.optionEnabledFunc != nil && context.optionEnabledFunc(o) == false { - continue - } +func toCliFlags(options *cmd.OptionsSet) []cli.Flag { + flags := make([]cli.Flag, 0) + for _, o := range options.Sorted() { short := []string{o.Short} if o.Short == "" { short = nil } - //TODO: Handle EnvName?? switch o.Type { case cmd.SelectOption: - - options = append(options, &cli.BoolFlag{ + flags = append(flags, &cli.BoolFlag{ Name: o.Name, Aliases: short, Usage: o.Description, - Value: false, + Value: o.Default.(bool), }) case cmd.BoolOption: - options = append(options, &cli.BoolFlag{ + flags = append(flags, &cli.BoolFlag{ Name: o.Name, Aliases: short, Usage: o.Description, - Value: false, + Value: o.Default.(bool), }) case cmd.StringOption: - options = append(options, &cli.StringFlag{ + flags = append(flags, &cli.StringFlag{ Name: o.Name, Aliases: short, Usage: o.Description, - Value: o.Default, + Value: o.Default.(string), }) } } - return options + return flags } diff --git a/cmd/centry/runtime.go b/cmd/centry/runtime.go index 3716740..346645b 100644 --- a/cmd/centry/runtime.go +++ b/cmd/centry/runtime.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "sort" "strings" "github.com/kristofferahl/go-centry/internal/pkg/config" @@ -12,30 +14,35 @@ import ( // Runtime defines the runtime type Runtime struct { - context *Context cli *cli.App + context *Context + file string args []string + events []string } // NewRuntime builds a runtime based on the given arguments func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { // Create the runtime - runtime := &Runtime{} + runtime := &Runtime{ + cli: nil, + context: context, + file: "", + args: []string{}, + events: []string{}, + } // Args - file := "" - runtime.args = []string{} if len(inputArgs) >= 1 { - file = inputArgs[0] + runtime.file = inputArgs[0] runtime.args = inputArgs[1:] } // Load manifest - manifest, err := config.LoadManifest(file) + manifest, err := config.LoadManifest(runtime.file) if err != nil { return nil, err } - context.manifest = manifest // Create the log manager @@ -43,35 +50,41 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { // Create global options options := createGlobalOptions(context) - flags := createGlobalFlags(context) - - // Parse global options to get cli args - // args, err = options.Parse(args, context.io) - // if err != nil { - // return nil, err - // } // Initialize cli app := &cli.App{ - Name: context.manifest.Config.Name, - HelpName: context.manifest.Config.Name, - Usage: "A tool for building declarative CLI's over bash scripts, written in go.", // TODO: Set from manifest config - UsageText: "", - Version: context.manifest.Config.Version, - HideHelpCommand: true, + Name: context.manifest.Config.Name, + HelpName: context.manifest.Config.Name, + Usage: "A tool for building declarative CLI's over bash scripts, written in go.", // TODO: Set from manifest config + UsageText: "", + Version: context.manifest.Config.Version, Commands: make([]*cli.Command, 0), - Flags: flags, - } + Flags: toCliFlags(options), + + HideHelpCommand: true, + CustomAppHelpTemplate: cliHelpTemplate, + + Writer: context.io.Stdout, + ErrWriter: context.io.Stderr, + + Before: func(c *cli.Context) error { + // Override the current log level from options + logLevel := c.String("config.log.level") + if c.Bool("quiet") { + logLevel = "panic" + } + context.log.TrySetLogLevel(logLevel) - // TODO: Fix log level from options - // Override the current log level from options - // logLevel := options.GetString("config.log.level") - // if options.GetBool("quiet") { - // logLevel = "panic" - // } - //context.log.TrySetLogLevel(logLevel) - context.log.TrySetLogLevel("debug") + // Print runtime events + logger := context.log.GetLogger() + for _, e := range runtime.events { + logger.Debugf(e) + } + + return nil + }, + } logger := context.log.GetLogger() @@ -121,9 +134,6 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { cmd.Help = fn.Help } - cmdKey := strings.Replace(fn.Name, script.FunctionNameSplitChar(), " ", -1) - cmdKeyParts := strings.Split(cmdKey, " ") - scriptCmd := &ScriptCommand{ Context: context, Log: logger.WithFields(logrus.Fields{}), @@ -132,11 +142,11 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { Script: script, Function: fn.Name, } - cliCmd := scriptCmd.ToCLICommand() - var root *cli.Command + cmdKeyParts := scriptCmd.GetCommandInvocationPath() + var root *cli.Command for depth, cmdKeyPart := range cmdKeyParts { if depth == 0 { if getCommand(app.Commands, cmdKeyPart) == nil { @@ -175,12 +185,13 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { } } - logger.Debugf("Registered command \"%s\"", cmdKey) + runtime.events = append(runtime.events, fmt.Sprintf("Registered command \"%s\"", scriptCmd.GetCommandInvocation())) } } } - runtime.context = context + sortCommands(app.Commands) + runtime.cli = app return runtime, nil @@ -219,3 +230,9 @@ func getCommand(commands []*cli.Command, name string) *cli.Command { return nil } + +func sortCommands(commands []*cli.Command) { + sort.Slice(commands, func(i, j int) bool { + return commands[i].Name < commands[j].Name + }) +} diff --git a/cmd/centry/runtime_test.go b/cmd/centry/runtime_test.go index 7a3a60f..5f3a6ca 100644 --- a/cmd/centry/runtime_test.go +++ b/cmd/centry/runtime_test.go @@ -80,8 +80,9 @@ func TestMain(t *testing.T) { g.Describe("invoke with invalid option", func() { g.It("should return error", func() { res := execQuiet("--invalidoption test args foo bar") - g.Assert(res.Stdout).Equal("") - g.Assert(res.Error.Error()).Equal("flag provided but not defined: -invalidoption") + g.Assert(strings.Contains(res.Stdout, "Incorrect Usage. flag provided but not defined: -invalidoption")).IsTrue() + g.Assert(strings.Contains(res.Stderr, "flag provided but not defined: -invalidoption")).IsTrue() + g.Assert(res.Error == nil).IsTrue() }) }) }) @@ -121,8 +122,8 @@ func TestMain(t *testing.T) { result := execQuiet("") g.It("should display help", func() { - expected := `Usage: centry` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue() + expected := `USAGE:` + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() }) }) @@ -130,8 +131,8 @@ func TestMain(t *testing.T) { result := execQuiet("-h") g.It("should display help", func() { - expected := `Usage: centry` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue() + expected := `USAGE:` + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() }) }) @@ -139,8 +140,8 @@ func TestMain(t *testing.T) { result := execQuiet("--help") g.It("should display help", func() { - expected := `Usage: centry` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue() + expected := `USAGE:` + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() }) }) @@ -148,35 +149,27 @@ func TestMain(t *testing.T) { result := execQuiet("") g.It("should display available commands", func() { - expected := `Commands: - delete Deletes stuff - get Gets stuff - post Creates stuff - put Creates/Updates stuff` + expected := `COMMANDS: + delete Deletes stuff + get Gets stuff + post Creates stuff + put Creates/Updates stuff` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stderr) + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) }) g.It("should display global options", func() { - expected := `Global options: - --boolopt, -B A custom option - --config.log.level Overrides the log level - --help, -h Displays help - --production Sets the context to production - --quiet, -q Disables logging - --staging Sets the context to staging - --stringopt, -S A custom option - --version, -v Displays the version of the cli` - - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stderr) - }) - }) - - g.Describe("call without arguments", func() { - result := execQuiet("") - - g.It("should display help text", func() { - g.Assert(strings.Contains(result.Stderr, "Usage: centry")).IsTrue(result.Stderr) + expected := `OPTIONS: + --boolopt, -B A custom option (default: false) + --config.log.level value Overrides the log level (default: "debug") + --production Sets the context to production (default: false) + --quiet, -q Disables logging (default: false) + --staging Sets the context to staging (default: false) + --stringopt value, -S value A custom option (default: "foobar") + --help, -h show help (default: false) + --version, -v print the version (default: false)` + + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) }) }) }) @@ -188,7 +181,7 @@ func TestMain(t *testing.T) { g.It("should display version", func() { expected := `1.0.0` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue() + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() }) }) @@ -197,7 +190,7 @@ func TestMain(t *testing.T) { g.It("should display version", func() { expected := `1.0.0` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue() + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() }) }) }) diff --git a/cmd/centry/script.go b/cmd/centry/script.go index 083514f..3984591 100644 --- a/cmd/centry/script.go +++ b/cmd/centry/script.go @@ -23,16 +23,27 @@ type ScriptCommand struct { Function string } +// GetCommandInvocation returns the command invocation string +func (sc *ScriptCommand) GetCommandInvocation() string { + return strings.Replace(sc.Function, sc.Script.FunctionNameSplitChar(), " ", -1) +} + +// GetCommandInvocationPath returns the command invocation path +func (sc *ScriptCommand) GetCommandInvocationPath() []string { + return strings.Split(sc.GetCommandInvocation(), " ") +} + // ToCLICommand returns a CLI command func (sc *ScriptCommand) ToCLICommand() *cli.Command { - cmdKeys := strings.Split(strings.Replace(sc.Function, sc.Script.FunctionNameSplitChar(), " ", -1), " ") + cmdKeys := sc.GetCommandInvocationPath() + cmdName := cmdKeys[len(cmdKeys)-1] return &cli.Command{ - Name: cmdKeys[len(cmdKeys)-1], - Usage: sc.Synopsis(), - UsageText: sc.Help(), + Name: cmdName, + Usage: sc.Command.Description, + UsageText: sc.Command.Help, HideHelpCommand: true, Action: func(c *cli.Context) error { - ec := sc.Run(c.Args().Slice()) + ec := sc.Run(c, c.Args().Slice()) if ec > 0 { return cli.Exit("command exited with non zero exit code", ec) } @@ -42,13 +53,13 @@ func (sc *ScriptCommand) ToCLICommand() *cli.Command { } // Run builds the source and executes it -func (sc *ScriptCommand) Run(args []string) int { +func (sc *ScriptCommand) Run(c *cli.Context, args []string) int { sc.Log.Debugf("Executing command \"%v\"", sc.Function) var source string switch sc.Script.Language() { case "bash": - source = generateBashSource(sc, args) + source = generateBashSource(c, sc, args) sc.Log.Debugf("Generated bash source:\n%s\n", source) default: sc.Log.Errorf("Unsupported script language %s", sc.Script.Language()) @@ -73,17 +84,7 @@ func (sc *ScriptCommand) Run(args []string) int { return 0 } -// Help returns the help text of the ScriptCommand -func (sc *ScriptCommand) Help() string { - return sc.Command.Help -} - -// Synopsis returns the synopsis of the ScriptCommand -func (sc *ScriptCommand) Synopsis() string { - return sc.Command.Description -} - -func generateBashSource(sc *ScriptCommand, args []string) string { +func generateBashSource(c *cli.Context, sc *ScriptCommand, args []string) string { source := []string{} source = append(source, "#!/usr/bin/env bash") @@ -94,7 +95,7 @@ func generateBashSource(sc *ScriptCommand, args []string) string { source = append(source, "") source = append(source, "# Set exports from flags") - for _, v := range optionsSetToEnvVars(sc.GlobalOptions) { + for _, v := range optionsSetToEnvVars(c, sc.GlobalOptions) { if v.Value != "" { value := v.Value if v.IsString() { diff --git a/cmd/centry/serve.go b/cmd/centry/serve.go index f92e220..5996505 100644 --- a/cmd/centry/serve.go +++ b/cmd/centry/serve.go @@ -23,8 +23,9 @@ type ServeCommand struct { // ToCLICommand returns a CLI command func (sc *ServeCommand) ToCLICommand() *cli.Command { return &cli.Command{ - Name: "serve", - Usage: sc.Synopsis(), + Name: "serve", + Usage: "Exposes commands over HTTP", + UsageText: "", Action: func(c *cli.Context) error { ec := sc.Run(c.Args().Slice()) if ec > 0 { @@ -55,16 +56,6 @@ func (sc *ServeCommand) Run(args []string) int { return 0 } -// Help returns the help text of the ServeCommand -func (sc *ServeCommand) Help() string { - return "No help here..." -} - -// Synopsis returns the synopsis of the ServeCommand -func (sc *ServeCommand) Synopsis() string { - return "Exposes commands over HTTP" -} - func configureBasicAuth() *api.BasicAuth { var auth *api.BasicAuth baUsername := os.Getenv("CENTRY_SERVE_USERNAME") diff --git a/go.mod b/go.mod index 1336cf1..b21a960 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db github.com/ghodss/yaml v1.0.0 github.com/gorilla/mux v1.7.3 - github.com/mitchellh/cli v1.0.0 github.com/santhosh-tekuri/jsonschema/v2 v2.1.0 github.com/sirupsen/logrus v1.4.2 github.com/urfave/cli/v2 v2.2.0 diff --git a/go.sum b/go.sum index ae3f2a8..4076e2c 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,18 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db h1:gb2Z18BhTPJPpLQWj4T+rfKHYCHxRHCtRxhKKjRidVw= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v2 v2.1.0 h1:7KOtBzox6l1PbyZCuQfo923yIBpoMtGCDOD78P9lv9g= @@ -42,10 +24,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU= github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/pkg/cmd/option.go b/internal/pkg/cmd/option.go index 974c15e..8d5ef81 100644 --- a/internal/pkg/cmd/option.go +++ b/internal/pkg/cmd/option.go @@ -1,21 +1,15 @@ package cmd import ( - "bytes" - "flag" "fmt" "sort" "strconv" - - "github.com/kristofferahl/go-centry/internal/pkg/io" ) // OptionsSet represents a set of flags that can be passed to the cli type OptionsSet struct { - Name string - items map[string]*Option - ShortCircuitParseFunc func(arg string) bool - flags *flag.FlagSet + Name string + items map[string]*Option } // OptionType defines the type of an option @@ -98,135 +92,3 @@ func (s *OptionsSet) Sorted() []*Option { return options } - -// AsFlagSet returns the set of options as a FlagSet -func (s *OptionsSet) AsFlagSet() *flag.FlagSet { - fs := flag.NewFlagSet(s.Name, flag.ContinueOnError) - - for _, o := range s.Sorted() { - o := o - switch o.Type { - case StringOption: - val := o.Default - if val == nil { - val = "" - } - newStringFlag(fs, o, val.(string)) - case BoolOption: - newBoolFlag(fs, o, o.Default.(bool)) - case SelectOption: - newBoolFlag(fs, o, o.Default.(bool)) - default: - // TODO: Handle unsupported type - } - } - - return fs -} - -func newStringFlag(fs *flag.FlagSet, o *Option, def string) { - f := fs.String(o.Name, def, o.Description) - if o.Short != "" { - fs.StringVar(f, o.Short, def, o.Description) - } - o.value = (*stringValue)(f) -} - -func newBoolFlag(fs *flag.FlagSet, o *Option, def bool) { - f := fs.Bool(o.Name, def, o.Description) - if o.Short != "" { - fs.BoolVar(f, o.Short, def, o.Description) - } - o.value = (*boolValue)(f) -} - -// Parse pareses the args using a flagset and returns the remaining arguments -func (s *OptionsSet) Parse(args []string, io io.InputOutput) ([]string, error) { - parse := true - - if s.ShortCircuitParseFunc != nil { - for _, arg := range args { - if s.ShortCircuitParseFunc(arg) { - parse = false - break - } - } - } - - if parse { - s.flags = s.AsFlagSet() - s.flags.SetOutput(bytes.NewBufferString("")) - - err := s.flags.Parse(args) - if err != nil { - return nil, err - } - - soc := make(map[string][]string) - - for _, o := range s.Sorted() { - if o.Type == SelectOption && s.GetBool(o.Name) != o.Default.(bool) { - key := o.EnvName - if key == "" { - key = o.Name - } - - if _, ok := soc[key]; !ok { - soc[key] = make([]string, 0) - } - - soc[key] = append(soc[key], o.Name) - - if len(soc[key]) > 1 { - return nil, fmt.Errorf("ambiguous flag usage %v", soc[key]) - } - } - } - - args = s.flags.Args() - } - - return args, nil -} - -// GetValueString returns the value of a given option -func (s *OptionsSet) GetValueString(key string) string { - option := s.items[key] - result := "" - - if option.value != nil { - result = option.value.string() - } - - return result -} - -// GetBool returns the parsed value of a given option -func (s *OptionsSet) GetBool(key string) bool { - option := s.items[key] - result := false - - if option.value != nil { - switch v := s.items[key].value.(type) { - case *boolValue: - result = bool(*v) - } - } - - return result -} - -// GetString returns the parsed value of a given option -func (s *OptionsSet) GetString(key string) string { - option := s.items[key] - result := "" - - if option.value != nil { - switch v := s.items[key].value.(type) { - case *stringValue: - result = string(*v) - } - } - - return result -} diff --git a/internal/pkg/cmd/option_test.go b/internal/pkg/cmd/option_test.go index 77e320c..c6e4bde 100644 --- a/internal/pkg/cmd/option_test.go +++ b/internal/pkg/cmd/option_test.go @@ -1,11 +1,9 @@ package cmd import ( - "flag" "testing" . "github.com/franela/goblin" - "github.com/kristofferahl/go-centry/internal/pkg/io" ) func TestMain(t *testing.T) { @@ -52,374 +50,5 @@ func TestMain(t *testing.T) { g.Assert(err2.Error()).Equal("an option with the name \"Option\" has already been added") }) }) - - g.Describe("AsFlagSet", func() { - g.Describe("with no options", func() { - os := NewOptionsSet("Name") - fs := os.AsFlagSet() - - g.It("should have error handling turned off", func() { - g.Assert(fs.ErrorHandling()).Equal(flag.ContinueOnError) - }) - - g.It("should have no flags", func() { - c := 0 - fs.VisitAll(func(flag *flag.Flag) { - c++ - }) - g.Assert(c).Equal(0) - }) - }) - - g.Describe("with options", func() { - os := NewOptionsSet("Name") - os.Add(&Option{Type: StringOption, Name: "Option"}) - fs := os.AsFlagSet() - - g.It("should have error handling turned off", func() { - g.Assert(fs.ErrorHandling()).Equal(flag.ContinueOnError) - }) - - g.It("should have flags", func() { - c := 0 - var f *flag.Flag - fs.VisitAll(func(flag *flag.Flag) { - c++ - f = flag - }) - g.Assert(c).Equal(1) - g.Assert(f.Name).Equal("Option") - }) - }) - }) - - g.Describe("Parse", func() { - g.Describe("with no options", func() { - os := NewOptionsSet("Name") - - g.Describe("passing nil as args", func() { - g.It("should return 0 args", func() { - rest, err := os.Parse(nil, io.Headless()) - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - }) - - g.Describe("passing 0 args", func() { - g.It("should return 0 args", func() { - rest, err := os.Parse([]string{}, io.Headless()) - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - }) - - g.Describe("passing args", func() { - g.It("should return all args", func() { - rest, err := os.Parse([]string{"a1", "a2", "a3"}, io.Headless()) - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(3) - }) - }) - }) - - g.Describe("boolean options", func() { - withBooleanOption := func(defaultValue bool) *OptionsSet { - os := NewOptionsSet("Name") - os.Add(&Option{ - Type: BoolOption, - Name: "Option", - Default: defaultValue, - }) - return os - } - - g.Describe("with default value false", func() { - g.Describe("passing nil as args", func() { - os := withBooleanOption(false) - rest, err := os.Parse(nil, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should not have value for otpions", func() { - g.Assert(os.GetBool("Option")).Equal(false) - }) - }) - - g.Describe("passing flag with value", func() { - os := withBooleanOption(false) - rest, err := os.Parse([]string{"--Option=true", "arg"}, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(1) - g.Assert(rest[0]).Equal("arg") - }) - - g.It("should not have value for option", func() { - g.Assert(os.GetBool("Option")).Equal(true) - }) - }) - - g.Describe("passing flag without value", func() { - os := withBooleanOption(false) - rest, err := os.Parse([]string{"--Option", "arg"}, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(1) - g.Assert(rest[0]).Equal("arg") - }) - - g.It("should not have value for option", func() { - g.Assert(os.GetBool("Option")).Equal(true) - }) - }) - }) - - g.Describe("with default value true", func() { - g.Describe("passing nil as args", func() { - os := withBooleanOption(true) - rest, err := os.Parse(nil, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should not have value for otpions", func() { - g.Assert(os.GetBool("Option")).Equal(true) - }) - }) - - g.Describe("passing flag with value", func() { - os := withBooleanOption(true) - rest, err := os.Parse([]string{"--Option=false", "arg"}, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(1) - g.Assert(rest[0]).Equal("arg") - }) - - g.It("should not have value for option", func() { - g.Assert(os.GetBool("Option")).Equal(false) - }) - }) - - g.Describe("passing flag without value", func() { - os := withBooleanOption(true) - rest, err := os.Parse([]string{"--Option", "arg"}, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(1) - g.Assert(rest[0]).Equal("arg") - }) - - g.It("should not have value for option", func() { - g.Assert(os.GetBool("Option")).Equal(true) - }) - }) - }) - }) - - g.Describe("string options", func() { - withStringOption := func(defaultValue string) *OptionsSet { - os := NewOptionsSet("Name") - os.Add(&Option{ - Type: StringOption, - Name: "Option", - Default: defaultValue, - }) - return os - } - - g.Describe("without default value", func() { - g.Describe("passing nil as args", func() { - os := withStringOption("") - rest, err := os.Parse(nil, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should not have value for otpions", func() { - g.Assert(os.GetString("Option")).Equal("") - }) - }) - - g.Describe("passing flag with value", func() { - os := withStringOption("") - rest, err := os.Parse([]string{"--Option", "value"}, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should not have value for option", func() { - g.Assert(os.GetString("Option")).Equal("value") - }) - }) - }) - - g.Describe("string option with default value", func() { - g.Describe("passing nil", func() { - os := withStringOption("DefaultValue") - rest, err := os.Parse(nil, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should not have value for otpions", func() { - g.Assert(os.GetString("Option")).Equal("DefaultValue") - }) - }) - - g.Describe("passing flag with value", func() { - os := withStringOption("DefaultValue") - rest, err := os.Parse([]string{"--Option", "value"}, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should override default value", func() { - g.Assert(os.GetString("Option")).Equal("value") - }) - }) - }) - }) - - g.Describe("select options", func() { - withSelectOptions := func(defaultValue bool) *OptionsSet { - os := NewOptionsSet("Name") - os.Add(&Option{ - Type: SelectOption, - Name: "Option1", - EnvName: "OneOf", - Default: defaultValue, - }) - os.Add(&Option{ - Type: SelectOption, - Name: "Option2", - EnvName: "OneOf", - Default: defaultValue, - }) - os.Add(&Option{ - Type: SelectOption, - Name: "Option3", - EnvName: "OneOf", - Default: defaultValue, - }) - return os - } - - g.Describe("passing nil", func() { - os := withSelectOptions(false) - rest, err := os.Parse(nil, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should not have default value for otpions", func() { - g.Assert(os.GetBool("Option1")).Equal(false) - g.Assert(os.GetBool("Option2")).Equal(false) - }) - }) - - g.Describe("passing flag should", func() { - g.Describe("with no value", func() { - os := withSelectOptions(false) - rest, err := os.Parse([]string{"--Option1"}, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should set value", func() { - g.Assert(os.GetBool("Option1")).Equal(true) - g.Assert(os.GetBool("Option2")).Equal(false) - }) - }) - - g.Describe("with value false", func() { - os := withSelectOptions(false) - rest, err := os.Parse([]string{"--Option1=false"}, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should set value", func() { - g.Assert(os.GetBool("Option1")).Equal(false) - g.Assert(os.GetBool("Option2")).Equal(false) - }) - }) - - g.Describe("with value true", func() { - os := withSelectOptions(false) - rest, err := os.Parse([]string{"--Option1=true"}, io.Headless()) - - g.It("should return 0 args and no error", func() { - g.Assert(err).Equal(nil) - g.Assert(len(rest)).Equal(0) - }) - - g.It("should set value", func() { - g.Assert(os.GetBool("Option1")).Equal(true) - g.Assert(os.GetBool("Option2")).Equal(false) - }) - }) - }) - - g.Describe("passing multiple flags", func() { - g.Describe("when default is false", func() { - os := withSelectOptions(false) - rest, err := os.Parse([]string{"--Option3=false", "--Option2=true", "--Option1"}, io.Headless()) - - g.It("should return 0 args and error", func() { - g.Assert(err != nil).IsTrue("expected error") - g.Assert(err.Error()).Equal("ambiguous flag usage [Option1 Option2]") - g.Assert(len(rest)).Equal(0) - }) - - g.It("should set value", func() { - g.Assert(os.GetBool("Option1")).Equal(true) - g.Assert(os.GetBool("Option2")).Equal(true) - g.Assert(os.GetBool("Option3")).Equal(false) - }) - }) - - g.Describe("when default is true", func() { - os := withSelectOptions(true) - rest, err := os.Parse([]string{"--Option3=false", "--Option2", "--Option1=false"}, io.Headless()) - - g.It("should return 0 args and error", func() { - g.Assert(err != nil).IsTrue("expected error") - g.Assert(err.Error()).Equal("ambiguous flag usage [Option1 Option3]") - g.Assert(len(rest)).Equal(0) - }) - - g.It("should set value", func() { - g.Assert(os.GetBool("Option1")).Equal(false) - g.Assert(os.GetBool("Option2")).Equal(true) - g.Assert(os.GetBool("Option3")).Equal(false) - }) - }) - }) - }) - }) }) } diff --git a/internal/pkg/config/manifest.go b/internal/pkg/config/manifest.go index f540aa5..177a28a 100644 --- a/internal/pkg/config/manifest.go +++ b/internal/pkg/config/manifest.go @@ -26,8 +26,8 @@ type Manifest struct { type Command struct { Name string `yaml:"name,omitempty"` Path string `yaml:"path,omitempty"` - Help string `yaml:"help,omitempty"` // NOTE: The Help text is displayed only when the command matches the full path of a command Description string `yaml:"description,omitempty"` + Help string `yaml:"help,omitempty"` Annotations map[string]string `yaml:"annotations,omitempty"` } From fe086ea9d10dfa9bf0011bdd7bce036d2c59ae65 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Mon, 30 Mar 2020 15:40:48 +0200 Subject: [PATCH 03/19] - Fixed issue with souring of invalid scripts. --- internal/pkg/shell/bash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/shell/bash.go b/internal/pkg/shell/bash.go index 13580db..f5cbd19 100644 --- a/internal/pkg/shell/bash.go +++ b/internal/pkg/shell/bash.go @@ -63,7 +63,7 @@ func (s *BashScript) FullPath() string { // FunctionNames returns functions in declared in the script func (s *BashScript) FunctionNames() ([]string, error) { - callArgs := []string{"-c", fmt.Sprintf("source %s; declare -F", s.FullPath())} + callArgs := []string{"-c", fmt.Sprintf("set -e; source %s; declare -F", s.FullPath())} io, buf := io.BufferedCombined() From bef9dc5ede85025966f44d65d799e19fa3903be8 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Sat, 4 Apr 2020 13:12:37 +0200 Subject: [PATCH 04/19] - Adding more tests, running setup just once. --- internal/pkg/config/manifest_test.go | 5 +---- internal/pkg/config/setup_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 internal/pkg/config/setup_test.go diff --git a/internal/pkg/config/manifest_test.go b/internal/pkg/config/manifest_test.go index a5204b0..bc7940b 100644 --- a/internal/pkg/config/manifest_test.go +++ b/internal/pkg/config/manifest_test.go @@ -10,12 +10,9 @@ import ( . "github.com/franela/goblin" ) -func TestMain(t *testing.T) { +func TestManifest(t *testing.T) { g := Goblin(t) - // Esuring the workdir is the root of the repo - os.Chdir("../../../") - g.Describe("LoadManifest", func() { g.It("returns error for invalid manifest file", func() { _, err := LoadManifest("test/data/invalid.yaml") diff --git a/internal/pkg/config/setup_test.go b/internal/pkg/config/setup_test.go new file mode 100644 index 0000000..1bdd37e --- /dev/null +++ b/internal/pkg/config/setup_test.go @@ -0,0 +1,10 @@ +package config + +import ( + "os" +) + +func init() { + // Esuring the workdir is the root of the repo + os.Chdir("../../../") +} From cb8042c50bf39619d7340cea5db5dd01bed2c5b4 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Sat, 4 Apr 2020 13:13:55 +0200 Subject: [PATCH 05/19] - Added a Validate func to Option. --- internal/pkg/cmd/option.go | 20 ++++++++++++++++---- internal/pkg/cmd/option_test.go | 13 ++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/internal/pkg/cmd/option.go b/internal/pkg/cmd/option.go index 8d5ef81..70e7ee0 100644 --- a/internal/pkg/cmd/option.go +++ b/internal/pkg/cmd/option.go @@ -35,6 +35,19 @@ type Option struct { value valuePointer } +// Validate returns true if the option is concidered valid +func (o *Option) Validate() error { + if o.Name == "" { + return fmt.Errorf("missing option name") + } + + if o.Type == "" { + return fmt.Errorf("missing option type") + } + + return nil +} + type boolValue bool func (b *boolValue) string() string { return strconv.FormatBool(bool(*b)) } @@ -61,16 +74,15 @@ func (s *OptionsSet) Add(option *Option) error { return fmt.Errorf("an option is required") } - if option.Name == "" { - return fmt.Errorf("missing option name") + err := option.Validate() + if err != nil { + return err } if _, ok := s.items[option.Name]; ok { return fmt.Errorf("an option with the name \"%s\" has already been added", option.Name) } - // TODO: Validate option type - s.items[option.Name] = option return nil diff --git a/internal/pkg/cmd/option_test.go b/internal/pkg/cmd/option_test.go index c6e4bde..2732e7d 100644 --- a/internal/pkg/cmd/option_test.go +++ b/internal/pkg/cmd/option_test.go @@ -21,7 +21,7 @@ func TestMain(t *testing.T) { g.Describe("Add", func() { g.It("should add option", func() { os := NewOptionsSet("Name") - os.Add(&Option{Name: "Option"}) + os.Add(&Option{Name: "Option", Type: StringOption}) g.Assert(len(os.Sorted())).Equal(1) }) @@ -40,10 +40,17 @@ func TestMain(t *testing.T) { g.Assert(err.Error()).Equal("missing option name") }) + g.It("should return error when option type is unset", func() { + os := NewOptionsSet("Name") + err := os.Add(&Option{Name: "foo"}) + g.Assert(len(os.Sorted())).Equal(0) + g.Assert(err.Error()).Equal("missing option type") + }) + g.It("should return error when option name already exists", func() { os := NewOptionsSet("Name") - err1 := os.Add(&Option{Name: "Option"}) - err2 := os.Add(&Option{Name: "Option"}) + err1 := os.Add(&Option{Name: "Option", Type: StringOption}) + err2 := os.Add(&Option{Name: "Option", Type: StringOption}) g.Assert(len(os.Sorted())).Equal(1) g.Assert(err1).Equal(nil) g.Assert(err2 != nil).IsTrue("expected an error") From 291a2adbac22486732d54e9aa80097f0c260e1e2 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Sat, 4 Apr 2020 13:16:19 +0200 Subject: [PATCH 06/19] - Fixed spelling mistake. --- internal/pkg/shell/bash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/shell/bash.go b/internal/pkg/shell/bash.go index f5cbd19..21b0fe5 100644 --- a/internal/pkg/shell/bash.go +++ b/internal/pkg/shell/bash.go @@ -85,7 +85,7 @@ func (s *BashScript) FunctionNames() ([]string, error) { return functions, nil } -// FunctionAnnotations returns function annotations in declared in the script +// FunctionAnnotations returns function annotations declared in the script file func (s *BashScript) FunctionAnnotations() ([]*config.Annotation, error) { annotations := make([]*config.Annotation, 0) From 0b3f59c476f0a3de252d27757e44df013f1b3cd9 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Sat, 4 Apr 2020 13:19:05 +0200 Subject: [PATCH 07/19] - Fixing issue with nil default values. --- cmd/centry/options.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/cmd/centry/options.go b/cmd/centry/options.go index a90fff9..29b5250 100644 --- a/cmd/centry/options.go +++ b/cmd/centry/options.go @@ -73,25 +73,37 @@ func toCliFlags(options *cmd.OptionsSet) []cli.Flag { switch o.Type { case cmd.SelectOption: + def := false + if o.Default != nil { + def = o.Default.(bool) + } flags = append(flags, &cli.BoolFlag{ Name: o.Name, Aliases: short, Usage: o.Description, - Value: o.Default.(bool), + Value: def, }) case cmd.BoolOption: + def := false + if o.Default != nil { + def = o.Default.(bool) + } flags = append(flags, &cli.BoolFlag{ Name: o.Name, Aliases: short, Usage: o.Description, - Value: o.Default.(bool), + Value: def, }) case cmd.StringOption: + def := "" + if o.Default != nil { + def = o.Default.(string) + } flags = append(flags, &cli.StringFlag{ Name: o.Name, Aliases: short, Usage: o.Description, - Value: o.Default.(string), + Value: def, }) } } From 7964ce2fb7892f8a156e007264036419e580e5c9 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Sat, 4 Apr 2020 13:20:47 +0200 Subject: [PATCH 08/19] - Passing Function to script command. --- cmd/centry/runtime.go | 2 +- cmd/centry/script.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/centry/runtime.go b/cmd/centry/runtime.go index 346645b..5b29a2b 100644 --- a/cmd/centry/runtime.go +++ b/cmd/centry/runtime.go @@ -140,7 +140,7 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { GlobalOptions: options, Command: cmd, Script: script, - Function: fn.Name, + Function: *fn, } cliCmd := scriptCmd.ToCLICommand() diff --git a/cmd/centry/script.go b/cmd/centry/script.go index 3984591..0a14ec5 100644 --- a/cmd/centry/script.go +++ b/cmd/centry/script.go @@ -20,12 +20,12 @@ type ScriptCommand struct { Command config.Command GlobalOptions *cmd.OptionsSet Script shell.Script - Function string + Function shell.Function } // GetCommandInvocation returns the command invocation string func (sc *ScriptCommand) GetCommandInvocation() string { - return strings.Replace(sc.Function, sc.Script.FunctionNameSplitChar(), " ", -1) + return strings.Replace(sc.Function.Name, sc.Script.FunctionNameSplitChar(), " ", -1) } // GetCommandInvocationPath returns the command invocation path @@ -54,7 +54,7 @@ func (sc *ScriptCommand) ToCLICommand() *cli.Command { // Run builds the source and executes it func (sc *ScriptCommand) Run(c *cli.Context, args []string) int { - sc.Log.Debugf("Executing command \"%v\"", sc.Function) + sc.Log.Debugf("Executing command \"%v\"", sc.Function.Name) var source string switch sc.Script.Language() { @@ -76,11 +76,11 @@ func (sc *ScriptCommand) Run(c *cli.Context, args []string) int { } } - sc.Log.Errorf("Command %v exited with error! %v", sc.Function, err) + sc.Log.Errorf("Command %v exited with error! %v", sc.Function.Name, err) return exitCode } - sc.Log.Debugf("Finished executing command %v...", sc.Function) + sc.Log.Debugf("Finished executing command %v...", sc.Function.Name) return 0 } @@ -117,7 +117,7 @@ func generateBashSource(c *cli.Context, sc *ScriptCommand, args []string) string source = append(source, "") source = append(source, "# Executing command") - source = append(source, fmt.Sprintf("%s %s", sc.Function, strings.Join(args, " "))) + source = append(source, fmt.Sprintf("%s %s", sc.Function.Name, strings.Join(args, " "))) return strings.Join(source, "\n") } From 552dee461672e56eea71be3d5bd750e733c611dd Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Sat, 4 Apr 2020 13:26:09 +0200 Subject: [PATCH 09/19] - Removed comments from readme. --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 8aec0a8..88d9f4d 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,3 @@ scripts/test ```bash scripts/run-centry ``` - - - - -https://github.com/kristofferahl/go-centry -https://github.com/urfave/cli/blob/master/docs/v2/manual.md#subcommands -https://github.com/spf13/cobra From 0ac6648445b482843fc541368a845e316be2f37d Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Sat, 4 Apr 2020 14:45:07 +0200 Subject: [PATCH 10/19] - [BREAKING] Changed format for command annotations in scripts. - Basic implementation for command options using script annotations. - Added tests for parsing annotations. --- cmd/centry/options.go | 2 +- cmd/centry/runtime.go | 2 +- cmd/centry/runtime_test.go | 15 ++++ cmd/centry/script.go | 16 +++- examples/centry/commands/get.sh | 22 +++-- examples/centry/commands/rotate.sh | 6 +- internal/pkg/cmd/option.go | 16 ++++ internal/pkg/cmd/option_test.go | 40 +++++++++ internal/pkg/config/annotations.go | 73 ++++++++++------ internal/pkg/config/annotations_test.go | 106 ++++++++++++++++++++++++ internal/pkg/shell/bash.go | 66 +++++++++++++-- internal/pkg/shell/types.go | 2 + test/data/commands/test.sh | 12 +++ 13 files changed, 336 insertions(+), 42 deletions(-) create mode 100644 internal/pkg/config/annotations_test.go diff --git a/cmd/centry/options.go b/cmd/centry/options.go index 29b5250..d7ec241 100644 --- a/cmd/centry/options.go +++ b/cmd/centry/options.go @@ -62,7 +62,7 @@ func createGlobalOptions(context *Context) *cmd.OptionsSet { return options } -func toCliFlags(options *cmd.OptionsSet) []cli.Flag { +func optionsSetToFlags(options *cmd.OptionsSet) []cli.Flag { flags := make([]cli.Flag, 0) for _, o := range options.Sorted() { diff --git a/cmd/centry/runtime.go b/cmd/centry/runtime.go index 5b29a2b..3bbca80 100644 --- a/cmd/centry/runtime.go +++ b/cmd/centry/runtime.go @@ -60,7 +60,7 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { Version: context.manifest.Config.Version, Commands: make([]*cli.Command, 0), - Flags: toCliFlags(options), + Flags: optionsSetToFlags(options), HideHelpCommand: true, CustomAppHelpTemplate: cliHelpTemplate, diff --git a/cmd/centry/runtime_test.go b/cmd/centry/runtime_test.go index 5f3a6ca..d3ec228 100644 --- a/cmd/centry/runtime_test.go +++ b/cmd/centry/runtime_test.go @@ -115,6 +115,21 @@ func TestMain(t *testing.T) { }) }) }) + + g.Describe("command options", func() { + g.Describe("invoking command with options", func() { + g.It("should have arguments passed", func() { + g.Assert(execQuiet("test args --cmdstringopt=hello --cmdboolopt --cmdsel1 --cmdsel2 foo bar baz").Stdout).Equal("test:args (foo bar baz)\n") + }) + + g.It("should have multipe environment variables set", func() { + out := execQuiet("test env --cmdstringopt=world --cmdboolopt --cmdsel1 --cmdsel2").Stdout + test.AssertKeyValueExists(g, "CMDSTRINGOPT", "world", out) + test.AssertKeyValueExists(g, "CMDBOOLOPT", "true", out) + test.AssertKeyValueExists(g, "CMDSELECTOPT", "cmdsel2", out) + }) + }) + }) }) g.Describe("help", func() { diff --git a/cmd/centry/script.go b/cmd/centry/script.go index 0a14ec5..6564edc 100644 --- a/cmd/centry/script.go +++ b/cmd/centry/script.go @@ -49,6 +49,7 @@ func (sc *ScriptCommand) ToCLICommand() *cli.Command { } return nil }, + Flags: optionsSetToFlags(sc.Function.Options), } } @@ -93,7 +94,7 @@ func generateBashSource(c *cli.Context, sc *ScriptCommand, args []string) string source = append(source, fmt.Sprintf("cd %s || exit 1", sc.Context.manifest.BasePath)) source = append(source, "") - source = append(source, "# Set exports from flags") + source = append(source, "# Set exports from global options") for _, v := range optionsSetToEnvVars(c, sc.GlobalOptions) { if v.Value != "" { @@ -105,6 +106,19 @@ func generateBashSource(c *cli.Context, sc *ScriptCommand, args []string) string } } + source = append(source, "") + source = append(source, "# Set exports from local options") + + for _, v := range optionsSetToEnvVars(c, sc.Function.Options) { + if v.Value != "" { + value := v.Value + if v.IsString() { + value = fmt.Sprintf("'%s'", v.Value) + } + source = append(source, fmt.Sprintf("export %s=%s", v.Name, value)) + } + } + source = append(source, "") source = append(source, "# Sourcing scripts") for _, s := range sc.Context.manifest.Scripts { diff --git a/examples/centry/commands/get.sh b/examples/centry/commands/get.sh index 43683d4..5b41883 100644 --- a/examples/centry/commands/get.sh +++ b/examples/centry/commands/get.sh @@ -1,13 +1,25 @@ #!/usr/bin/env bash -# centry.cmd.description/get:env=Prints environment variables -# centry.cmd.help/get:env=Prints environment variables. Usage: ./stack get env [<...options>] +# centry.cmd[get:env]/description=Prints environment variables +# centry.cmd[get:env]/help=Prints environment variables. Usage: ./stack get env [<...options>] +# centry.cmd[get:env].option[filter]/short=f +# centry.cmd[get:env].option[filter]/description=Filters environment variables based on the provided value +# centry.cmd[get:env].option[sanitize]/type=bool +# centry.cmd[get:env].option[sanitize]/description=Clean output so that no secrets are leaked +# centry.cmd[get:env].option[sanitize]/envName=SANITIZE_OUTPUT get:env() { - env | ${SORTED} + local output + output="$(env | ${SORTED} | grep "${FILTER}")" + + if [[ ${SANITIZE_OUTPUT} ]]; then + echo "${output}" | sed 's/\=.*$/=***/' + else + echo "${output}" + fi } -# centry.cmd.description/get:files=Prints files from the current working directory -# centry.cmd.help/get:files=Prints files from the current working directory. Usage: ./stack get files [<...options>] +# centry.cmd[get:files]/description=Prints files from the current working directory +# centry.cmd[get:files]/help=Prints files from the current working directory. Usage: ./stack get files [<...options>] get:files() { ls -ahl | ${SORTED} } diff --git a/examples/centry/commands/rotate.sh b/examples/centry/commands/rotate.sh index 0f5a09c..4c58e16 100644 --- a/examples/centry/commands/rotate.sh +++ b/examples/centry/commands/rotate.sh @@ -1,16 +1,16 @@ #!/usr/bin/env bash -# centry.cmd.description/rotate:secrets=Rotate secrets +# centry.cmd[rotate:secrets]/description=Rotate secrets rotate:secrets() { echo "rotate:secrets ($*)" } -# centry.cmd.description/rotate:kubernetes:workers=Rotate kubernetes worker nodes +# centry.cmd[rotate:kubernetes:workers]/description=Rotate kubernetes worker nodes rotate:kubernetes:workers() { echo "rotate:kubernetes:workers ($*)" } -# centry.cmd.description/rotate:kubernetes:masters=Rotate kubernetes master nodes +# centry.cmd[rotate:kubernetes:masters]/description=Rotate kubernetes master nodes rotate:kubernetes:masters() { echo "rotate:kubernetes;masters ($*)" } diff --git a/internal/pkg/cmd/option.go b/internal/pkg/cmd/option.go index 70e7ee0..22dc56a 100644 --- a/internal/pkg/cmd/option.go +++ b/internal/pkg/cmd/option.go @@ -4,6 +4,7 @@ import ( "fmt" "sort" "strconv" + "strings" ) // OptionsSet represents a set of flags that can be passed to the cli @@ -24,6 +25,21 @@ const BoolOption OptionType = "bool" // SelectOption defines a boolean select value option const SelectOption OptionType = "select" +// StringToOptionType returns the OptionType matching the provided string +func StringToOptionType(s string) OptionType { + s = strings.ToLower(s) + switch s { + case "string": + return StringOption + case "bool": + return BoolOption + case "select": + return SelectOption + default: + return StringOption + } +} + // Option represents a flag that can be passed to the cli type Option struct { Type OptionType diff --git a/internal/pkg/cmd/option_test.go b/internal/pkg/cmd/option_test.go index 2732e7d..d61995d 100644 --- a/internal/pkg/cmd/option_test.go +++ b/internal/pkg/cmd/option_test.go @@ -1,11 +1,27 @@ package cmd import ( + "math/rand" "testing" + "time" . "github.com/franela/goblin" ) +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func randomString(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} + func TestMain(t *testing.T) { g := Goblin(t) @@ -58,4 +74,28 @@ func TestMain(t *testing.T) { }) }) }) + + g.Describe("StringToOptionType", func() { + g.It("should default to StringOption", func() { + g.Assert(StringToOptionType(randomString(10))).Equal(StringOption) + }) + + g.It("should return StringOption", func() { + g.Assert(StringToOptionType("string")).Equal(StringOption) + g.Assert(StringToOptionType("String")).Equal(StringOption) + g.Assert(StringToOptionType("STRING")).Equal(StringOption) + }) + + g.It("should return BoolOption", func() { + g.Assert(StringToOptionType("bool")).Equal(BoolOption) + g.Assert(StringToOptionType("Bool")).Equal(BoolOption) + g.Assert(StringToOptionType("BOOL")).Equal(BoolOption) + }) + + g.It("should return SelectOption", func() { + g.Assert(StringToOptionType("select")).Equal(SelectOption) + g.Assert(StringToOptionType("Select")).Equal(SelectOption) + g.Assert(StringToOptionType("SELECT")).Equal(SelectOption) + }) + }) } diff --git a/internal/pkg/config/annotations.go b/internal/pkg/config/annotations.go index 75374fb..eb8b6c9 100644 --- a/internal/pkg/config/annotations.go +++ b/internal/pkg/config/annotations.go @@ -2,58 +2,79 @@ package config import ( "fmt" + "regexp" "strings" ) // TrueString defines a true value const TrueString string = "true" -// CommandAnnotationAPIServe defines an annotation -const CommandAnnotationAPIServe string = "centry.api/serve" +// CommandAnnotationCmdNamespace defines an annotation namespace +const CommandAnnotationCmdNamespace string = "centry.cmd" -// CommandAnnotationDescriptionNamespace denines an annotation namespace -const CommandAnnotationDescriptionNamespace string = "centry.cmd.description" +// CommandAnnotationCmdOptionNamespace defines an annotation namespace +const CommandAnnotationCmdOptionNamespace string = "centry.cmd.option" -// CommandAnnotationHelpNamespace denines an annotation namespace -const CommandAnnotationHelpNamespace string = "centry.cmd.help" +// CommandAnnotationAPIServe defines an annotation +const CommandAnnotationAPIServe string = "centry.api/serve" -// CommandAnnotationNamespaces holds a list of command annotation namespaces -var CommandAnnotationNamespaces = []string{ - CommandAnnotationDescriptionNamespace, - CommandAnnotationHelpNamespace, -} +// // CommandAnnotationDescription defines an annotation namespace +// const CommandAnnotationDescription string = "centry.cmd/description" +// +// // CommandAnnotationHelp defines an annotation namespace +// const CommandAnnotationHelp string = "centry.cmd/help" +// +// // CommandAnnotationOptionType defines an annotation namespace +// const CommandAnnotationOptionType string = "centry.cmd.option/type" // Annotation defines an annotation type Annotation struct { - Namespace string - Key string - Value string + Namespace string + NamespaceValues map[string]string + Key string + Value string } // ParseAnnotation parses text into an annotation func ParseAnnotation(text string) (*Annotation, error) { text = strings.TrimSpace(text) - if !strings.HasPrefix(text, "centry") || !strings.Contains(text, "=") || !strings.Contains(text, "/") { + if !strings.HasPrefix(text, "centry") || !strings.Contains(text, "/") || !strings.Contains(text, "=") { return nil, nil } - kvp := strings.Split(text, "=") + namespaceKeyValueString := strings.SplitN(text, "/", 2) + namespace := namespaceKeyValueString[0] + keyValueString := namespaceKeyValueString[1] + + kvp := strings.SplitN(keyValueString, "=", 2) if len(kvp) != 2 { return nil, fmt.Errorf("Failed to parse annotation! The text \"%s\" is not a valid annotation", text) } - nk := kvp[0] - v := kvp[1] + return &Annotation{ + Namespace: cleanupNamespace(namespace), + NamespaceValues: extractNamespaceValues(namespace), + Key: kvp[0], + Value: kvp[1], + }, nil +} - nkkvp := strings.Split(nk, "/") - if len(nkkvp) != 2 { - return nil, fmt.Errorf("Failed to parse annotation! The text \"%s\" is not a valid annotation", text) +func extractNamespaceValues(namespace string) (params map[string]string) { + var compRegEx = regexp.MustCompile("\\.(\\w+)\\[([0-9A-Za-z_:]+)\\]") + match := compRegEx.FindAllStringSubmatch(namespace, -1) + + params = make(map[string]string) + for _, kv := range match { + k := kv[1] + v := kv[2] + params[k] = v } - return &Annotation{ - Namespace: nkkvp[0], - Key: nkkvp[1], - Value: v, - }, nil + return +} + +func cleanupNamespace(namespace string) string { + var compRegEx = regexp.MustCompile("(\\.*)(\\[([0-9A-Za-z_:]+)\\])") + return compRegEx.ReplaceAllString(namespace, `${1}`) } diff --git a/internal/pkg/config/annotations_test.go b/internal/pkg/config/annotations_test.go new file mode 100644 index 0000000..c760ab1 --- /dev/null +++ b/internal/pkg/config/annotations_test.go @@ -0,0 +1,106 @@ +package config + +import ( + "testing" + + . "github.com/franela/goblin" +) + +func TestAnnotations(t *testing.T) { + g := Goblin(t) + + g.Describe("ParseAnnotation", func() { + g.Describe("InvalidAnnotations", func() { + g.It("returns nil when line is not an annotation", func() { + annotation, err := ParseAnnotation("foo bar baz") + g.Assert(annotation == nil).IsTrue("expected no annotation") + g.Assert(err == nil).IsTrue("expected no error") + }) + + g.It("returns nil when annotation is missing centry prefix", func() { + annotation, err := ParseAnnotation("foo/bar=baz") + g.Assert(annotation == nil).IsTrue("expected no annotation") + g.Assert(err == nil).IsTrue("expected no error") + }) + + g.It("returns nil when annotation is missing slash", func() { + annotation, err := ParseAnnotation("centry_bar=baz") + g.Assert(annotation == nil).IsTrue("expected no annotation") + g.Assert(err == nil).IsTrue("expected no error") + }) + + g.It("returns nil when annotation is missing equals sign", func() { + annotation, err := ParseAnnotation("centry/bar_baz") + g.Assert(annotation == nil).IsTrue("expected no annotation") + g.Assert(err == nil).IsTrue("expected no error") + }) + + g.It("returns error when annotation only has equals sign before slash", func() { + annotation, err := ParseAnnotation("centry=bar/bar") + g.Assert(annotation == nil).IsTrue("expected no annotation") + g.Assert(err != nil).IsTrue("expected no error") + }) + }) + + g.Describe("ValidAnnotations", func() { + g.It("returns annotation", func() { + annotation, err := ParseAnnotation("centry/foo=bar") + g.Assert(annotation != nil).IsTrue("expected annotation") + g.Assert(err == nil).IsTrue("expected no error") + g.Assert(annotation.Namespace).Equal("centry") + g.Assert(annotation.Key).Equal("foo") + g.Assert(annotation.Value).Equal("bar") + }) + + g.It("returns annotation when it contains multiple equal signs", func() { + annotation, err := ParseAnnotation("centry/foo=bar=baz") + g.Assert(annotation != nil).IsTrue("expected no annotation") + g.Assert(err == nil).IsTrue("expected no error") + g.Assert(annotation.Namespace).Equal("centry") + g.Assert(annotation.Key).Equal("foo") + g.Assert(annotation.Value).Equal("bar=baz") + }) + + g.It("returns annotation when it contains multiple slashes", func() { + annotation, err := ParseAnnotation("centry/foo=bar/baz") + g.Assert(annotation != nil).IsTrue("expected no annotation") + g.Assert(err == nil).IsTrue("expected no error") + g.Assert(annotation.Namespace).Equal("centry") + g.Assert(annotation.Key).Equal("foo") + g.Assert(annotation.Value).Equal("bar/baz") + }) + + g.It("returns annotation when namespace contains key[value]", func() { + annotation, err := ParseAnnotation("centry.key[value]/foo=bar") + g.Assert(annotation != nil).IsTrue("expected no annotation") + g.Assert(err == nil).IsTrue("expected no error") + g.Assert(annotation.Namespace).Equal("centry.key") + g.Assert(annotation.NamespaceValues["key"]).Equal("value") + g.Assert(annotation.Key).Equal("foo") + g.Assert(annotation.Value).Equal("bar") + }) + + g.It("returns annotation when namespace contains multiple key[value]", func() { + annotation, err := ParseAnnotation("centry.key1[value1].key2[value2]/foo=bar") + g.Assert(annotation != nil).IsTrue("expected no annotation") + g.Assert(err == nil).IsTrue("expected no error") + g.Assert(annotation.Namespace).Equal("centry.key1.key2") + g.Assert(annotation.NamespaceValues["key1"]).Equal("value1") + g.Assert(annotation.NamespaceValues["key2"]).Equal("value2") + g.Assert(annotation.Key).Equal("foo") + g.Assert(annotation.Value).Equal("bar") + }) + + g.It("returns annotation when namespace contains key[value] where value has special character", func() { + annotation, err := ParseAnnotation("centry.key1[value:1].key2[value_2]/foo=bar") + g.Assert(annotation != nil).IsTrue("expected no annotation") + g.Assert(err == nil).IsTrue("expected no error") + g.Assert(annotation.Namespace).Equal("centry.key1.key2") + g.Assert(annotation.NamespaceValues["key1"]).Equal("value:1") + g.Assert(annotation.NamespaceValues["key2"]).Equal("value_2") + g.Assert(annotation.Key).Equal("foo") + g.Assert(annotation.Value).Equal("bar") + }) + }) + }) +} diff --git a/internal/pkg/shell/bash.go b/internal/pkg/shell/bash.go index 21b0fe5..7961b6e 100644 --- a/internal/pkg/shell/bash.go +++ b/internal/pkg/shell/bash.go @@ -8,6 +8,7 @@ import ( "path" "strings" + "github.com/kristofferahl/go-centry/internal/pkg/cmd" "github.com/kristofferahl/go-centry/internal/pkg/config" "github.com/kristofferahl/go-centry/internal/pkg/io" "github.com/sirupsen/logrus" @@ -129,18 +130,73 @@ func (s *BashScript) Functions() ([]*Function, error) { return nil, err } + // TODO: Make this part shared for all shell types for _, fname := range fnames { - f := &Function{Name: fname} + s.Log.WithFields(logrus.Fields{ + "func": fname, + }).Debugf("building function") + + f := &Function{ + Name: fname, + Options: cmd.NewOptionsSet(fname), + } + + options := make(map[string]*cmd.Option, 0) + for _, a := range annotations { - if a.Key == fname { - switch a.Namespace { - case config.CommandAnnotationDescriptionNamespace: + cmdName := a.NamespaceValues["cmd"] + if cmdName == "" || cmdName != f.Name { + continue + } + + s.Log.WithFields(logrus.Fields{ + "func": a.NamespaceValues["cmd"], + "namespace": a.Namespace, + "key": a.Key, + }).Debugf("handling annotation") + + switch a.Namespace { + case config.CommandAnnotationCmdOptionNamespace: + name := a.NamespaceValues["option"] + if name == "" { + continue + } + if options[name] == nil { + options[name] = &cmd.Option{Type: cmd.StringOption, Name: name} + } + switch a.Key { + case "type": + options[name].Type = cmd.StringToOptionType(a.Value) + case "short": + options[name].Short = a.Value + case "envName": + options[name].EnvName = a.Value + case "description": + options[name].Description = a.Value + case "default": + options[name].Default = a.Value + } + case config.CommandAnnotationCmdNamespace: + switch a.Key { + case "description": f.Description = a.Value - case config.CommandAnnotationHelpNamespace: + case "help": f.Help = a.Value } } } + + for _, v := range options { + if err := v.Validate(); err != nil { + s.Log.WithFields(logrus.Fields{ + "option": v.Name, + "type": v.Type, + }).Warn(err.Error()) + } else { + f.Options.Add(v) + } + } + funcs = append(funcs, f) } diff --git a/internal/pkg/shell/types.go b/internal/pkg/shell/types.go index 0ed51bf..9a43dff 100644 --- a/internal/pkg/shell/types.go +++ b/internal/pkg/shell/types.go @@ -1,5 +1,6 @@ package shell +import "github.com/kristofferahl/go-centry/internal/pkg/cmd" import "github.com/kristofferahl/go-centry/internal/pkg/io" // Executable defines the interface of an executable program @@ -12,6 +13,7 @@ type Function struct { Name string Description string Help string + Options *cmd.OptionsSet } // Script defines the interface of a script file diff --git a/test/data/commands/test.sh b/test/data/commands/test.sh index f30fbac..2c249be 100644 --- a/test/data/commands/test.sh +++ b/test/data/commands/test.sh @@ -1,9 +1,21 @@ #!/usr/bin/env bash +# centry.cmd[test:args].option[cmdstringopt]/type=string +# centry.cmd[test:args].option[cmdboolopt]/type=bool +# centry.cmd[test:args].option[cmdsel1]/type=select +# centry.cmd[test:args].option[cmdsel1]/envName=CMDSELECTOPT +# centry.cmd[test:args].option[cmdsel2]/type=select +# centry.cmd[test:args].option[cmdsel2]/envName=CMDSELECTOPT test:args() { echo "test:args ($*)" } +# centry.cmd[test:env].option[cmdstringopt]/type=string +# centry.cmd[test:env].option[cmdboolopt]/type=bool +# centry.cmd[test:env].option[cmdsel1]/type=select +# centry.cmd[test:env].option[cmdsel1]/envName=CMDSELECTOPT +# centry.cmd[test:env].option[cmdsel2]/type=select +# centry.cmd[test:env].option[cmdsel2]/envName=CMDSELECTOPT test:env() { env | sort } From 6a827841fcb436179444e6fc056f70937254a0d4 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Mon, 6 Apr 2020 08:19:12 +0200 Subject: [PATCH 11/19] - Removed unused func. --- cmd/centry/env.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cmd/centry/env.go b/cmd/centry/env.go index 6a608a4..69cb190 100644 --- a/cmd/centry/env.go +++ b/cmd/centry/env.go @@ -25,10 +25,6 @@ func (v envVar) IsString() bool { return v.Type == envTypeString } -func (v envVar) IsBool() bool { - return v.Type == envTypeBool -} - func optionsSetToEnvVars(c *cli.Context, set *cmd.OptionsSet) []envVar { envVars := make([]envVar, 0) for _, o := range set.Sorted() { From d4526aea2e2c2050621e71cbd7b2a6627301088b Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Mon, 6 Apr 2020 17:31:46 +0200 Subject: [PATCH 12/19] - Changed help and version flag texts. --- cmd/centry/options.go | 2 -- cmd/centry/runtime.go | 11 +++++++++++ cmd/centry/runtime_test.go | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cmd/centry/options.go b/cmd/centry/options.go index d7ec241..3064cc0 100644 --- a/cmd/centry/options.go +++ b/cmd/centry/options.go @@ -28,8 +28,6 @@ func createGlobalOptions(context *Context) *cmd.OptionsSet { Default: false, }) - // TODO: Override default version and help flags to get unified descriptions? - // Adding global options specified by the manifest for _, o := range manifest.Options { o := o diff --git a/cmd/centry/runtime.go b/cmd/centry/runtime.go index 3bbca80..5e89a67 100644 --- a/cmd/centry/runtime.go +++ b/cmd/centry/runtime.go @@ -51,6 +51,17 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { // Create global options options := createGlobalOptions(context) + cli.HelpFlag = &cli.BoolFlag{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Show help", + } + cli.VersionFlag = &cli.BoolFlag{ + Name: "version", + Aliases: []string{"v"}, + Usage: "Print the version", + } + // Initialize cli app := &cli.App{ Name: context.manifest.Config.Name, diff --git a/cmd/centry/runtime_test.go b/cmd/centry/runtime_test.go index d3ec228..9546dbb 100644 --- a/cmd/centry/runtime_test.go +++ b/cmd/centry/runtime_test.go @@ -181,8 +181,8 @@ func TestMain(t *testing.T) { --quiet, -q Disables logging (default: false) --staging Sets the context to staging (default: false) --stringopt value, -S value A custom option (default: "foobar") - --help, -h show help (default: false) - --version, -v print the version (default: false)` + --help, -h Show help (default: false) + --version, -v Print the version (default: false)` g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) }) From c0407c073a5a75e6331110bc3da1f8999151e8a3 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Mon, 6 Apr 2020 19:31:01 +0200 Subject: [PATCH 13/19] - Added more tests and refactored manifest. --- cmd/centry/serve.go | 6 ++- internal/pkg/config/annotations.go | 23 ++++---- internal/pkg/config/manifest.go | 24 +++++++++ internal/pkg/config/manifest_test.go | 80 +++++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 15 deletions(-) diff --git a/cmd/centry/serve.go b/cmd/centry/serve.go index 5996505..05810d5 100644 --- a/cmd/centry/serve.go +++ b/cmd/centry/serve.go @@ -108,7 +108,8 @@ func (sc *ServeCommand) executeHandler() func(w http.ResponseWriter, r *http.Req context := NewContext(API, io) context.commandEnabledFunc = func(cmd config.Command) bool { - if cmd.Annotations == nil || cmd.Annotations[config.CommandAnnotationAPIServe] != config.TrueString { + serveAnnotation, _ := cmd.Annotation(config.CommandAnnotationAPINamespace, "serve") + if serveAnnotation == nil || serveAnnotation.Value != config.TrueString { return false } @@ -116,7 +117,8 @@ func (sc *ServeCommand) executeHandler() func(w http.ResponseWriter, r *http.Req } context.optionEnabledFunc = func(opt config.Option) bool { - if opt.Annotations == nil || opt.Annotations[config.CommandAnnotationAPIServe] != config.TrueString { + serveAnnotation, _ := opt.Annotation(config.CommandAnnotationAPINamespace, "serve") + if serveAnnotation == nil || serveAnnotation.Value != config.TrueString { return false } diff --git a/internal/pkg/config/annotations.go b/internal/pkg/config/annotations.go index eb8b6c9..f090129 100644 --- a/internal/pkg/config/annotations.go +++ b/internal/pkg/config/annotations.go @@ -15,17 +15,8 @@ const CommandAnnotationCmdNamespace string = "centry.cmd" // CommandAnnotationCmdOptionNamespace defines an annotation namespace const CommandAnnotationCmdOptionNamespace string = "centry.cmd.option" -// CommandAnnotationAPIServe defines an annotation -const CommandAnnotationAPIServe string = "centry.api/serve" - -// // CommandAnnotationDescription defines an annotation namespace -// const CommandAnnotationDescription string = "centry.cmd/description" -// -// // CommandAnnotationHelp defines an annotation namespace -// const CommandAnnotationHelp string = "centry.cmd/help" -// -// // CommandAnnotationOptionType defines an annotation namespace -// const CommandAnnotationOptionType string = "centry.cmd.option/type" +// CommandAnnotationAPINamespace defines an annotation namespace +const CommandAnnotationAPINamespace string = "centry.api" // Annotation defines an annotation type Annotation struct { @@ -35,6 +26,16 @@ type Annotation struct { Value string } +// AnnotationNamespaceKey creates an annotation namespace/key string +func AnnotationNamespaceKey(namespace, key string) string { + return fmt.Sprintf("%s/%s", namespace, key) +} + +// AnnotationString creates an annotation string +func AnnotationString(namespace, key, value string) string { + return fmt.Sprintf("%s=%s", AnnotationNamespaceKey(namespace, key), value) +} + // ParseAnnotation parses text into an annotation func ParseAnnotation(text string) (*Annotation, error) { text = strings.TrimSpace(text) diff --git a/internal/pkg/config/manifest.go b/internal/pkg/config/manifest.go index 177a28a..2f4b2b0 100644 --- a/internal/pkg/config/manifest.go +++ b/internal/pkg/config/manifest.go @@ -31,6 +31,11 @@ type Command struct { Annotations map[string]string `yaml:"annotations,omitempty"` } +// Annotation returns a parsed annotation if present +func (c Command) Annotation(namespace, key string) (*Annotation, error) { + return ParseAnnotation(getAnnotationString(c.Annotations, namespace, key)) +} + // Option defines the structure of options type Option struct { Type cmd.OptionType `yaml:"type,omitempty"` @@ -42,6 +47,11 @@ type Option struct { Annotations map[string]string `yaml:"annotations,omitempty"` } +// Annotation returns a parsed annotation if present +func (o Option) Annotation(namespace, key string) (*Annotation, error) { + return ParseAnnotation(getAnnotationString(o.Annotations, namespace, key)) +} + // Config defines the structure for the configuration section type Config struct { Name string `yaml:"name,omitempty"` @@ -106,3 +116,17 @@ func parseManifestYaml(bs []byte) (*Manifest, error) { } return &m, nil } + +func getAnnotationString(annotations map[string]string, namespace, key string) string { + if annotations == nil { + return "" + } + + namespaceKey := AnnotationNamespaceKey(namespace, key) + value := annotations[namespaceKey] + if value == "" { + return "" + } + + return AnnotationString(namespace, key, value) +} diff --git a/internal/pkg/config/manifest_test.go b/internal/pkg/config/manifest_test.go index bc7940b..dd62aee 100644 --- a/internal/pkg/config/manifest_test.go +++ b/internal/pkg/config/manifest_test.go @@ -40,7 +40,7 @@ func TestManifest(t *testing.T) { }) }) - g.Describe("readManifestFile", func() { + g.Describe("read file", func() { g.It("returns byte slice when file is found", func() { file, _ := ioutil.TempFile("", "manifest") defer os.Remove(file.Name()) @@ -57,7 +57,7 @@ func TestManifest(t *testing.T) { }) }) - g.Describe("parseManifestFile", func() { + g.Describe("parse file", func() { g.It("returns manifest for valid yaml", func() { m, err := parseManifestYaml([]byte(`config:`)) g.Assert(m != nil).IsTrue("exected manifest") @@ -71,4 +71,80 @@ func TestManifest(t *testing.T) { g.Assert(strings.HasPrefix(err.Error(), "Failed to parse manifest yaml")).IsTrue("expected error message") }) }) + + g.Describe("command", func() { + g.Describe("annotations", func() { + g.It("returns nil when command has no annotations", func() { + c := Command{} + annotation, err := c.Annotation("x", "y") + g.Assert(annotation == nil).IsTrue("exected no annotation") + g.Assert(err == nil).IsTrue("expected error to be nil") + }) + + g.It("returns nil when command does not contain annotation", func() { + c := Command{ + Annotations: map[string]string{ + "centry.foo/bar": "baz", + }, + } + annotation, err := c.Annotation("x", "y") + g.Assert(annotation == nil).IsTrue("exected no annotation") + g.Assert(err == nil).IsTrue("expected error to be nil") + }) + + g.It("returns annotation when command contains annotation", func() { + c := Command{ + Annotations: map[string]string{ + "centry.x/x": "1", + "centry.y/y": "2", + "centry.z/z": "3", + }, + } + annotation, err := c.Annotation("centry.y", "y") + g.Assert(annotation != nil).IsTrue("exected annotation") + g.Assert(annotation.Namespace).Equal("centry.y") + g.Assert(annotation.Key).Equal("y") + g.Assert(annotation.Value).Equal("2") + g.Assert(err == nil).IsTrue("expected error to be nil") + }) + }) + }) + + g.Describe("option", func() { + g.Describe("annotations", func() { + g.It("returns nil when option has no annotations", func() { + o := Option{} + annotation, err := o.Annotation("x", "y") + g.Assert(annotation == nil).IsTrue("exected no annotation") + g.Assert(err == nil).IsTrue("expected error to be nil") + }) + + g.It("returns nil when option does not contain annotation", func() { + o := Option{ + Annotations: map[string]string{ + "centry.foo/bar": "baz", + }, + } + annotation, err := o.Annotation("x", "y") + g.Assert(annotation == nil).IsTrue("exected no annotation") + g.Assert(err == nil).IsTrue("expected error to be nil") + }) + + g.It("returns annotation when option contains annotation", func() { + o := Option{ + Annotations: map[string]string{ + "centry.x/x": "1", + "centry.y/y": "2", + "centry.z/z": "3", + }, + } + annotation, err := o.Annotation("centry.y", "y") + g.Assert(annotation != nil).IsTrue("exected annotation") + g.Assert(annotation.Namespace).Equal("centry.y") + g.Assert(annotation.Key).Equal("y") + g.Assert(annotation.Value).Equal("2") + g.Assert(err == nil).IsTrue("expected error to be nil") + }) + }) + }) } From 3c213e15963f49fb445ad62d4690f9c5fdae3c13 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Mon, 6 Apr 2020 20:20:09 +0200 Subject: [PATCH 14/19] - Cleanup and refactoring of NewRuntime func and added more tests. - Moved and refactored things related to environment variables. - Moved and refactored things related to commands and building of commands. - Renamed functions in Script interface. --- cmd/centry/commands.go | 147 ++++++++++++++++++++++++++++ cmd/centry/env.go | 74 -------------- cmd/centry/options.go | 60 +++++++++++- cmd/centry/runtime.go | 155 ++---------------------------- cmd/centry/runtime_test.go | 38 ++++++++ cmd/centry/script.go | 2 +- internal/pkg/shell/bash.go | 10 +- internal/pkg/shell/environment.go | 41 ++++++++ internal/pkg/shell/types.go | 4 +- test/data/commands/get.sh | 2 + 10 files changed, 301 insertions(+), 232 deletions(-) create mode 100644 cmd/centry/commands.go delete mode 100644 cmd/centry/env.go create mode 100644 internal/pkg/shell/environment.go diff --git a/cmd/centry/commands.go b/cmd/centry/commands.go new file mode 100644 index 0000000..9b215f9 --- /dev/null +++ b/cmd/centry/commands.go @@ -0,0 +1,147 @@ +package main + +import ( + "fmt" + "sort" + "strings" + + "github.com/kristofferahl/go-centry/internal/pkg/cmd" + "github.com/kristofferahl/go-centry/internal/pkg/config" + "github.com/kristofferahl/go-centry/internal/pkg/shell" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +func registerBuiltinCommands(runtime *Runtime) { + context := runtime.context + + if context.executor == CLI { + serveCmd := &ServeCommand{ + Manifest: context.manifest, + Log: context.log.GetLogger().WithFields(logrus.Fields{ + "command": "serve", + }), + } + runtime.cli.Commands = append(runtime.cli.Commands, serveCmd.ToCLICommand()) + } +} + +func registerManifestCommands(runtime *Runtime, options *cmd.OptionsSet) { + context := runtime.context + + for _, cmd := range context.manifest.Commands { + cmd := cmd + + if context.commandEnabledFunc != nil && context.commandEnabledFunc(cmd) == false { + continue + } + + script := createScript(cmd, context) + + funcs, err := script.Functions() + if err != nil { + context.log.GetLogger().WithFields(logrus.Fields{ + "command": cmd.Name, + }).Errorf("Failed to parse script functions. %v", err) + } else { + for _, fn := range funcs { + fn := fn + cmd := cmd + namespace := script.FunctionNamespace(cmd.Name) + + if fn.Name != cmd.Name && strings.HasPrefix(fn.Name, namespace) == false { + continue + } + + cmdDescription := cmd.Description + if fn.Description != "" { + cmd.Description = fn.Description + } + + cmdHelp := cmd.Help + if fn.Help != "" { + cmd.Help = fn.Help + } + + scriptCmd := &ScriptCommand{ + Context: context, + Log: context.log.GetLogger().WithFields(logrus.Fields{}), + GlobalOptions: options, + Command: cmd, + Script: script, + Function: *fn, + } + cliCmd := scriptCmd.ToCLICommand() + + cmdKeyParts := scriptCmd.GetCommandInvocationPath() + + var root *cli.Command + for depth, cmdKeyPart := range cmdKeyParts { + if depth == 0 { + if getCommand(runtime.cli.Commands, cmdKeyPart) == nil { + if depth == len(cmdKeyParts)-1 { + // add destination command + runtime.cli.Commands = append(runtime.cli.Commands, cliCmd) + } else { + // add placeholder + runtime.cli.Commands = append(runtime.cli.Commands, &cli.Command{ + Name: cmdKeyPart, + Usage: cmdDescription, + UsageText: cmdHelp, + HideHelpCommand: true, + Action: nil, + }) + } + } + root = getCommand(runtime.cli.Commands, cmdKeyPart) + } else { + if getCommand(root.Subcommands, cmdKeyPart) == nil { + if depth == len(cmdKeyParts)-1 { + // add destination command + root.Subcommands = append(root.Subcommands, cliCmd) + } else { + // add placeholder + root.Subcommands = append(root.Subcommands, &cli.Command{ + Name: cmdKeyPart, + Usage: "...", + UsageText: "", + HideHelpCommand: true, + Action: nil, + }) + } + } + root = getCommand(root.Subcommands, cmdKeyPart) + } + } + + runtime.events = append(runtime.events, fmt.Sprintf("Registered command \"%s\"", scriptCmd.GetCommandInvocation())) + } + } + } +} + +func getCommand(commands []*cli.Command, name string) *cli.Command { + for _, c := range commands { + if c.HasName(name) { + return c + } + } + + return nil +} + +func sortCommands(commands []*cli.Command) { + sort.Slice(commands, func(i, j int) bool { + return commands[i].Name < commands[j].Name + }) +} + +func createScript(cmd config.Command, context *Context) shell.Script { + return &shell.BashScript{ + BasePath: context.manifest.BasePath, + Path: cmd.Path, + Log: context.log.GetLogger().WithFields(logrus.Fields{ + "script": cmd.Path, + }), + } +} diff --git a/cmd/centry/env.go b/cmd/centry/env.go deleted file mode 100644 index 69cb190..0000000 --- a/cmd/centry/env.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "sort" - "strings" - - "github.com/kristofferahl/go-centry/internal/pkg/cmd" - "github.com/urfave/cli/v2" -) - -type envType string - -const ( - envTypeString envType = "string" - envTypeBool envType = "bool" -) - -type envVar struct { - Name string - Value string - Type envType -} - -func (v envVar) IsString() bool { - return v.Type == envTypeString -} - -func optionsSetToEnvVars(c *cli.Context, set *cmd.OptionsSet) []envVar { - envVars := make([]envVar, 0) - for _, o := range set.Sorted() { - o := o - - envName := o.EnvName - if envName == "" { - envName = o.Name - } - envName = strings.Replace(strings.ToUpper(envName), ".", "_", -1) - envName = strings.Replace(strings.ToUpper(envName), "-", "_", -1) - - value := c.String(o.Name) - - switch o.Type { - case cmd.BoolOption: - envVars = append(envVars, envVar{ - Name: envName, - Value: value, - Type: envTypeBool, - }) - case cmd.StringOption: - envVars = append(envVars, envVar{ - Name: envName, - Value: value, - Type: envTypeString, - }) - case cmd.SelectOption: - if value == "true" { - envVars = append(envVars, envVar{ - Name: envName, - Value: o.Name, - Type: envTypeString, - }) - } - } - } - - return sortEnv(envVars) -} - -func sortEnv(vars []envVar) []envVar { - sort.Slice(vars, func(i, j int) bool { - return vars[i].Name < vars[j].Name - }) - return vars -} diff --git a/cmd/centry/options.go b/cmd/centry/options.go index 3064cc0..7d808a3 100644 --- a/cmd/centry/options.go +++ b/cmd/centry/options.go @@ -1,18 +1,31 @@ package main import ( + "strings" + "github.com/kristofferahl/go-centry/internal/pkg/cmd" + "github.com/kristofferahl/go-centry/internal/pkg/shell" "github.com/urfave/cli/v2" ) -// OptionSetGlobal is the name of the global OptionsSet -const OptionSetGlobal = "Global" +func configureDefaultOptions() { + cli.HelpFlag = &cli.BoolFlag{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Show help", + } + cli.VersionFlag = &cli.BoolFlag{ + Name: "version", + Aliases: []string{"v"}, + Usage: "Print the version", + } +} func createGlobalOptions(context *Context) *cmd.OptionsSet { manifest := context.manifest // Add global options - options := cmd.NewOptionsSet(OptionSetGlobal) + options := cmd.NewOptionsSet("Global") options.Add(&cmd.Option{ Type: cmd.StringOption, @@ -108,3 +121,44 @@ func optionsSetToFlags(options *cmd.OptionsSet) []cli.Flag { return flags } + +func optionsSetToEnvVars(c *cli.Context, set *cmd.OptionsSet) []shell.EnvironmentVariable { + envVars := make([]shell.EnvironmentVariable, 0) + for _, o := range set.Sorted() { + o := o + + envName := o.EnvName + if envName == "" { + envName = o.Name + } + envName = strings.Replace(strings.ToUpper(envName), ".", "_", -1) + envName = strings.Replace(strings.ToUpper(envName), "-", "_", -1) + + value := c.String(o.Name) + + switch o.Type { + case cmd.BoolOption: + envVars = append(envVars, shell.EnvironmentVariable{ + Name: envName, + Value: value, + Type: shell.EnvironmentVariableTypeBool, + }) + case cmd.StringOption: + envVars = append(envVars, shell.EnvironmentVariable{ + Name: envName, + Value: value, + Type: shell.EnvironmentVariableTypeString, + }) + case cmd.SelectOption: + if value == "true" { + envVars = append(envVars, shell.EnvironmentVariable{ + Name: envName, + Value: o.Name, + Type: shell.EnvironmentVariableTypeString, + }) + } + } + } + + return shell.SortEnvironmentVariables(envVars) +} diff --git a/cmd/centry/runtime.go b/cmd/centry/runtime.go index 5e89a67..233b64e 100644 --- a/cmd/centry/runtime.go +++ b/cmd/centry/runtime.go @@ -1,14 +1,8 @@ package main import ( - "fmt" - "sort" - "strings" - "github.com/kristofferahl/go-centry/internal/pkg/config" "github.com/kristofferahl/go-centry/internal/pkg/log" - "github.com/kristofferahl/go-centry/internal/pkg/shell" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -51,19 +45,11 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { // Create global options options := createGlobalOptions(context) - cli.HelpFlag = &cli.BoolFlag{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Show help", - } - cli.VersionFlag = &cli.BoolFlag{ - Name: "version", - Aliases: []string{"v"}, - Usage: "Print the version", - } + // Configure default options + configureDefaultOptions() // Initialize cli - app := &cli.App{ + runtime.cli = &cli.App{ Name: context.manifest.Config.Name, HelpName: context.manifest.Config.Name, Usage: "A tool for building declarative CLI's over bash scripts, written in go.", // TODO: Set from manifest config @@ -97,113 +83,14 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { }, } - logger := context.log.GetLogger() - // Register builtin commands - if context.executor == CLI { - serveCmd := &ServeCommand{ - Manifest: context.manifest, - Log: logger.WithFields(logrus.Fields{ - "command": "serve", - }), - } - app.Commands = append(app.Commands, serveCmd.ToCLICommand()) - } + registerBuiltinCommands(runtime) - // Build commands - for _, cmd := range context.manifest.Commands { - cmd := cmd - - if context.commandEnabledFunc != nil && context.commandEnabledFunc(cmd) == false { - continue - } - - script := createScript(cmd, context) - - funcs, err := script.Functions() - if err != nil { - logger.WithFields(logrus.Fields{ - "command": cmd.Name, - }).Errorf("Failed to parse script functions. %v", err) - } else { - for _, fn := range funcs { - fn := fn - cmd := cmd - namespace := script.CreateFunctionNamespace(cmd.Name) - - if fn.Name != cmd.Name && strings.HasPrefix(fn.Name, namespace) == false { - continue - } - - cmdDescription := cmd.Description - if fn.Description != "" { - cmd.Description = fn.Description - } - - cmdHelp := cmd.Help - if fn.Help != "" { - cmd.Help = fn.Help - } - - scriptCmd := &ScriptCommand{ - Context: context, - Log: logger.WithFields(logrus.Fields{}), - GlobalOptions: options, - Command: cmd, - Script: script, - Function: *fn, - } - cliCmd := scriptCmd.ToCLICommand() - - cmdKeyParts := scriptCmd.GetCommandInvocationPath() - - var root *cli.Command - for depth, cmdKeyPart := range cmdKeyParts { - if depth == 0 { - if getCommand(app.Commands, cmdKeyPart) == nil { - if depth == len(cmdKeyParts)-1 { - // add destination command - app.Commands = append(app.Commands, cliCmd) - } else { - // add placeholder - app.Commands = append(app.Commands, &cli.Command{ - Name: cmdKeyPart, - Usage: cmdDescription, - UsageText: cmdHelp, - HideHelpCommand: true, - Action: nil, - }) - } - } - root = getCommand(app.Commands, cmdKeyPart) - } else { - if getCommand(root.Subcommands, cmdKeyPart) == nil { - if depth == len(cmdKeyParts)-1 { - // add destination command - root.Subcommands = append(root.Subcommands, cliCmd) - } else { - // add placeholder - root.Subcommands = append(root.Subcommands, &cli.Command{ - Name: cmdKeyPart, - Usage: "...", - UsageText: "", - HideHelpCommand: true, - Action: nil, - }) - } - } - root = getCommand(root.Subcommands, cmdKeyPart) - } - } - - runtime.events = append(runtime.events, fmt.Sprintf("Registered command \"%s\"", scriptCmd.GetCommandInvocation())) - } - } - } + // Register manifest commands + registerManifestCommands(runtime, options) - sortCommands(app.Commands) - - runtime.cli = app + // Sort commands + sortCommands(runtime.cli.Commands) return runtime, nil } @@ -221,29 +108,3 @@ func (runtime *Runtime) Execute() int { return 0 } - -func createScript(cmd config.Command, context *Context) shell.Script { - return &shell.BashScript{ - BasePath: context.manifest.BasePath, - Path: cmd.Path, - Log: context.log.GetLogger().WithFields(logrus.Fields{ - "script": cmd.Path, - }), - } -} - -func getCommand(commands []*cli.Command, name string) *cli.Command { - for _, c := range commands { - if c.HasName(name) { - return c - } - } - - return nil -} - -func sortCommands(commands []*cli.Command) { - sort.Slice(commands, func(i, j int) bool { - return commands[i].Name < commands[j].Name - }) -} diff --git a/cmd/centry/runtime_test.go b/cmd/centry/runtime_test.go index 9546dbb..862640a 100644 --- a/cmd/centry/runtime_test.go +++ b/cmd/centry/runtime_test.go @@ -187,6 +187,44 @@ func TestMain(t *testing.T) { g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) }) }) + + g.Describe("command help", func() { + result := execQuiet("get --help") + + g.It("should display available commands", func() { + expected := `NAME: + centry get - Gets stuff + +USAGE: + centry get command [command options] [arguments...] + +COMMANDS: + sub Description for subcommand + +OPTIONS: + --help, -h Show help (default: false) + --version, -v Print the version (default: false)` + + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) + }) + }) + + g.Describe("subcommand help", func() { + result := execQuiet("get sub --help") + + g.It("should display full help", func() { + expected := `NAME: + centry get sub - Description for subcommand + +USAGE: + Help text for sub command + +OPTIONS: + --help, -h Show help (default: false)` + + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) + }) + }) }) g.Describe("global options", func() { diff --git a/cmd/centry/script.go b/cmd/centry/script.go index 6564edc..f011989 100644 --- a/cmd/centry/script.go +++ b/cmd/centry/script.go @@ -25,7 +25,7 @@ type ScriptCommand struct { // GetCommandInvocation returns the command invocation string func (sc *ScriptCommand) GetCommandInvocation() string { - return strings.Replace(sc.Function.Name, sc.Script.FunctionNameSplitChar(), " ", -1) + return strings.Replace(sc.Function.Name, sc.Script.FunctionNamespaceSplitChar(), " ", -1) } // GetCommandInvocationPath returns the command invocation path diff --git a/internal/pkg/shell/bash.go b/internal/pkg/shell/bash.go index 7961b6e..9619eb2 100644 --- a/internal/pkg/shell/bash.go +++ b/internal/pkg/shell/bash.go @@ -203,12 +203,12 @@ func (s *BashScript) Functions() ([]*Function, error) { return funcs, nil } -// CreateFunctionNamespace returns a namespaced function name -func (s *BashScript) CreateFunctionNamespace(name string) string { - return fmt.Sprintf("%s%s", name, s.FunctionNameSplitChar()) +// FunctionNamespace returns a namespaced function name +func (s *BashScript) FunctionNamespace(name string) string { + return fmt.Sprintf("%s%s", name, s.FunctionNamespaceSplitChar()) } -// FunctionNameSplitChar returns the separator used for function namespaces -func (s *BashScript) FunctionNameSplitChar() string { +// FunctionNamespaceSplitChar returns the separator used for function namespaces +func (s *BashScript) FunctionNamespaceSplitChar() string { return ":" } diff --git a/internal/pkg/shell/environment.go b/internal/pkg/shell/environment.go new file mode 100644 index 0000000..8dc9af3 --- /dev/null +++ b/internal/pkg/shell/environment.go @@ -0,0 +1,41 @@ +package shell + +import ( + "sort" +) + +// EnvironmentVariableType represents a type of an environment variable +type EnvironmentVariableType string + +const ( + // EnvironmentVariableTypeString represents a string environment variable + EnvironmentVariableTypeString EnvironmentVariableType = "string" + + // EnvironmentVariableTypeBool represents a boolean environment variable + EnvironmentVariableTypeBool EnvironmentVariableType = "bool" +) + +// EnvironmentVariable represents an environment variable +type EnvironmentVariable struct { + Name string + Value string + Type EnvironmentVariableType +} + +// IsString returns true if the environment variable is of type string +func (v EnvironmentVariable) IsString() bool { + return v.Type == EnvironmentVariableTypeString +} + +// IsBool returns true if the environment variable is of type boolean +func (v EnvironmentVariable) IsBool() bool { + return v.Type == EnvironmentVariableTypeBool +} + +// SortEnvironmentVariables sorts environment variables by name +func SortEnvironmentVariables(vars []EnvironmentVariable) []EnvironmentVariable { + sort.Slice(vars, func(i, j int) bool { + return vars[i].Name < vars[j].Name + }) + return vars +} diff --git a/internal/pkg/shell/types.go b/internal/pkg/shell/types.go index 9a43dff..f65870d 100644 --- a/internal/pkg/shell/types.go +++ b/internal/pkg/shell/types.go @@ -23,6 +23,6 @@ type Script interface { FullPath() string RelativePath() string Functions() (funcs []*Function, err error) - CreateFunctionNamespace(name string) string - FunctionNameSplitChar() string + FunctionNamespace(name string) string + FunctionNamespaceSplitChar() string } diff --git a/test/data/commands/get.sh b/test/data/commands/get.sh index 148e0b5..4e8f6a6 100644 --- a/test/data/commands/get.sh +++ b/test/data/commands/get.sh @@ -4,6 +4,8 @@ get() { echo "get ($*)" } +# centry.cmd[get:sub]/description=Description for subcommand +# centry.cmd[get:sub]/help=Help text for sub command get:sub() { echo "get:sub ($*)" } From f85916fac2043072153ae2feac4f7f1fe0a04823 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Mon, 6 Apr 2020 20:30:43 +0200 Subject: [PATCH 15/19] - Fixed bug in example. --- examples/centry/commands/get.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/centry/commands/get.sh b/examples/centry/commands/get.sh index 5b41883..26bd08e 100644 --- a/examples/centry/commands/get.sh +++ b/examples/centry/commands/get.sh @@ -11,7 +11,7 @@ get:env() { local output output="$(env | ${SORTED} | grep "${FILTER}")" - if [[ ${SANITIZE_OUTPUT} ]]; then + if [[ ${SANITIZE_OUTPUT} == true ]]; then echo "${output}" | sed 's/\=.*$/=***/' else echo "${output}" From 19f40d405719b4bf25743b43554c3b98da6dbe42 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Tue, 7 Apr 2020 09:45:13 +0200 Subject: [PATCH 16/19] - Better tests for subcommands. --- cmd/centry/runtime_test.go | 54 +++++++++++++++++++++++++++++++++----- test/data/commands/test.sh | 18 +++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/cmd/centry/runtime_test.go b/cmd/centry/runtime_test.go index 862640a..4888991 100644 --- a/cmd/centry/runtime_test.go +++ b/cmd/centry/runtime_test.go @@ -189,17 +189,20 @@ func TestMain(t *testing.T) { }) g.Describe("command help", func() { - result := execQuiet("get --help") + result := execQuiet("test --help") g.It("should display available commands", func() { expected := `NAME: - centry get - Gets stuff + centry test - Test stuff USAGE: - centry get command [command options] [arguments...] + centry test command [command options] [arguments...] COMMANDS: - sub Description for subcommand + args Test stuff + env Test stuff + placeholder ... + subcommand Description for subcommand OPTIONS: --help, -h Show help (default: false) @@ -210,11 +213,11 @@ OPTIONS: }) g.Describe("subcommand help", func() { - result := execQuiet("get sub --help") + result := execQuiet("test subcommand --help") g.It("should display full help", func() { expected := `NAME: - centry get sub - Description for subcommand + centry test subcommand - Description for subcommand USAGE: Help text for sub command @@ -225,6 +228,45 @@ OPTIONS: g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) }) }) + + g.Describe("placeholder help", func() { + result := execQuiet("test placeholder --help") + + g.It("should display full help", func() { + expected := `NAME: + centry test placeholder - ... + +USAGE: + centry test placeholder command [command options] [arguments...] + +COMMANDS: + subcommand1 Description for placeholder subcommand1 + subcommand2 Description for placeholder subcommand2 + +OPTIONS: + --help, -h Show help (default: false) + --version, -v Print the version (default: false)` + + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) + }) + + g.Describe("placeholder subcommand help", func() { + result := execQuiet("test placeholder subcommand1 --help") + + g.It("should display full help", func() { + expected := `NAME: + centry test placeholder subcommand1 - Description for placeholder subcommand1 + +USAGE: + Help text for placeholder subcommand1 + +OPTIONS: + --help, -h Show help (default: false)` + + g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) + }) + }) + }) }) g.Describe("global options", func() { diff --git a/test/data/commands/test.sh b/test/data/commands/test.sh index 2c249be..d97b4e6 100644 --- a/test/data/commands/test.sh +++ b/test/data/commands/test.sh @@ -19,3 +19,21 @@ test:args() { test:env() { env | sort } + +# centry.cmd[test:subcommand]/description=Description for subcommand +# centry.cmd[test:subcommand]/help=Help text for sub command +test:subcommand() { + echo "test:subcommand ($*)" +} + +# centry.cmd[test:placeholder:subcommand1]/description=Description for placeholder subcommand1 +# centry.cmd[test:placeholder:subcommand1]/help=Help text for placeholder subcommand1 +test:placeholder:subcommand1() { + echo "test:placeholder:subcommand1 ($*)" +} + +# centry.cmd[test:placeholder:subcommand2]/description=Description for placeholder subcommand2 +# centry.cmd[test:placeholder:subcommand2]/help=Help text for placeholder subcommand2 +test:placeholder:subcommand2() { + echo "test:placeholder:subcommand2 ($*)" +} From d8efcabc4ae6a2fee7127dd6a5b9dc0950b96fce Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Thu, 9 Apr 2020 18:03:09 +0200 Subject: [PATCH 17/19] - Refactored runtime tests and test data. --- cmd/centry/runtime_test.go | 321 +++++++++--------- internal/pkg/config/manifest_test.go | 4 +- internal/pkg/test/error.go | 21 ++ internal/pkg/test/strings.go | 56 +++ test/data/commands/command_test.sh | 29 ++ test/data/commands/delete.sh | 9 - test/data/commands/get.sh | 11 - test/data/commands/help_test.sh | 28 ++ test/data/commands/option_test.sh | 13 + test/data/commands/postput.sh | 17 - test/data/commands/script_test.sh | 5 + test/data/commands/test.sh | 39 --- test/data/main_test.yaml | 53 --- ...nvalid.yaml => manifest_test_invalid.yaml} | 0 test/data/manifest_test_valid.yaml | 37 ++ test/data/runtime_test.yaml | 49 +++ 16 files changed, 406 insertions(+), 286 deletions(-) create mode 100644 internal/pkg/test/error.go create mode 100644 test/data/commands/command_test.sh delete mode 100644 test/data/commands/delete.sh delete mode 100644 test/data/commands/get.sh create mode 100644 test/data/commands/help_test.sh create mode 100644 test/data/commands/option_test.sh delete mode 100644 test/data/commands/postput.sh create mode 100644 test/data/commands/script_test.sh delete mode 100644 test/data/commands/test.sh delete mode 100644 test/data/main_test.yaml rename test/data/{invalid.yaml => manifest_test_invalid.yaml} (100%) create mode 100644 test/data/manifest_test_valid.yaml create mode 100644 test/data/runtime_test.yaml diff --git a/cmd/centry/runtime_test.go b/cmd/centry/runtime_test.go index 4888991..8e5a879 100644 --- a/cmd/centry/runtime_test.go +++ b/cmd/centry/runtime_test.go @@ -29,178 +29,249 @@ func TestMain(t *testing.T) { g.Describe("scripts", func() { g.It("loads script in the expected order", func() { + expected := "Loading init.sh\nLoading helpers.sh" os.Setenv("OUTPUT_DEBUG", "true") - g.Assert(strings.HasPrefix(execQuiet("get").Stdout, "Loading init.sh\nLoading helpers.sh\n")).IsTrue() + out := execQuiet("scripttest") + test.AssertStringContains(g, out.Stdout, expected) os.Unsetenv("OUTPUT_DEBUG") }) }) + g.Describe("commands", func() { + g.Describe("invoking command", func() { + g.Describe("with arguments", func() { + g.It("should have arguments passed", func() { + expected := "command args (foo bar)" + out := execQuiet("commandtest foo bar") + test.AssertStringContains(g, out.Stdout, expected) + }) + }) + + g.Describe("without arguments", func() { + g.It("should have no arguments passed", func() { + expected := "command args ()" + out := execQuiet("commandtest") + test.AssertStringContains(g, out.Stdout, expected) + }) + }) + }) + + g.Describe("invoking sub command", func() { + g.Describe("with arguments", func() { + g.It("should have arguments passed", func() { + expected := "subcommand args (foo bar)" + out := execQuiet("commandtest subcommand foo bar") + test.AssertStringContains(g, out.Stdout, expected) + }) + }) + + g.Describe("without arguments", func() { + g.It("should have no arguments passed", func() { + expected := "subcommand args ()" + out := execQuiet("commandtest subcommand") + test.AssertStringContains(g, out.Stdout, expected) + }) + }) + }) + + g.Describe("command options", func() { + g.Describe("invoking command with options", func() { + g.It("should have arguments passed", func() { + expected := "command args (foo bar baz)" + out := execQuiet("commandtest options args --cmdstringopt=hello --cmdboolopt --cmdsel1 --cmdsel2 foo bar baz") + test.AssertStringContains(g, out.Stdout, expected) + }) + + g.It("should have environment variables set", func() { + out := execQuiet("commandtest options printenv --cmdstringopt=world --cmdboolopt --cmdsel1 --cmdsel2") + test.AssertStringHasKeyValue(g, out.Stdout, "CMDSTRINGOPT", "world") + test.AssertStringHasKeyValue(g, out.Stdout, "CMDBOOLOPT", "true") + test.AssertStringHasKeyValue(g, out.Stdout, "CMDSELECTOPT", "cmdsel2") + }) + }) + }) + }) + g.Describe("options", func() { g.Describe("invoke without option", func() { g.It("should pass arguments", func() { - g.Assert(execQuiet("test args foo bar").Stdout).Equal("test:args (foo bar)\n") + expected := "args (foo bar)" + out := execQuiet("optiontest args foo bar") + test.AssertStringContains(g, out.Stdout, expected) }) + // TODO: Add assertions for all default values? g.It("should have default value for environment variable set", func() { - test.AssertKeyValueExists(g, "STRINGOPT", "foobar", execQuiet("test env").Stdout) + out := execQuiet("optiontest printenv") + test.AssertStringHasKeyValue(g, out.Stdout, "STRINGOPT", "foobar") }) }) g.Describe("invoke with single option", func() { g.It("should have arguments passed", func() { - g.Assert(execQuiet("--staging test args foo bar").Stdout).Equal("test:args (foo bar)\n") + expected := "args (foo bar)" + out := execQuiet("--boolopt optiontest args foo bar") + test.AssertStringContains(g, out.Stdout, expected) }) - g.It("should have environment set to staging", func() { - test.AssertKeyValueExists(g, "CONTEXT", "staging", execQuiet("--staging test env").Stdout) + g.It("should have environment set for select option", func() { + out := execQuiet("--selectopt1 optiontest printenv") + test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPT", "selectopt1") }) - g.It("should have environment set to last option with same env_name (production)", func() { - test.AssertKeyValueExists(g, "CONTEXT", "production", execQuiet("--staging=false --production test env").Stdout) + g.It("should have environment set to last select option with same env_name (selectopt2)", func() { + out := execQuiet("--selectopt1 --selectopt2 optiontest printenv") + test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPT", "selectopt2") }) - g.It("should have environment set to last option with same env_name (staging)", func() { - test.AssertKeyValueExists(g, "CONTEXT", "staging", execQuiet("--production=false --staging test env").Stdout) + // TODO: Do we really need =false?? + g.It("should have environment set to last select option with same env_name (selectopt1)", func() { + out := execQuiet("--selectopt2=false --selectopt1 optiontest printenv") + test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPT", "selectopt1") }) }) g.Describe("invoke with multiple options", func() { g.It("should have arguments passed", func() { - g.Assert(execQuiet("--staging --stringopt=baz test args foo bar").Stdout).Equal("test:args (foo bar)\n") + expected := "args (bar foo)" + out := execQuiet("--boolopt --stringopt=foo optiontest args bar foo") + test.AssertStringContains(g, out.Stdout, expected) }) g.It("should have multipe environment variables set", func() { - out := execQuiet("--staging --stringopt=bazer --boolopt test env").Stdout - test.AssertKeyValueExists(g, "CONTEXT", "staging", out) - test.AssertKeyValueExists(g, "STRINGOPT", "bazer", out) - test.AssertKeyValueExists(g, "BOOLOPT", "true", out) + out := execQuiet("--selectopt2 --stringopt=blazer --boolopt optiontest printenv") + + test.AssertStringHasKeyValue(g, out.Stdout, "STRINGOPT", "blazer") + test.AssertStringHasKeyValue(g, out.Stdout, "BOOLOPT", "true") + test.AssertStringHasKeyValue(g, out.Stdout, "SELECTOPT", "selectopt2") }) }) g.Describe("invoke with invalid option", func() { - g.It("should return error", func() { - res := execQuiet("--invalidoption test args foo bar") - g.Assert(strings.Contains(res.Stdout, "Incorrect Usage. flag provided but not defined: -invalidoption")).IsTrue() - g.Assert(strings.Contains(res.Stderr, "flag provided but not defined: -invalidoption")).IsTrue() - g.Assert(res.Error == nil).IsTrue() + g.It("should print error message", func() { + out := execQuiet("--invalidoption optiontest args") + test.AssertStringContains(g, out.Stdout, "Incorrect Usage. flag provided but not defined: -invalidoption") + test.AssertStringContains(g, out.Stderr, "flag provided but not defined: -invalidoption") + test.AssertNoError(g, out.Error) }) }) }) - g.Describe("commands", func() { - g.Describe("invoking command", func() { - g.Describe("with arguments", func() { - g.It("should have arguments passed", func() { - g.Assert(execQuiet("get foo bar").Stdout).Equal("get (foo bar)\n") + g.Describe("global options", func() { + g.Describe("version", func() { + g.Describe("--version", func() { + g.It("should display version", func() { + expected := `1.0.0` + out := execQuiet("--version") + test.AssertStringContains(g, out.Stdout, expected) }) }) - g.Describe("without arguments", func() { - g.It("should have no arguments passed", func() { - g.Assert(execQuiet("get").Stdout).Equal("get ()\n") + g.Describe("-v", func() { + g.It("should display version", func() { + expected := `1.0.0` + out := execQuiet("-v") + test.AssertStringContains(g, out.Stdout, expected) }) }) }) - g.Describe("invoking sub command", func() { - g.Describe("with arguments", func() { - g.It("should have arguments passed", func() { - g.Assert(execQuiet("get sub foo bar").Stdout).Equal("get:sub (foo bar)\n") + g.Describe("quiet", func() { + g.Describe("--quiet", func() { + g.It("should disable logging", func() { + expected := `Changing loglevel to panic (from debug)` + out := execWithLogging("--quiet") + test.AssertStringContains(g, out.Stderr, expected) }) }) - g.Describe("without arguments", func() { - g.It("should have no arguments passed", func() { - g.Assert(execQuiet("get sub").Stdout).Equal("get:sub ()\n") + g.Describe("-q", func() { + g.It("should disable logging", func() { + expected := `Changing loglevel to panic (from debug)` + out := execWithLogging("-q") + test.AssertStringContains(g, out.Stderr, expected) }) }) }) - g.Describe("command options", func() { - g.Describe("invoking command with options", func() { - g.It("should have arguments passed", func() { - g.Assert(execQuiet("test args --cmdstringopt=hello --cmdboolopt --cmdsel1 --cmdsel2 foo bar baz").Stdout).Equal("test:args (foo bar baz)\n") - }) + g.Describe("--config.log.level=info", func() { + g.It("should change log level to info", func() { + expected := `Changing loglevel to info (from debug)` + out := execWithLogging("--config.log.level=info") + test.AssertStringContains(g, out.Stderr, expected) + }) + }) - g.It("should have multipe environment variables set", func() { - out := execQuiet("test env --cmdstringopt=world --cmdboolopt --cmdsel1 --cmdsel2").Stdout - test.AssertKeyValueExists(g, "CMDSTRINGOPT", "world", out) - test.AssertKeyValueExists(g, "CMDBOOLOPT", "true", out) - test.AssertKeyValueExists(g, "CMDSELECTOPT", "cmdsel2", out) - }) + g.Describe("--config.log.level=error", func() { + g.It("should change log level to error", func() { + expected := `Changing loglevel to error (from debug)` + out := execWithLogging("--config.log.level=error") + test.AssertStringContains(g, out.Stderr, expected) }) }) }) g.Describe("help", func() { g.Describe("call with no arguments", func() { - result := execQuiet("") - g.It("should display help", func() { - expected := `USAGE:` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() + expected := "USAGE:" + out := execQuiet("") + test.AssertStringContains(g, out.Stdout, expected) }) }) g.Describe("call with -h", func() { - result := execQuiet("-h") - g.It("should display help", func() { - expected := `USAGE:` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() + expected := "USAGE:" + out := execQuiet("-h") + test.AssertStringContains(g, out.Stdout, expected) }) }) g.Describe("call with --help", func() { - result := execQuiet("--help") - g.It("should display help", func() { - expected := `USAGE:` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() + expected := "USAGE:" + out := execQuiet("--help") + test.AssertStringContains(g, out.Stdout, expected) }) }) g.Describe("output", func() { - result := execQuiet("") - + out := execQuiet("") g.It("should display available commands", func() { expected := `COMMANDS: - delete Deletes stuff - get Gets stuff - post Creates stuff - put Creates/Updates stuff` + commandtest Command tests + helptest Help tests + optiontest Option tests + scripttest Script tests` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) + test.AssertStringContains(g, out.Stdout, expected) }) g.It("should display global options", func() { expected := `OPTIONS: --boolopt, -B A custom option (default: false) --config.log.level value Overrides the log level (default: "debug") - --production Sets the context to production (default: false) --quiet, -q Disables logging (default: false) - --staging Sets the context to staging (default: false) + --selectopt1 Sets the selection to option 1 (default: false) + --selectopt2 Sets the selection to option 2 (default: false) --stringopt value, -S value A custom option (default: "foobar") --help, -h Show help (default: false) --version, -v Print the version (default: false)` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) + test.AssertStringContains(g, out.Stdout, expected) }) }) - g.Describe("command help", func() { - result := execQuiet("test --help") - - g.It("should display available commands", func() { + g.Describe("command help output", func() { + g.It("should display full help", func() { expected := `NAME: - centry test - Test stuff + centry helptest - Help tests USAGE: - centry test command [command options] [arguments...] + centry helptest command [command options] [arguments...] COMMANDS: - args Test stuff - env Test stuff placeholder ... subcommand Description for subcommand @@ -208,36 +279,35 @@ OPTIONS: --help, -h Show help (default: false) --version, -v Print the version (default: false)` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) + out := execQuiet("helptest --help") + test.AssertStringContains(g, out.Stdout, expected) }) }) - g.Describe("subcommand help", func() { - result := execQuiet("test subcommand --help") - + g.Describe("subcommand help output", func() { g.It("should display full help", func() { expected := `NAME: - centry test subcommand - Description for subcommand + centry helptest subcommand - Description for subcommand USAGE: Help text for sub command OPTIONS: - --help, -h Show help (default: false)` + --opt1 value, -o value Help text for opt1 (default: "footothebar") + --help, -h Show help (default: false)` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) + out := execQuiet("helptest subcommand --help") + test.AssertStringContains(g, out.Stdout, expected) }) }) - g.Describe("placeholder help", func() { - result := execQuiet("test placeholder --help") - + g.Describe("placeholder help output", func() { g.It("should display full help", func() { expected := `NAME: - centry test placeholder - ... + centry helptest placeholder - ... USAGE: - centry test placeholder command [command options] [arguments...] + centry helptest placeholder command [command options] [arguments...] COMMANDS: subcommand1 Description for placeholder subcommand1 @@ -247,86 +317,27 @@ OPTIONS: --help, -h Show help (default: false) --version, -v Print the version (default: false)` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) + out := execQuiet("helptest placeholder --help") + test.AssertStringContains(g, out.Stdout, expected) }) g.Describe("placeholder subcommand help", func() { - result := execQuiet("test placeholder subcommand1 --help") - g.It("should display full help", func() { expected := `NAME: - centry test placeholder subcommand1 - Description for placeholder subcommand1 + centry helptest placeholder subcommand1 - Description for placeholder subcommand1 USAGE: Help text for placeholder subcommand1 OPTIONS: - --help, -h Show help (default: false)` - - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue("\n\nEXPECTED:\n\n", expected, "\n\nTO BE FOUND IN:\n\n", result.Stdout) - }) - }) - }) - }) - - g.Describe("global options", func() { - g.Describe("version", func() { - g.Describe("--version", func() { - result := execQuiet("--version") - - g.It("should display version", func() { - expected := `1.0.0` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() - }) - }) - - g.Describe("-v", func() { - result := execQuiet("-v") + --opt1 value Help text for opt1 + --help, -h Show help (default: false)` - g.It("should display version", func() { - expected := `1.0.0` - g.Assert(strings.Contains(result.Stdout, expected)).IsTrue() + out := execQuiet("helptest placeholder subcommand1 --help") + test.AssertStringContains(g, out.Stdout, expected) }) }) }) - - g.Describe("quiet", func() { - g.Describe("--quiet", func() { - result := execWithLogging("--quiet") - - g.It("should disable logging", func() { - expected := `Changing loglevel to panic (from debug)` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue(result.Stderr) - }) - }) - - g.Describe("-q", func() { - result := execWithLogging("-q") - - g.It("should disable logging", func() { - expected := `Changing loglevel to panic (from debug)` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue(result.Stderr) - }) - }) - }) - - g.Describe("--config.log.level=info", func() { - result := execWithLogging("--config.log.level=info") - - g.It("should change log level to info", func() { - expected := `Changing loglevel to info (from debug)` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue(result.Stderr) - }) - }) - - g.Describe("--config.log.level=error", func() { - result := execWithLogging("--config.log.level=error") - - g.It("should change log level to error", func() { - expected := `Changing loglevel to error (from debug)` - g.Assert(strings.Contains(result.Stderr, expected)).IsTrue(result.Stderr) - }) - }) }) } @@ -355,7 +366,7 @@ func execCentry(source string, quiet bool) *execResult { source = fmt.Sprintf("--quiet %s", source) } context := NewContext(CLI, io.Headless()) - runtime, err := NewRuntime(strings.Split(fmt.Sprintf("test/data/main_test.yaml %s", source), " "), context) + runtime, err := NewRuntime(strings.Split(fmt.Sprintf("test/data/runtime_test.yaml %s", source), " "), context) if err != nil { exitCode = 1 runtimeErr = err diff --git a/internal/pkg/config/manifest_test.go b/internal/pkg/config/manifest_test.go index dd62aee..2d45a1e 100644 --- a/internal/pkg/config/manifest_test.go +++ b/internal/pkg/config/manifest_test.go @@ -15,12 +15,12 @@ func TestManifest(t *testing.T) { g.Describe("LoadManifest", func() { g.It("returns error for invalid manifest file", func() { - _, err := LoadManifest("test/data/invalid.yaml") + _, err := LoadManifest("test/data/manifest_test_invalid.yaml") g.Assert(err != nil).IsTrue("expected validation error") }) g.It("returns manifest when file is found", func() { - path := "test/data/main_test.yaml" + path := "test/data/manifest_test_valid.yaml" absPath, _ := filepath.Abs(path) basePath := filepath.Dir(absPath) diff --git a/internal/pkg/test/error.go b/internal/pkg/test/error.go new file mode 100644 index 0000000..7de0914 --- /dev/null +++ b/internal/pkg/test/error.go @@ -0,0 +1,21 @@ +package test + +import ( + "fmt" + + "github.com/franela/goblin" +) + +// AssertError asserts that error is not nil +func AssertError(g *goblin.G, e error) { + if e == nil { + g.Fail("expected an error") + } +} + +// AssertNoError asserts that error is nil +func AssertNoError(g *goblin.G, e error) { + if e != nil { + g.Fail(fmt.Sprintf("expected no error but got, %v", e)) + } +} diff --git a/internal/pkg/test/strings.go b/internal/pkg/test/strings.go index 6a7148d..81b0aa7 100644 --- a/internal/pkg/test/strings.go +++ b/internal/pkg/test/strings.go @@ -7,6 +7,28 @@ import ( "github.com/franela/goblin" ) +const tmplExpectedKey string = `expected key "%s" to be found in input + +INPUT +---------------------------------------------------- +%s +----------------------------------------------------` + +const tmplExpectedContains string = ` + +EXPECTED THIS +---------------------------------------------------- +%s +---------------------------------------------------- + +TO BE FOUND IN +---------------------------------------------------- +%s +----------------------------------------------------` + +const tmplExpectedValue string = `expected value "%s" for key "%s" but found "%s"` + +// TODO: Remove // AssertKeyValueExists asserts the given key and value is present on one of the lines given as input func AssertKeyValueExists(g *goblin.G, key, value, input string) { found := false @@ -30,3 +52,37 @@ func AssertKeyValueExists(g *goblin.G, key, value, input string) { g.Fail(fmt.Sprintf("\"%s\" key not found in input:\n\n%s", key, input)) } } + +// AssertStringHasKeyValue asserts the expected string is found in within the input +func AssertStringHasKeyValue(g *goblin.G, s, key, value string) { + found := false + lines := strings.Split(s, "\n") + for _, l := range lines { + parts := strings.Split(l, "=") + k := parts[0] + + var v string + if len(parts) > 1 { + v = parts[1] + } + + if k == key { + found = true + if v != value { + g.Fail(fmt.Sprintf(tmplExpectedValue, value, key, v)) + } + } + } + + if !found { + g.Fail(fmt.Sprintf(tmplExpectedKey, key, s)) + } +} + +// AssertStringContains asserts the expected string is found in within the input +func AssertStringContains(g *goblin.G, s, substring string) { + s = strings.TrimSpace(s) + substring = strings.TrimSpace(substring) + msg := fmt.Sprintf(tmplExpectedContains, substring, s) + g.Assert(strings.Contains(s, substring)).IsTrue(msg) +} diff --git a/test/data/commands/command_test.sh b/test/data/commands/command_test.sh new file mode 100644 index 0000000..d956c61 --- /dev/null +++ b/test/data/commands/command_test.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +commandtest() { + echo "command args ($*)" +} + +commandtest:subcommand() { + echo "subcommand args ($*)" +} + +# centry.cmd[commandtest:options:args].option[cmdstringopt]/type=string +# centry.cmd[commandtest:options:args].option[cmdboolopt]/type=bool +# centry.cmd[commandtest:options:args].option[cmdsel1]/type=select +# centry.cmd[commandtest:options:args].option[cmdsel1]/envName=CMDSELECTOPT +# centry.cmd[commandtest:options:args].option[cmdsel2]/type=select +# centry.cmd[commandtest:options:args].option[cmdsel2]/envName=CMDSELECTOPT +commandtest:options:args() { + echo "command args ($*)" +} + +# centry.cmd[commandtest:options:printenv].option[cmdstringopt]/type=string +# centry.cmd[commandtest:options:printenv].option[cmdboolopt]/type=bool +# centry.cmd[commandtest:options:printenv].option[cmdsel1]/type=select +# centry.cmd[commandtest:options:printenv].option[cmdsel1]/envName=CMDSELECTOPT +# centry.cmd[commandtest:options:printenv].option[cmdsel2]/type=select +# centry.cmd[commandtest:options:printenv].option[cmdsel2]/envName=CMDSELECTOPT +commandtest:options:printenv() { + env | sort +} diff --git a/test/data/commands/delete.sh b/test/data/commands/delete.sh deleted file mode 100644 index 27924c3..0000000 --- a/test/data/commands/delete.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -delete:db () { - echo "delete:db ($*)" -} - -delete:files () { - echo "delete:files ($*)" -} diff --git a/test/data/commands/get.sh b/test/data/commands/get.sh deleted file mode 100644 index 4e8f6a6..0000000 --- a/test/data/commands/get.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -get() { - echo "get ($*)" -} - -# centry.cmd[get:sub]/description=Description for subcommand -# centry.cmd[get:sub]/help=Help text for sub command -get:sub() { - echo "get:sub ($*)" -} diff --git a/test/data/commands/help_test.sh b/test/data/commands/help_test.sh new file mode 100644 index 0000000..47b7879 --- /dev/null +++ b/test/data/commands/help_test.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +helptest() { + echo "command args ($*)" +} + +# centry.cmd[helptest:subcommand]/description=Description for subcommand +# centry.cmd[helptest:subcommand]/help=Help text for sub command +# centry.cmd[helptest:subcommand].option[opt1]/description=Help text for opt1 +# centry.cmd[helptest:subcommand].option[opt1]/short=o +# centry.cmd[helptest:subcommand].option[opt1]/default=footothebar +helptest:subcommand() { + echo "helptest:subcommand ($*)" +} + +# centry.cmd[helptest:placeholder:subcommand1]/description=Description for placeholder subcommand1 +# centry.cmd[helptest:placeholder:subcommand1]/help=Help text for placeholder subcommand1 +# centry.cmd[helptest:placeholder:subcommand1].option[opt1]/description=Help text for opt1 +helptest:placeholder:subcommand1() { + echo "helptest:placeholder:subcommand1 ($*)" +} + +# centry.cmd[helptest:placeholder:subcommand2]/description=Description for placeholder subcommand2 +# centry.cmd[helptest:placeholder:subcommand2]/help=Help text for placeholder subcommand2 +# centry.cmd[helptest:placeholder:subcommand2].option[opt1]/description=Help text for opt1 +helptest:placeholder:subcommand2() { + echo "helptest:placeholder:subcommand2 ($*)" +} diff --git a/test/data/commands/option_test.sh b/test/data/commands/option_test.sh new file mode 100644 index 0000000..5fed10f --- /dev/null +++ b/test/data/commands/option_test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +optiontest:args() { + echo "args ($*)" +} + +optiontest:printenv() { + env | sort +} + +optiontest:noop() { + return 0 +} diff --git a/test/data/commands/postput.sh b/test/data/commands/postput.sh deleted file mode 100644 index ff68646..0000000 --- a/test/data/commands/postput.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -post() { - echo "post ($*)" -} - -put() { - echo "put ($*)" -} - -postignored() { - echo 'should be ignored' -} - -putignored() { - echo 'should be ignored' -} diff --git a/test/data/commands/script_test.sh b/test/data/commands/script_test.sh new file mode 100644 index 0000000..96a3d13 --- /dev/null +++ b/test/data/commands/script_test.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +scripttest() { + return 0 +} diff --git a/test/data/commands/test.sh b/test/data/commands/test.sh deleted file mode 100644 index d97b4e6..0000000 --- a/test/data/commands/test.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -# centry.cmd[test:args].option[cmdstringopt]/type=string -# centry.cmd[test:args].option[cmdboolopt]/type=bool -# centry.cmd[test:args].option[cmdsel1]/type=select -# centry.cmd[test:args].option[cmdsel1]/envName=CMDSELECTOPT -# centry.cmd[test:args].option[cmdsel2]/type=select -# centry.cmd[test:args].option[cmdsel2]/envName=CMDSELECTOPT -test:args() { - echo "test:args ($*)" -} - -# centry.cmd[test:env].option[cmdstringopt]/type=string -# centry.cmd[test:env].option[cmdboolopt]/type=bool -# centry.cmd[test:env].option[cmdsel1]/type=select -# centry.cmd[test:env].option[cmdsel1]/envName=CMDSELECTOPT -# centry.cmd[test:env].option[cmdsel2]/type=select -# centry.cmd[test:env].option[cmdsel2]/envName=CMDSELECTOPT -test:env() { - env | sort -} - -# centry.cmd[test:subcommand]/description=Description for subcommand -# centry.cmd[test:subcommand]/help=Help text for sub command -test:subcommand() { - echo "test:subcommand ($*)" -} - -# centry.cmd[test:placeholder:subcommand1]/description=Description for placeholder subcommand1 -# centry.cmd[test:placeholder:subcommand1]/help=Help text for placeholder subcommand1 -test:placeholder:subcommand1() { - echo "test:placeholder:subcommand1 ($*)" -} - -# centry.cmd[test:placeholder:subcommand2]/description=Description for placeholder subcommand2 -# centry.cmd[test:placeholder:subcommand2]/help=Help text for placeholder subcommand2 -test:placeholder:subcommand2() { - echo "test:placeholder:subcommand2 ($*)" -} diff --git a/test/data/main_test.yaml b/test/data/main_test.yaml deleted file mode 100644 index 09b7bd3..0000000 --- a/test/data/main_test.yaml +++ /dev/null @@ -1,53 +0,0 @@ -scripts: - - scripts/init.sh - - scripts/helpers.sh - -commands: - - name: get - path: commands/get.sh - description: Gets stuff - - - name: post - path: commands/postput.sh - description: Creates stuff - - - name: put - path: commands/postput.sh - description: Creates/Updates stuff - - - name: delete - path: commands/delete.sh - description: Deletes stuff - - - name: test - path: commands/test.sh - description: Test stuff - -options: - - name: stringopt - short: S - type: string - description: A custom option - default: foobar - - - name: boolopt - short: B - type: bool - description: A custom option - - - name: staging - type: select - env_name: CONTEXT - description: Sets the context to staging - - - name: production - type: select - env_name: CONTEXT - description: Sets the context to production - -config: - name: centry - version: 1.0.0 - log: - level: debug - prefix: '[centry] ' diff --git a/test/data/invalid.yaml b/test/data/manifest_test_invalid.yaml similarity index 100% rename from test/data/invalid.yaml rename to test/data/manifest_test_invalid.yaml diff --git a/test/data/manifest_test_valid.yaml b/test/data/manifest_test_valid.yaml new file mode 100644 index 0000000..1cc771a --- /dev/null +++ b/test/data/manifest_test_valid.yaml @@ -0,0 +1,37 @@ +scripts: + - scripts/init.sh + +commands: + - name: get + path: commands/get.sh + description: Gets stuff + help: Help get stuff + +options: + - name: stringopt + short: S + type: string + description: A custom option + default: foobar + + - name: boolopt + short: B + type: bool + description: A custom option + + - name: selectopt1 + type: select + env_name: SELECTOPT + description: Sets the selection to option 1 + + - name: selectopt2 + type: select + env_name: SELECTOPT + description: Sets the selection to option 2 + +config: + name: centry + version: 1.0.0 + log: + level: debug + prefix: '[centry] ' diff --git a/test/data/runtime_test.yaml b/test/data/runtime_test.yaml new file mode 100644 index 0000000..d09f771 --- /dev/null +++ b/test/data/runtime_test.yaml @@ -0,0 +1,49 @@ +scripts: + - scripts/init.sh + - scripts/helpers.sh + +commands: + - name: scripttest + path: commands/script_test.sh + description: Script tests + + - name: commandtest + path: commands/command_test.sh + description: Command tests + + - name: optiontest + path: commands/option_test.sh + description: Option tests + + - name: helptest + path: commands/help_test.sh + description: Help tests + +options: + - name: stringopt + short: S + type: string + description: A custom option + default: foobar + + - name: boolopt + short: B + type: bool + description: A custom option + + - name: selectopt1 + type: select + env_name: SELECTOPT + description: Sets the selection to option 1 + + - name: selectopt2 + type: select + env_name: SELECTOPT + description: Sets the selection to option 2 + +config: + name: centry + version: 1.0.0 + log: + level: debug + prefix: '[centry] ' From b542c86bb768fc03610985a28f7e00414536c251 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Thu, 9 Apr 2020 19:17:18 +0200 Subject: [PATCH 18/19] - Refactored runtime for better error handling with exit codes. --- cmd/centry/runtime.go | 120 +++++++++++++++++++++++++---- cmd/centry/runtime_test.go | 20 ++++- cmd/centry/script.go | 6 +- go.mod | 1 + go.sum | 2 + internal/pkg/test/output.go | 20 ++++- internal/pkg/test/strings.go | 25 ------ test/data/commands/command_test.sh | 4 + 8 files changed, 151 insertions(+), 47 deletions(-) diff --git a/cmd/centry/runtime.go b/cmd/centry/runtime.go index 233b64e..ef63d05 100644 --- a/cmd/centry/runtime.go +++ b/cmd/centry/runtime.go @@ -3,9 +3,12 @@ package main import ( "github.com/kristofferahl/go-centry/internal/pkg/config" "github.com/kristofferahl/go-centry/internal/pkg/log" + "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) +const metadataExitCode string = "exitcode" + // Runtime defines the runtime type Runtime struct { cli *cli.App @@ -66,20 +69,13 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { ErrWriter: context.io.Stderr, Before: func(c *cli.Context) error { - // Override the current log level from options - logLevel := c.String("config.log.level") - if c.Bool("quiet") { - logLevel = "panic" - } - context.log.TrySetLogLevel(logLevel) - - // Print runtime events - logger := context.log.GetLogger() - for _, e := range runtime.events { - logger.Debugf(e) - } - - return nil + return handleBefore(runtime, c) + }, + CommandNotFound: func(c *cli.Context, command string) { + handleCommandNotFound(runtime, c, command) + }, + ExitErrHandler: func(c *cli.Context, err error) { + handleExitErr(runtime, c, err) }, } @@ -102,9 +98,101 @@ func (runtime *Runtime) Execute() int { // Run cli err := runtime.cli.Run(args) if err != nil { - logger := runtime.context.log.GetLogger() - logger.Error(err) + runtime.context.log.GetLogger().Error(err) + } + + // Return exitcode defined in metadata + if runtime.cli.Metadata[metadataExitCode] != nil { + switch runtime.cli.Metadata[metadataExitCode].(type) { + case int: + return runtime.cli.Metadata[metadataExitCode].(int) + } + return 128 } return 0 } + +func handleBefore(runtime *Runtime, c *cli.Context) error { + // Override the current log level from options + logLevel := c.String("config.log.level") + if c.Bool("quiet") { + logLevel = "panic" + } + runtime.context.log.TrySetLogLevel(logLevel) + + // Print runtime events + logger := runtime.context.log.GetLogger() + for _, e := range runtime.events { + logger.Debugf(e) + } + + return nil +} + +func handleCommandNotFound(runtime *Runtime, c *cli.Context, command string) { + logger := runtime.context.log.GetLogger() + logger.WithFields(logrus.Fields{ + "command": command, + }).Warnf("Command not found!") + c.App.Metadata[metadataExitCode] = 127 +} + +// Handles errors implementing ExitCoder by printing their +// message and calling OsExiter with the given exit code. +// If the given error instead implements MultiError, each error will be checked +// for the ExitCoder interface, and OsExiter will be called with the last exit +// code found, or exit code 1 if no ExitCoder is found. +func handleExitErr(runtime *Runtime, context *cli.Context, err error) { + if err == nil { + return + } + + logger := runtime.context.log.GetLogger() + + if exitErr, ok := err.(cli.ExitCoder); ok { + if err.Error() != "" { + if _, ok := exitErr.(cli.ErrorFormatter); ok { + logger.WithFields(logrus.Fields{ + "command": context.Command.Name, + "code": exitErr.ExitCode(), + }).Errorf("%+v\n", err) + } else { + logger.WithFields(logrus.Fields{ + "command": context.Command.Name, + "code": exitErr.ExitCode(), + }).Error(err) + } + } + cli.OsExiter(exitErr.ExitCode()) + return + } + + if multiErr, ok := err.(cli.MultiError); ok { + code := handleMultiError(runtime, context, multiErr) + cli.OsExiter(code) + return + } +} + +func handleMultiError(runtime *Runtime, context *cli.Context, multiErr cli.MultiError) int { + code := 1 + for _, merr := range multiErr.Errors() { + if multiErr2, ok := merr.(cli.MultiError); ok { + code = handleMultiError(runtime, context, multiErr2) + } else if merr != nil { + if exitErr, ok := merr.(cli.ExitCoder); ok { + code = exitErr.ExitCode() + runtime.context.log.GetLogger().WithFields(logrus.Fields{ + "command": context.Command.Name, + "code": code, + }).Error(merr) + } else { + runtime.context.log.GetLogger().WithFields(logrus.Fields{ + "command": context.Command.Name, + }).Error(merr) + } + } + } + return code +} diff --git a/cmd/centry/runtime_test.go b/cmd/centry/runtime_test.go index 8e5a879..2e7fa7e 100644 --- a/cmd/centry/runtime_test.go +++ b/cmd/centry/runtime_test.go @@ -38,6 +38,20 @@ func TestMain(t *testing.T) { }) g.Describe("commands", func() { + g.Describe("invoking invalid command", func() { + g.It("should exit with status code 127", func() { + out := execQuiet("commandnotdefined") + g.Assert(out.ExitCode).Equal(127) + }) + }) + + g.Describe("invoking command that exits with a status code", func() { + g.It("should exit with exit code from command", func() { + out := execQuiet("commandtest exitcode") + g.Assert(out.ExitCode).Equal(111) + }) + }) + g.Describe("invoking command", func() { g.Describe("with arguments", func() { g.It("should have arguments passed", func() { @@ -100,9 +114,9 @@ func TestMain(t *testing.T) { test.AssertStringContains(g, out.Stdout, expected) }) - // TODO: Add assertions for all default values? g.It("should have default value for environment variable set", func() { out := execQuiet("optiontest printenv") + test.AssertStringHasKeyValue(g, out.Stdout, "BOOLOPT", "false") test.AssertStringHasKeyValue(g, out.Stdout, "STRINGOPT", "foobar") }) }) @@ -375,6 +389,10 @@ func execCentry(source string, quiet bool) *execResult { } }) + if out.ExitCode > 0 { + exitCode = out.ExitCode + } + return &execResult{ Source: source, ExitCode: exitCode, diff --git a/cmd/centry/script.go b/cmd/centry/script.go index f011989..9f7f39b 100644 --- a/cmd/centry/script.go +++ b/cmd/centry/script.go @@ -45,7 +45,7 @@ func (sc *ScriptCommand) ToCLICommand() *cli.Command { Action: func(c *cli.Context) error { ec := sc.Run(c, c.Args().Slice()) if ec > 0 { - return cli.Exit("command exited with non zero exit code", ec) + return cli.Exit("Command exited with non zero exit code", ec) } return nil }, @@ -77,11 +77,11 @@ func (sc *ScriptCommand) Run(c *cli.Context, args []string) int { } } - sc.Log.Errorf("Command %v exited with error! %v", sc.Function.Name, err) + sc.Log.Debugf("Script exited with error, %v", err) return exitCode } - sc.Log.Debugf("Finished executing command %v...", sc.Function.Name) + sc.Log.Debugf("Finished executing command %s...", sc.Function.Name) return 0 } diff --git a/go.mod b/go.mod index b21a960..798691b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/kristofferahl/go-centry go 1.13 require ( + bou.ke/monkey v1.0.2 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db github.com/ghodss/yaml v1.0.0 github.com/gorilla/mux v1.7.3 diff --git a/go.sum b/go.sum index 4076e2c..361e0cd 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= +bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= diff --git a/internal/pkg/test/output.go b/internal/pkg/test/output.go index d2ef84d..0689633 100644 --- a/internal/pkg/test/output.go +++ b/internal/pkg/test/output.go @@ -4,6 +4,8 @@ import ( "bytes" "io" "os" + + "bou.ke/monkey" ) // Output represents config for capturing stdout and or stderr. @@ -14,8 +16,9 @@ type Output struct { // OutputCapture contains the result of the capture opreation. type OutputCapture struct { - Stdout string - Stderr string + Stdout string + Stderr string + ExitCode int } // CaptureOutput captures stdout and stderr. @@ -25,6 +28,13 @@ func CaptureOutput(f func()) *OutputCapture { } func (output *Output) capture(f func()) *OutputCapture { + capturedExitCode := 0 + patchedOsExit := func(exitCode int) { + capturedExitCode = exitCode + } + patch := monkey.Patch(os.Exit, patchedOsExit) + defer patch.Unpatch() + rOut, wOut, errOut := os.Pipe() if errOut != nil { panic(errOut) @@ -53,6 +63,12 @@ func (output *Output) capture(f func()) *OutputCapture { f() + if capturedExitCode > 0 { + return &OutputCapture{ + ExitCode: capturedExitCode, + } + } + wOut.Close() wErr.Close() diff --git a/internal/pkg/test/strings.go b/internal/pkg/test/strings.go index 81b0aa7..6eeb3ec 100644 --- a/internal/pkg/test/strings.go +++ b/internal/pkg/test/strings.go @@ -28,31 +28,6 @@ TO BE FOUND IN const tmplExpectedValue string = `expected value "%s" for key "%s" but found "%s"` -// TODO: Remove -// AssertKeyValueExists asserts the given key and value is present on one of the lines given as input -func AssertKeyValueExists(g *goblin.G, key, value, input string) { - found := false - lines := strings.Split(input, "\n") - for _, l := range lines { - parts := strings.Split(l, "=") - k := parts[0] - - var v string - if len(parts) > 1 { - v = parts[1] - } - - if k == key { - found = true - g.Assert(v == value).IsTrue(fmt.Sprintf("wrong expected value for key \"%s\" expected=%s actual=%s", key, value, v)) - } - } - - if !found { - g.Fail(fmt.Sprintf("\"%s\" key not found in input:\n\n%s", key, input)) - } -} - // AssertStringHasKeyValue asserts the expected string is found in within the input func AssertStringHasKeyValue(g *goblin.G, s, key, value string) { found := false diff --git a/test/data/commands/command_test.sh b/test/data/commands/command_test.sh index d956c61..0c6278e 100644 --- a/test/data/commands/command_test.sh +++ b/test/data/commands/command_test.sh @@ -27,3 +27,7 @@ commandtest:options:args() { commandtest:options:printenv() { env | sort } + +commandtest:exitcode() { + exit 111 +} From 7e0bb9810e64fbf90a7cd583ac90f7de73ba1b54 Mon Sep 17 00:00:00 2001 From: Kristoffer Ahl Date: Thu, 9 Apr 2020 20:12:56 +0200 Subject: [PATCH 19/19] - Added support for custom cli descriptions. --- cmd/centry/runtime.go | 2 +- cmd/centry/runtime_test.go | 60 +++++++++++++++++----- examples/centry/centry.yaml | 1 + internal/pkg/config/manifest.go | 7 +-- internal/pkg/config/schema.go | 8 +-- schemas/manifest.json | 1 + test/data/manifest_test_valid.yaml | 1 + test/data/runtime_test.yaml | 1 + test/data/runtime_test_default_config.yaml | 4 ++ 9 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 test/data/runtime_test_default_config.yaml diff --git a/cmd/centry/runtime.go b/cmd/centry/runtime.go index ef63d05..4903acb 100644 --- a/cmd/centry/runtime.go +++ b/cmd/centry/runtime.go @@ -55,7 +55,7 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { runtime.cli = &cli.App{ Name: context.manifest.Config.Name, HelpName: context.manifest.Config.Name, - Usage: "A tool for building declarative CLI's over bash scripts, written in go.", // TODO: Set from manifest config + Usage: context.manifest.Config.Description, UsageText: "", Version: context.manifest.Config.Version, diff --git a/cmd/centry/runtime_test.go b/cmd/centry/runtime_test.go index 2e7fa7e..82b5ab7 100644 --- a/cmd/centry/runtime_test.go +++ b/cmd/centry/runtime_test.go @@ -252,6 +252,18 @@ func TestMain(t *testing.T) { g.Describe("output", func() { out := execQuiet("") + + g.It("should display the program name", func() { + expected := `NAME: + centry` + test.AssertStringContains(g, out.Stdout, expected) + }) + + g.It("should display the program description", func() { + expected := "A manifest file used for testing purposes" + test.AssertStringContains(g, out.Stdout, expected) + }) + g.It("should display available commands", func() { expected := `COMMANDS: commandtest Command tests @@ -275,6 +287,16 @@ func TestMain(t *testing.T) { test.AssertStringContains(g, out.Stdout, expected) }) + + g.Describe("default config output", func() { + g.It("should display the default program description", func() { + expected := `NAME: + name - A new cli application` + out := execQuiet("", "test/data/runtime_test_default_config.yaml") + test.AssertNoError(g, out.Error) + test.AssertStringContains(g, out.Stdout, expected) + }) + }) }) g.Describe("command help output", func() { @@ -334,10 +356,11 @@ OPTIONS: out := execQuiet("helptest placeholder --help") test.AssertStringContains(g, out.Stdout, expected) }) + }) - g.Describe("placeholder subcommand help", func() { - g.It("should display full help", func() { - expected := `NAME: + g.Describe("placeholder subcommand help output", func() { + g.It("should display full help", func() { + expected := `NAME: centry helptest placeholder subcommand1 - Description for placeholder subcommand1 USAGE: @@ -347,14 +370,15 @@ OPTIONS: --opt1 value Help text for opt1 --help, -h Show help (default: false)` - out := execQuiet("helptest placeholder subcommand1 --help") - test.AssertStringContains(g, out.Stdout, expected) - }) + out := execQuiet("helptest placeholder subcommand1 --help") + test.AssertStringContains(g, out.Stdout, expected) }) }) }) } +const defaultManifestPath string = "test/data/runtime_test.yaml" + type execResult struct { Source string ExitCode int @@ -363,15 +387,27 @@ type execResult struct { Stderr string } -func execQuiet(source string) *execResult { - return execCentry(source, true) +func execQuiet(source string, params ...string) *execResult { + manifestPath := defaultManifestPath + if len(params) > 0 { + if params[0] != "" { + manifestPath = params[0] + } + } + return execCentry(source, true, manifestPath) } -func execWithLogging(source string) *execResult { - return execCentry(source, false) +func execWithLogging(source string, params ...string) *execResult { + manifestPath := defaultManifestPath + if len(params) > 0 { + if params[0] != "" { + manifestPath = params[0] + } + } + return execCentry(source, false, manifestPath) } -func execCentry(source string, quiet bool) *execResult { +func execCentry(source string, quiet bool, manifestPath string) *execResult { var exitCode int var runtimeErr error @@ -380,7 +416,7 @@ func execCentry(source string, quiet bool) *execResult { source = fmt.Sprintf("--quiet %s", source) } context := NewContext(CLI, io.Headless()) - runtime, err := NewRuntime(strings.Split(fmt.Sprintf("test/data/runtime_test.yaml %s", source), " "), context) + runtime, err := NewRuntime(strings.Split(fmt.Sprintf("%s %s", manifestPath, source), " "), context) if err != nil { exitCode = 1 runtimeErr = err diff --git a/examples/centry/centry.yaml b/examples/centry/centry.yaml index 1d1604d..d0eec04 100644 --- a/examples/centry/centry.yaml +++ b/examples/centry/centry.yaml @@ -72,6 +72,7 @@ options: config: name: centry + description: A tool for building declarative CLI's over bash scripts, written in go version: 1.0.0 log: level: info diff --git a/internal/pkg/config/manifest.go b/internal/pkg/config/manifest.go index 2f4b2b0..887f992 100644 --- a/internal/pkg/config/manifest.go +++ b/internal/pkg/config/manifest.go @@ -54,9 +54,10 @@ func (o Option) Annotation(namespace, key string) (*Annotation, error) { // Config defines the structure for the configuration section type Config struct { - Name string `yaml:"name,omitempty"` - Version string `yaml:"version,omitempty"` - Log LogConfig `yaml:"log,omitempty"` + Name string `yaml:"name,omitempty"` + Description string `yaml:"description,omitempty"` + Version string `yaml:"version,omitempty"` + Log LogConfig `yaml:"log,omitempty"` } // LogConfig defines the structure for log configuration section diff --git a/internal/pkg/config/schema.go b/internal/pkg/config/schema.go index c15375f..2853311 100644 --- a/internal/pkg/config/schema.go +++ b/internal/pkg/config/schema.go @@ -1,6 +1,6 @@ // Code generated by go-bindata. DO NOT EDIT. // sources: -// schemas/manifest.json (1.841kB) +// schemas/manifest.json (1.902kB) package config @@ -69,7 +69,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _schemasManifestJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x95\x51\x6e\xdb\x30\x0c\x86\xdf\x7d\x0a\x41\xdb\xa3\x1b\x63\xaf\x3e\xc3\x6e\x30\x04\x83\x22\xd3\xb6\x3a\x8b\xf4\x28\x39\x6b\x50\xe4\xee\x83\x64\x25\xd3\xdc\x78\xcb\x1c\xac\x4f\x6a\x29\x7e\x14\xc9\xff\x4f\xf2\x5a\x08\x21\x3f\x3a\xdd\x83\x55\xb2\x16\xb2\xf7\x7e\xac\xab\xea\xd9\x11\x3e\xcd\xd1\x1d\x71\x57\xcd\x7f\x7e\x90\x65\x4c\x37\xcd\x25\xd5\xd5\x55\xd5\x19\xdf\x4f\x87\x9d\x26\x5b\x7d\x63\xe3\x3c\xb5\x2d\xb0\xea\x87\xaa\xa3\x27\x0d\xe8\xf9\x94\x70\x57\x59\x85\xa6\x05\xe7\x77\xa1\xbe\x2c\x8b\x50\xcd\x9f\x46\x08\xe5\xe8\xf0\x0c\xda\xcf\x2f\x8c\x4c\x23\xb0\x37\xe0\x64\x2d\x42\x8b\x42\x48\xa7\xd9\x8c\xfe\x57\x20\x43\x15\xb3\x3a\x45\x32\x86\x8d\x07\x9b\xe7\x65\x99\xce\xb3\xc1\x4e\xa6\x8b\x73\x3c\xcf\x33\x28\x35\x59\xab\xb0\x79\xf0\x85\x6c\x8c\x74\x73\x63\x98\x74\x83\xca\x06\xe6\xf5\x4d\x7f\xa5\x90\xd6\xe0\x67\xc0\xce\xf7\xb2\x16\x9f\x2e\x3d\x5e\x2a\xaa\x18\xfe\x67\xae\x87\x61\xdc\xc2\x35\x30\x2f\xdf\x10\x6e\xc1\x15\x22\x79\x15\x68\xf7\x3b\x9e\x76\x95\x74\xc8\xb4\x88\x18\xc3\xf7\xc9\x30\x04\xaf\x7d\x49\xbb\x2a\xd3\xec\x62\x7f\x53\x41\x1a\xaf\x8f\xbc\x8f\x80\x89\xb9\xb9\x11\xc0\xc9\x86\xd6\xb3\xd0\x81\x68\x08\xa7\x83\x21\x3c\xb1\x5f\xec\x69\xab\x1d\x5c\x4f\xec\xef\x02\xc3\xff\xea\x65\xb5\x10\xe0\xf1\xeb\xd6\x26\x1e\xf4\x48\x03\xad\x9a\x86\xfb\xa6\xf8\x1f\xf6\x8a\x4c\x99\x34\x58\xb1\x97\x26\x6c\x4d\x77\xcb\x5d\x0b\xcf\xac\x39\x66\x9b\xc2\xf2\x08\xec\xb6\xac\x55\x0e\xd4\xad\x38\xf6\xad\xcb\xff\xe4\xf3\x50\x0a\x8e\x30\xfc\xd5\xea\x0d\x1c\xa6\x18\x31\xd8\x52\x38\x7f\x28\xc6\x98\xc1\x4c\x3c\x7f\x7c\xd1\xe8\xa5\xf3\xe3\xdb\xd0\x9a\x97\x3b\x47\xcc\xd0\x4c\xdb\x62\xa1\xf1\xca\x17\xc8\x65\x9b\x49\xe4\x80\x45\x64\x91\x7e\xfd\x35\x28\xaf\xc2\x8b\x7d\x71\x2e\x7e\x06\x00\x00\xff\xff\x4a\x7c\xb4\xc0\x31\x07\x00\x00") +var _schemasManifestJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x95\x5d\x92\xd3\x30\x0c\xc7\xdf\x73\x0a\x8f\xe1\x31\xdb\x0c\xaf\x39\x03\x37\x60\x3a\x8c\xeb\x28\x89\x97\x58\x0a\xb2\x53\xb6\xb3\xd3\xbb\x33\x76\xdc\x62\xb2\x0d\x94\x74\xd8\x27\xb7\xb2\x7e\xb2\x3e\xfe\x6a\x5f\x0b\x21\xe4\x47\xa7\x7b\xb0\x4a\xd6\x42\xf6\xde\x8f\x75\x55\x3d\x3b\xc2\xa7\xd9\xba\x23\xee\xaa\xf9\xe3\x07\x59\x46\x77\xd3\x5c\x5c\x5d\x5d\x55\x9d\xf1\xfd\x74\xd8\x69\xb2\xd5\x37\x36\xce\x53\xdb\x02\xab\x7e\xa8\x3a\x7a\xd2\x80\x9e\x4f\x09\x77\x95\x55\x68\x5a\x70\x7e\x17\xe2\xcb\xb2\x08\xd1\xfc\x69\x84\x10\x8e\x0e\xcf\xa0\xfd\xfc\xc2\xc8\x34\x02\x7b\x03\x4e\xd6\x22\xa4\x28\x84\x74\x9a\xcd\xe8\x7f\x19\x32\x54\x31\xab\x53\x24\xa3\xd9\x78\xb0\xb9\x5f\xe6\xe9\x3c\x1b\xec\x64\xba\x38\xc7\xf3\x3c\x83\x52\x93\xb5\x0a\x9b\x07\x5f\xc8\xca\x48\x37\x37\x8a\x49\x37\xa8\x6c\x60\x5e\xdf\xe4\x57\x0a\x69\x0d\x7e\x06\xec\x7c\x2f\x6b\xf1\xe9\x92\xe3\x25\xa2\x8a\xe6\x7f\xe6\x7a\x18\xc6\x2d\x5c\x03\x73\xf3\x0d\xe1\x16\x5c\x21\x92\x57\x81\x76\xbf\xe3\xa9\x57\x69\x0e\xd9\x2c\x22\xc6\xf0\x7d\x32\x0c\x41\x6b\x5f\x52\xaf\xca\x54\xbb\xd8\xdf\x9c\x20\x8d\xd7\x47\xde\x67\x80\x89\xb9\xd9\x11\xc0\xc9\x86\xd4\x33\xd3\x81\x68\x08\xa7\x83\x21\x3c\xb1\x5f\xf4\x69\xab\x1c\x5c\x4f\xec\xef\x02\xc3\x77\xf5\xb2\x1a\x08\xf0\xf8\x75\x6b\x12\x0f\x6a\xa4\x81\x56\x4d\xc3\x7d\x55\xfc\x0f\x79\x45\xa6\x4c\x33\x58\x91\x97\x26\x6c\x4d\x77\x4b\x5d\x0b\xcd\xac\x29\x66\xdb\x84\x1f\x6a\xad\x3c\x02\xbb\x4d\xe0\x40\xdd\x8a\xdc\xdf\xae\xc8\x9f\x96\x24\x84\x82\x23\x0c\x7f\xdd\x93\x06\x0e\x53\xb4\x18\x6c\x29\x9c\x3f\x14\x63\xf4\x60\x26\x9e\x77\x1f\x8d\x5e\xae\x4d\x7c\x1b\x5a\xf3\x72\x67\x89\x19\x9a\x09\xa3\x58\x08\x64\xe5\xd7\xe7\xd2\xcd\xa4\x90\x80\x45\x64\xe1\x7e\xfd\x2b\x29\xaf\xaa\x11\xfb\xe2\x5c\xfc\x0c\x00\x00\xff\xff\xb3\x48\x6d\x8e\x6e\x07\x00\x00") func schemasManifestJsonBytes() ([]byte, error) { return bindataRead( @@ -84,8 +84,8 @@ func schemasManifestJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "schemas/manifest.json", size: 1841, mode: os.FileMode(0644), modTime: time.Unix(1580210584, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x15, 0xc6, 0x93, 0xc2, 0xc1, 0x9b, 0xe4, 0xde, 0xe2, 0x9d, 0xac, 0xaf, 0x15, 0x52, 0x34, 0xc1, 0x2b, 0x3d, 0xf1, 0x55, 0x31, 0x1d, 0x49, 0xcc, 0x34, 0x69, 0xcd, 0xd8, 0x21, 0x24, 0x73, 0xb7}} + info := bindataFileInfo{name: "schemas/manifest.json", size: 1902, mode: os.FileMode(0644), modTime: time.Unix(1586454269, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x80, 0x91, 0x57, 0x15, 0x23, 0x1b, 0x55, 0x70, 0x27, 0x3, 0xfc, 0xf7, 0xa2, 0xc4, 0x2d, 0x9, 0x58, 0x71, 0xea, 0x44, 0x6b, 0xef, 0x15, 0xa, 0xf3, 0x2b, 0x9e, 0x5c, 0x2d, 0x9e, 0x51, 0x6c}} return a, nil } diff --git a/schemas/manifest.json b/schemas/manifest.json index e1fd064..ab89c64 100644 --- a/schemas/manifest.json +++ b/schemas/manifest.json @@ -44,6 +44,7 @@ "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 }, "version": { "type": "string", "minLength": 1 }, "log": { "type": "object", diff --git a/test/data/manifest_test_valid.yaml b/test/data/manifest_test_valid.yaml index 1cc771a..1db8fd0 100644 --- a/test/data/manifest_test_valid.yaml +++ b/test/data/manifest_test_valid.yaml @@ -31,6 +31,7 @@ options: config: name: centry + description: A description from manifest file version: 1.0.0 log: level: debug diff --git a/test/data/runtime_test.yaml b/test/data/runtime_test.yaml index d09f771..abc9c96 100644 --- a/test/data/runtime_test.yaml +++ b/test/data/runtime_test.yaml @@ -43,6 +43,7 @@ options: config: name: centry + description: A manifest file used for testing purposes version: 1.0.0 log: level: debug diff --git a/test/data/runtime_test_default_config.yaml b/test/data/runtime_test_default_config.yaml new file mode 100644 index 0000000..d30c194 --- /dev/null +++ b/test/data/runtime_test_default_config.yaml @@ -0,0 +1,4 @@ +commands: [] +config: + name: name + version: version