From 908cf68df3234e67b71aea35b7bedbcbcaa3feaa Mon Sep 17 00:00:00 2001 From: Khash Sajadi Date: Wed, 23 Oct 2019 18:08:41 -0700 Subject: [PATCH 1/3] samples --- samples/kubernetes/job.yml | 13 +++++++++++++ samples/kubernetes/workflow-1.yml | 4 ++++ samples/kubernetes/workflow-2.yml | 8 ++++++++ samples/kubernetes/workflow-3.yml | 11 +++++++++++ samples/kubernetes/workflow-4.yml | 13 +++++++++++++ 5 files changed, 49 insertions(+) create mode 100644 samples/kubernetes/job.yml create mode 100644 samples/kubernetes/workflow-1.yml create mode 100644 samples/kubernetes/workflow-2.yml create mode 100644 samples/kubernetes/workflow-3.yml create mode 100644 samples/kubernetes/workflow-4.yml diff --git a/samples/kubernetes/job.yml b/samples/kubernetes/job.yml new file mode 100644 index 0000000..ee1d89f --- /dev/null +++ b/samples/kubernetes/job.yml @@ -0,0 +1,13 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: pi +spec: + template: + spec: + containers: + - name: pi + image: perl + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never + backoffLimit: 4 diff --git a/samples/kubernetes/workflow-1.yml b/samples/kubernetes/workflow-1.yml new file mode 100644 index 0000000..dfa20dd --- /dev/null +++ b/samples/kubernetes/workflow-1.yml @@ -0,0 +1,4 @@ +version: 1 +steps: + - name: run + command: kubectl apply -f job.yml diff --git a/samples/kubernetes/workflow-2.yml b/samples/kubernetes/workflow-2.yml new file mode 100644 index 0000000..0fe26c8 --- /dev/null +++ b/samples/kubernetes/workflow-2.yml @@ -0,0 +1,8 @@ +version: 1 +steps: + - name: run + command: kubectl apply -f job.yml + timeout: 1m + probe: + command: kubectl wait --for=condition=complete job/pi + timeout: 1m diff --git a/samples/kubernetes/workflow-3.yml b/samples/kubernetes/workflow-3.yml new file mode 100644 index 0000000..7e950c5 --- /dev/null +++ b/samples/kubernetes/workflow-3.yml @@ -0,0 +1,11 @@ +version: 1 +steps: + - name: run + command: kubectl apply -f job.yml + probe: + command: kubectl wait --for=condition=complete job/pi + timeout: 1m + - name: cleanup + command: kubectl delete job pi + depends_on: + - run diff --git a/samples/kubernetes/workflow-4.yml b/samples/kubernetes/workflow-4.yml new file mode 100644 index 0000000..1238f34 --- /dev/null +++ b/samples/kubernetes/workflow-4.yml @@ -0,0 +1,13 @@ +version: 1 +steps: + - name: run + command: kubectl apply -f job.yml + probe: + command: kubectl wait --for=condition=complete job/pi + timeout: 1m + preflights: + - command: kubectl version + - name: cleanup + command: kubectl delete job pi + depends_on: + - run From 5def10af353d4fc13f99efd63c061adab4d54784 Mon Sep 17 00:00:00 2001 From: Khash Sajadi Date: Wed, 23 Oct 2019 18:54:19 -0700 Subject: [PATCH 2/3] ask to proceed support --- README.md | 2 +- cmd/run.go | 2 ++ samples/timeout.yml | 11 +++++++++ utils/interactive.go | 31 ++++++++++++++++++++++++ utils/spinner.go | 5 ++-- utils/step.go | 1 + utils/workflow.go | 11 ++++++++- vendor/github.com/sirupsen/logrus/go.mod | 2 ++ vendor/github.com/spf13/viper/go.mod | 2 ++ 9 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 samples/timeout.yml create mode 100644 utils/interactive.go diff --git a/README.md b/README.md index d026de8..e366a2c 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ steps: This workflow will run `kubectl apply -f manifest.yml` first. If it returns with exist status 0 (it ran successfully), will then run `kubectl wait --for=condition=complete job/myjob` until it returns with exist status 0 and considers the step successful. -Trackman can continue running if a step fails if the step has a `continue_on_failure: true`. +Trackman can continue running if a step fails if the step has a `continue_on_fail: true`. ### Timeouts diff --git a/cmd/run.go b/cmd/run.go index e97e09c..ae6a7ae 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -29,9 +29,11 @@ func init() { runCmd.Flags().StringVarP(&workflowFile, "file", "f", "", "workflow file to run") runCmd.Flags().DurationP("timeout", "", 10*time.Second, "global timeout unless overwritten by a step") runCmd.Flags().IntP("concurrency", "", runtime.NumCPU()-1, "maximum number of concurrent steps to run") + runCmd.Flags().BoolP("yes", "y", false, "Answer Yes to all confirmation questions") _ = viper.BindPFlag("timeout", runCmd.Flags().Lookup("timeout")) _ = viper.BindPFlag("concurrency", runCmd.Flags().Lookup("concurrency")) + _ = viper.BindPFlag("confirm.yes", runCmd.Flags().Lookup("yes")) rootCmd.AddCommand(runCmd) } diff --git a/samples/timeout.yml b/samples/timeout.yml new file mode 100644 index 0000000..cfb171f --- /dev/null +++ b/samples/timeout.yml @@ -0,0 +1,11 @@ +version: 1 +steps: + - name: hello + command: echo "Hello, World!" + depends_on: + - timeout + - name: timeout + ask_to_proceed: true + command: sleep 20 + timeout: 10s + continue_on_fail: true diff --git a/utils/interactive.go b/utils/interactive.go new file mode 100644 index 0000000..3fabbdc --- /dev/null +++ b/utils/interactive.go @@ -0,0 +1,31 @@ +package utils + +import ( + "bufio" + "fmt" + "log" + "os" + "strings" +) + +func confirm(s string, tries int) bool { + r := bufio.NewReader(os.Stdin) + + for ; tries > 0; tries-- { + fmt.Printf("%s [y/N]: ", s) + + res, err := r.ReadString('\n') + if err != nil { + log.Fatal(err) + } + + // Empty input (i.e. "\n") + if len(res) < 2 { + continue + } + + return strings.ToLower(strings.TrimSpace(res))[0] == 'y' + } + + return false +} diff --git a/utils/spinner.go b/utils/spinner.go index 9a078a1..13bcdcb 100644 --- a/utils/spinner.go +++ b/utils/spinner.go @@ -7,9 +7,8 @@ import ( "syscall" "time" - "github.com/kballard/go-shellquote" - "github.com/google/uuid" + "github.com/kballard/go-shellquote" "github.com/sirupsen/logrus" ) @@ -183,7 +182,7 @@ func (s *Spinner) Run(ctx context.Context) error { if cmdCtx.Err() == context.DeadlineExceeded { s.push(ctx, NewEvent(s, EventRunTimeout, nil)) - return fmt.Errorf("step %s timed out after %s", s.step.Name, s.timeout) + return fmt.Errorf("Timed out after %s", s.timeout) } if exitErr, ok := err.(*exec.ExitError); ok { diff --git a/utils/step.go b/utils/step.go index f9760ff..6eb577d 100644 --- a/utils/step.go +++ b/utils/step.go @@ -32,6 +32,7 @@ type Step struct { Probe *Probe `yaml:"probe" json:"probe"` DependsOn []string `yaml:"depends_on" json:"depends_on"` Preflights []Preflight `yaml:"preflights" json:"preflights"` + AskToProceed bool `yaml:"ask_to_proceed" json:"ask_to_proceed"` options *StepOptions workflow *Workflow diff --git a/utils/workflow.go b/utils/workflow.go index 41441d4..b17c109 100644 --- a/utils/workflow.go +++ b/utils/workflow.go @@ -8,8 +8,9 @@ import ( "sync" "time" - "github.com/hashicorp/go-multierror" + "github.com/spf13/viper" + "github.com/hashicorp/go-multierror" "github.com/sirupsen/logrus" "golang.org/x/sync/semaphore" "gopkg.in/yaml.v2" @@ -166,6 +167,14 @@ func (w *Workflow) Run(ctx context.Context) (runErrors error, stepErrors error) w.logger.WithField(FldStep, toRun.Name).Trace("Preparing to run") + if toRun.AskToProceed && !viper.GetBool("confirm.yes") { + // we need an interactive permission for this + if !confirm(fmt.Sprintf("Run %s?", toRun.Name), 1) { + w.logger.WithField(FldStep, toRun.Name).Info("Stopping execution") + w.stop(ctx) + } + } + err := toRun.Run(ctx) if err != nil { stepErrors = multierror.Append(err, stepErrors) diff --git a/vendor/github.com/sirupsen/logrus/go.mod b/vendor/github.com/sirupsen/logrus/go.mod index 8261a2b..09c5253 100644 --- a/vendor/github.com/sirupsen/logrus/go.mod +++ b/vendor/github.com/sirupsen/logrus/go.mod @@ -8,3 +8,5 @@ require ( github.com/stretchr/testify v1.2.2 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 ) + +go 1.13 diff --git a/vendor/github.com/spf13/viper/go.mod b/vendor/github.com/spf13/viper/go.mod index 86e801c..439d2d8 100644 --- a/vendor/github.com/spf13/viper/go.mod +++ b/vendor/github.com/spf13/viper/go.mod @@ -22,3 +22,5 @@ require ( golang.org/x/text v0.3.0 // indirect gopkg.in/yaml.v2 v2.2.2 ) + +go 1.13 From d0a6b61410dd193d1ff522f6fdf192a04d4f8711 Mon Sep 17 00:00:00 2001 From: Khash Sajadi Date: Thu, 24 Oct 2019 11:06:37 -0700 Subject: [PATCH 3/3] ask to proceed + dump command features + docs --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 2 +- samples/timeout.yml | 1 + utils/spinner.go | 1 + utils/step.go | 1 + utils/workflow.go | 7 +++++-- 6 files changed, 58 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e366a2c..ff03ca6 100644 --- a/README.md +++ b/README.md @@ -139,8 +139,45 @@ steps: message: "Oh nose!" ``` +## Workflow Attributes + +The following attributes can be set for the workflow: +| Attribute | Description | Default | +|---|---|---| +| version | Workflow format version | `1` | +| version | Any metadata for the workflow | None | +| steps | List of all workflow steps (See below) | [] | + +## Step Attributes + +The following attributes can be set for each step: + +| Attribute | Description | Default | +|---|---|---| +| metadata | Any metadata for the step | None | +| name | Given name for the step | `''` | +| command | Command to run, including arguments | `''` | +| continue_on_fail | Continue to the next step even after failure | `false` | +| timeout | Timeout after which the step will be stopped. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | Never | +| workdir | Work directory for the step | None | +| probe | Health probe definition. See above | None | +| depends_on | List of the steps this one depends on (should run after all of them have successfully finished) | [] | +| preflights | List of pre-flight checks (see above) | None | +| ask_to_proceed | Stops the execution of the workflow and asks the user for a confirmation to continue | `false` | +| show_command | Shows the command and arguments for this step before running it | `false` | + ## Trackman CLI +### Global Options + +The CLI supports the following global options: + +| Option | Description | Default | +|---|---|---| +| config | Config file | $HOME/.trackman.yaml | +| log-level | Log level | info | +| no-update | Don't update trackman CLI automatically | false | + ### Run Runs the given workflow. Use `--help` for more details. @@ -149,6 +186,18 @@ Runs the given workflow. Use `--help` for more details. $ trackman run -f file.yml ``` +### Params + +Run command supports the following options + +| Option | Description | Default | +|---|---|---| +| file, f | Workflow file | None | +| timeout | Timeout after which the step will be stopped. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | 10 seconds | +| concurrency | Number of concurrent steps to run | Number of CPUs - 1 | +| yes, y | Answer Yes to all `ask_to_proceed` questions | false | + + ### Update Manually checks for updates. It can also switch the current release channel. diff --git a/cmd/root.go b/cmd/root.go index 2561912..e8f7caa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,7 +27,7 @@ var ( func init() { UpdateDone = &sync.WaitGroup{} cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)") + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.trackman.yaml)") rootCmd.PersistentFlags().String("log-level", "info", "log level. Use debug to see process output") rootCmd.PersistentFlags().Bool("no-update", false, "turn off auto update") diff --git a/samples/timeout.yml b/samples/timeout.yml index cfb171f..fe2c856 100644 --- a/samples/timeout.yml +++ b/samples/timeout.yml @@ -6,6 +6,7 @@ steps: - timeout - name: timeout ask_to_proceed: true + show_command: true command: sleep 20 timeout: 10s continue_on_fail: true diff --git a/utils/spinner.go b/utils/spinner.go index 13bcdcb..6fe39a4 100644 --- a/utils/spinner.go +++ b/utils/spinner.go @@ -164,6 +164,7 @@ func (s *Spinner) Run(ctx context.Context) error { errChannel := NewLogWriter(ctx, logrus.ErrorLevel) logger.WithField(FldStep, s.Name).Tracef("Running %s with %s", s.cmd, s.args) + cmd := exec.CommandContext(cmdCtx, s.cmd, s.args...) cmd.Stderr = errChannel cmd.Stdout = outChannel diff --git a/utils/step.go b/utils/step.go index 6eb577d..802eb08 100644 --- a/utils/step.go +++ b/utils/step.go @@ -33,6 +33,7 @@ type Step struct { DependsOn []string `yaml:"depends_on" json:"depends_on"` Preflights []Preflight `yaml:"preflights" json:"preflights"` AskToProceed bool `yaml:"ask_to_proceed" json:"ask_to_proceed"` + ShowCommand bool `yaml:"show_command" json:"show_command"` options *StepOptions workflow *Workflow diff --git a/utils/workflow.go b/utils/workflow.go index b17c109..7461e45 100644 --- a/utils/workflow.go +++ b/utils/workflow.go @@ -8,10 +8,9 @@ import ( "sync" "time" - "github.com/spf13/viper" - "github.com/hashicorp/go-multierror" "github.com/sirupsen/logrus" + "github.com/spf13/viper" "golang.org/x/sync/semaphore" "gopkg.in/yaml.v2" ) @@ -167,6 +166,10 @@ func (w *Workflow) Run(ctx context.Context) (runErrors error, stepErrors error) w.logger.WithField(FldStep, toRun.Name).Trace("Preparing to run") + if toRun.ShowCommand { + w.logger.WithField(FldStep, toRun.Name).Info(toRun.Command) + } + if toRun.AskToProceed && !viper.GetBool("confirm.yes") { // we need an interactive permission for this if !confirm(fmt.Sprintf("Run %s?", toRun.Name), 1) {