Skip to content

Commit

Permalink
feat: with redone ui
Browse files Browse the repository at this point in the history
Signed-off-by: Kasper J. Hermansen <[email protected]>

feat: with no interactive instead

Signed-off-by: Kasper J. Hermansen <[email protected]>

Sets the interactive command as well as SHUTTLE_INTERACTIVE

interactive is surfaced both in cli as well as env variable

SHUTTLE_INTERACTIVE=true/default/false

default=false

Signed-off-by: Kasper J. Hermansen <[email protected]>

feat: introduce early parse flags to make sure we get root commands set before building run and ls

Signed-off-by: Kasper J. Hermansen <[email protected]>

feat: with help comment

Signed-off-by: Kasper J. Hermansen <[email protected]>

feat: fully compatible

Signed-off-by: Kasper J. Hermansen <[email protected]>

refactor: run subcommand

Signed-off-by: Kasper J. Hermansen <[email protected]>

docs: document sub run

Signed-off-by: Kasper J. Hermansen <[email protected]>

docs: more docs

Signed-off-by: Kasper J. Hermansen <[email protected]>

feat: make sure to print error properly

Signed-off-by: Kasper J. Hermansen <[email protected]>

feat: return named runCmd

Signed-off-by: Kasper J. Hermansen <[email protected]>

wip: still working on cmd help

Signed-off-by: Kasper J. Hermansen <[email protected]>

feat: fixed interactive mode

Signed-off-by: Kasper J. Hermansen <[email protected]>

feat: fix go mod

Signed-off-by: Kasper J. Hermansen <[email protected]>
  • Loading branch information
kjuulh committed Oct 20, 2023
1 parent 4ae6b67 commit 51472e1
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 58 deletions.
29 changes: 24 additions & 5 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,14 @@ If none of above is used, then the argument will expect a full plan spec.`)
return rootCmd, ctxProvider
}

func Execute(out, err io.Writer) {
rootCmd, uii := initializedRoot(out, err)
func Execute(stdout, stderr io.Writer) {
rootCmd, uii, err := initializedRoot(stdout, stderr)
if err != nil {
telemetry.TraceError(stdcontext.Background(), "init", err)
checkError(uii, err)
fmt.Printf("failed to initialize with error: %s", err)
return
}

if err := rootCmd.Execute(); err != nil {
telemetry.TraceError(
Expand All @@ -134,13 +140,22 @@ func Execute(out, err io.Writer) {
}
}

func initializedRoot(out, err io.Writer) (*cobra.Command, *ui.UI) {
func initializedRootFromArgs(out, err io.Writer, args []string) (*cobra.Command, *ui.UI, error) {
uii := ui.Create(out, err)

rootCmd, ctxProvider := newRoot(uii)
rootCmd.SetOut(out)
rootCmd.SetErr(err)

// Parses falgs early such that we can build PersistentFlags on rootCmd used
// for building various subcommands in both run and ls. This is required otherwise
// Run and LS will not get closured variables from contextProvider
rootCmd.ParseFlags(args)

runCmd, initErr := newRun(uii, ctxProvider)
if initErr != nil {
return nil, nil, initErr
}
rootCmd.AddCommand(
newDocumentation(uii, ctxProvider),
newCompletion(uii),
Expand All @@ -149,15 +164,19 @@ func initializedRoot(out, err io.Writer) (*cobra.Command, *ui.UI) {
newHas(uii, ctxProvider),
newLs(uii, ctxProvider),
newPlan(uii, ctxProvider),
runCmd,
newPrepare(uii, ctxProvider),
newRun(uii, ctxProvider),
newTemplate(uii, ctxProvider),
newVersion(uii),
newConfig(uii, ctxProvider),
newTelemetry(uii),
)

return rootCmd, uii
return rootCmd, uii, nil
}

func initializedRoot(out, err io.Writer) (*cobra.Command, *ui.UI, error) {
return initializedRootFromArgs(out, err, os.Args[1:])
}

type contextProvider func() (config.ShuttleProjectContext, error)
Expand Down
193 changes: 161 additions & 32 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,191 @@ package cmd

import (
stdcontext "context"
"fmt"
"os"
"os/signal"
"sort"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/iancoleman/strcase"
"github.com/spf13/cobra"

"github.com/lunarway/shuttle/pkg/config"
"github.com/lunarway/shuttle/pkg/executors"
"github.com/lunarway/shuttle/pkg/telemetry"
"github.com/lunarway/shuttle/pkg/ui"
)

func newRun(uii *ui.UI, contextProvider contextProvider) *cobra.Command {
func newRun(uii *ui.UI, contextProvider contextProvider) (*cobra.Command, error) {
var (
flagTemplate string
validateArgs bool
flagTemplate string
validateArgs bool
interactiveArg bool
)
shuttleInteractive := os.Getenv("SHUTTLE_INTERACTIVE")
var shuttleInteractiveDefault bool
if shuttleInteractive == "true" {
shuttleInteractiveDefault = true
}

executorRegistry := executors.NewRegistry(executors.ShellExecutor, executors.TaskExecutor)

runCmd := &cobra.Command{
Use: "run [command]",
Short: "Run a plan script",
Long: `Specify which plan script to run`,
Args: cobra.MinimumNArgs(1),
SilenceUsage: true,
}

context, err := contextProvider()
if err != nil {
return nil, err
}

// For each script construct a run command specific for said script
for script, value := range context.Scripts {
runCmd.AddCommand(
newRunSubCommand(
uii,
context,
script,
value,
executorRegistry,
&interactiveArg,
validateArgs,
),
)
}

runCmd.Flags().
StringVar(&flagTemplate, "template", "", "Template string to use. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].")
runCmd.Flags().
BoolVar(&validateArgs, "validate", true, "Validate arguments against script definition in plan and exit with 1 on unknown or missing arguments")
runCmd.PersistentFlags().
BoolVar(&interactiveArg, "interactive", shuttleInteractiveDefault, "sets whether to enable ui for getting missing values via. prompt instead of failing immediadly, default is set by [SHUTTLE_INTERACTIVE=true/false]")
return runCmd, nil
}

func newRunSubCommand(
uii *ui.UI,
context config.ShuttleProjectContext,
script string,
value config.ShuttlePlanScript,
executorRegistry *executors.Registry,
interactiveArg *bool,
validateArgs bool,
) *cobra.Command {
// Args are best suited as kebab-case on the command line
argName := func(input string) string {
return strcase.ToKebab(input)
}

parseKeyValuePair := func(arg string) (string, string, bool) {
keyvaluearg := strings.Split(arg, "=")
if len(keyvaluearg) == 2 {
key := keyvaluearg[0]
value := keyvaluearg[1]

return key, value, true
}

return "", "", false
}

// Legacy key=value pairs into standard args that cobra can understand
applyLegacyArgs := func(args []string, inputArgs map[string]*string) {
for _, inputArg := range args {
key, value, ok := parseKeyValuePair(inputArg)
if ok {
inputArgs[key] = &value
}
}
}

// In case interactive is turned on and arg is missing, we ask for missing values
createPrompt := func(inputArgs map[string]*string, arg config.ShuttleScriptArgs) (string, error) {
prompt := []*survey.Question{
{
Name: argName(arg.Name),
Prompt: &survey.Input{
Message: argName(arg.Name),
Default: *inputArgs[arg.Name],
Help: arg.Description,
},
},
}
if arg.Required {
prompt[0].Validate = survey.Required
}
var output string
err := survey.Ask(prompt, &output)
if err != nil {
return "", err
}

return output, nil
}

// Decide whether to fall back on prompt or give a hard error
validateInputArgs := func(value config.ShuttlePlanScript, inputArgs map[string]*string) error {
for _, arg := range value.Args {
arg := arg

if *inputArgs[arg.Name] == "" && *interactiveArg {
output, err := createPrompt(inputArgs, arg)
if err != nil {
return err
}
if output != "" {
inputArgs[arg.Name] = &output
}

} else if *inputArgs[arg.Name] == "" && arg.Required {
return fmt.Errorf("Error: required flag(s) \"%s\" not set", argName(arg.Name))
}
}

return nil
}

// Produce a stable list of arguments
sort.Slice(value.Args, func(i, j int) bool {
return value.Args[i].Name < value.Args[j].Name
})

// Initialize a collection of variables for use in args set pr command
inputArgs := make(map[string]*string, 0)
for _, arg := range value.Args {
arg := arg
inputArgs[arg.Name] = new(string)
}

cmd := &cobra.Command{
Use: script,
Short: value.Description,
Long: value.Description,
RunE: func(cmd *cobra.Command, args []string) error {
commandName := args[0]
if *interactiveArg {
uii.Verboseln("Running using interactive mode!")
}

ctx := cmd.Context()
ctx, traceInfo, traceError, traceEnd := trace(ctx, commandName, args)
ctx, _, traceError, traceEnd := trace(ctx, script, args)
defer traceEnd()

context, err := contextProvider()
if err != nil {
traceError(err)
applyLegacyArgs(args, inputArgs)
if err := validateInputArgs(value, inputArgs); err != nil {
return err
}
traceInfo(
telemetry.WithPhase("after-plan-pull"),
telemetry.WithEntry("plan", context.Config.Plan),
)

ctx, cancel := withSignal(ctx, uii)
defer cancel()
actualArgs := make(map[string]string, len(inputArgs))
for k, v := range inputArgs {
actualArgs[k] = *v
}

err = executorRegistry.Execute(ctx, context, commandName, args[1:], validateArgs)
err := executorRegistry.Execute(ctx, context, script, actualArgs, validateArgs)
if err != nil {
traceError(err)
return err
Expand All @@ -54,24 +195,12 @@ func newRun(uii *ui.UI, contextProvider contextProvider) *cobra.Command {
return nil
},
}
for _, arg := range value.Args {
arg := arg
cmd.Flags().StringVar(inputArgs[arg.Name], argName(arg.Name), "", arg.Description)
}

runCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
scripts := cmd.Flags().Args()
if len(scripts) == 0 {
runCmd.Usage()
return
}
context, err := contextProvider()
checkError(uii, err)

err = executors.Help(context.Scripts, scripts[0], cmd.OutOrStdout(), flagTemplate)
checkError(uii, err)
})
runCmd.Flags().
StringVar(&flagTemplate, "template", "", "Template string to use. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].")
runCmd.Flags().
BoolVar(&validateArgs, "validate", true, "Validate arguments against script definition in plan and exit with 1 on unknown or missing arguments")
return runCmd
return cmd
}

// withSignal returns a copy of parent with a new Done channel. The returned
Expand Down
23 changes: 17 additions & 6 deletions cmd/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,24 @@ Make sure you are in a project using shuttle and that a 'shuttle.yaml' file is a
'foo' not supplied but is required
Script 'required_arg' accepts the following arguments:
foo (required)
`,
err: errors.New(`exit code 2 - Arguments not valid:
'foo' not supplied but is required
foo (requiredshuttle run required_arg [flags]
Script 'required_arg' accepts the following arguments:
foo (required)`),
Flags:
--foo string
-h, --help help for required_arg
Global Flags:
-c, --clean Start from clean setup
--interactive sets whether to enable ui for getting missing values via. prompt instead of failing immediadly, default is set by [SHUTTLE_INTERACTIVE=true/false]
--plan string Overload the plan used.
Specifying a local path with either an absolute path (/some/plan) or a relative path (../some/plan) to another location
for the selected plan.
Select a version of a git plan by using #branch, #sha or #tag
If none of above is used, then the argument will expect a full plan spec.
-p, --project string Project path (default ".")
--skip-pull Skip git plan pulling step
-v, --verbose Print verbose outpu)`,
err: errors.New(`EOF`),
},
{
name: "script succeeds with required argument",
Expand Down
1 change: 0 additions & 1 deletion cmd/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ func WithRunTelemetry(
) stdcontext.Context {
ctx = stdcontext.WithValue(ctx, telemetry.TelemetryCommand, commandName)
if len(args) != 0 {
// TODO: Make sure we sanitize secrets, somehow
ctx = stdcontext.WithValue(ctx, telemetry.TelemetryCommandArgs, strings.Join(args[1:], " "))
}
return ctx
Expand Down
20 changes: 16 additions & 4 deletions cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func args(s ...string) []string {
Expand All @@ -16,6 +17,7 @@ func args(s ...string) []string {
type testCase struct {
name string
input []string
initErr error
stdoutput string
erroutput string
err error
Expand All @@ -38,10 +40,13 @@ func executeTestCasesWithCustomAssertion(
stdBuf := new(bytes.Buffer)
errBuf := new(bytes.Buffer)

rootCmd, _ := initializedRoot(stdBuf, errBuf)
rootCmd, _, err := initializedRootFromArgs(stdBuf, errBuf, tc.input)
if err != nil {
require.Equal(t, tc.initErr, err)
}
rootCmd.SetArgs(tc.input)

err := rootCmd.Execute()
err = rootCmd.Execute()
if tc.err == nil {
assert.NoError(t, err)
} else {
Expand Down Expand Up @@ -77,10 +82,17 @@ func executeTestContainsCases(t *testing.T, testCases []testCase) {

stdBuf := new(bytes.Buffer)
errBuf := new(bytes.Buffer)
rootCmd, _ := initializedRoot(stdBuf, errBuf)
rootCmd, _, err := initializedRootFromArgs(stdBuf, errBuf, tc.input)
if err != nil {
if tc.initErr == nil {
require.NoError(t, err)
} else {
require.Equal(t, tc.initErr, err)
}
}
rootCmd.SetArgs(tc.input)

err := rootCmd.Execute()
err = rootCmd.Execute()
if tc.err == nil {
assert.NoError(t, err)
} else {
Expand Down
Loading

0 comments on commit 51472e1

Please sign in to comment.