Skip to content

Commit

Permalink
- add approval assertion for user confirmation
Browse files Browse the repository at this point in the history
- add `approval` to `goal init`
- omit for keys for yaml in `goal init`
  • Loading branch information
aaabramov committed Nov 12, 2021
1 parent a909432 commit 065cea6
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 32 deletions.
50 changes: 27 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,31 @@ Simply type `goal` to see list of available goals and their dependencies:
```shell
$ goal
Available goals:
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| GOAL | ENVIRONMENT | CLI | DESCRIPTION | ASSERTIONS |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| gcloud-ssh | dev | gcloud compute ssh dev-vm --zone=us-central1-c | SSH to dev | gcloud.project == "dev-project" |
+ +-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| | stage | gcloud compute ssh stage-vm --zone=us-central1-c | SSH to stage | gcloud.project == "stage-project" |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| helm-upgrade | dev | helm upgrade release-name -f values.yaml -f values/dev.yaml . | helm upgrade on dev | kubectl.context == "gke_project_region_dev" |
+ +-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| | stage | helm upgrade release-name -f values.yaml -f values/stage.yaml . | helm upgrade on stage | kubectl.context == "gke_project_region_stage" |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| k8s-apply | dev | kubectl apply -f deployment.yaml | kubectl apply on dev | kubectl.context == "gke_project_region_dev" |
+ +-------------+ +-----------------------------+-----------------------------------------------+
| | stage | | kubectl apply on stage | kubectl.context == "gke_project_region_stage" |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| terraform-apply | dev | terraform apply -var-file vars/dev.tfvars | Terraform apply on dev | terraform.workspace == "dev" |
+ +-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| | stage | terraform apply -var-file vars/stage.tfvars | Terraform apply on stage | terraform.workspace == "stage" |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| terraform-workspace | | terraform workspace show | Current terraform workspace | |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
| test | | go test -v ./... | Run go tests | |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+-----------------------------------------------+
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| GOAL | ENVIRONMENT | CLI | DESCRIPTION | ASSERTIONS |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| gcloud-ssh | dev | gcloud compute ssh dev-vm --zone=us-central1-c | SSH to dev | 1. gcloud.project == "dev-project" |
+ +-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| | stage | gcloud compute ssh stage-vm --zone=us-central1-c | SSH to stage | 1. gcloud.project == "stage-project" |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| helm-upgrade | dev | helm upgrade release-name -f values.yaml -f values/dev.yaml . | helm upgrade on dev | 1. kubectl.context == "gke_project_region_dev" |
+ +-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| | stage | helm upgrade release-name -f values.yaml -f values/stage.yaml . | helm upgrade on stage | 1. kubectl.context == "gke_project_region_stage" |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| k8s-apply | dev | kubectl apply -f deployment.yaml | kubectl apply on dev | 1. kubectl.context == "gke_project_region_dev" |
| | | | | 2. Manual approval |
+ +-------------+ +-----------------------------+--------------------------------------------------+
| | stage | | kubectl apply on stage | 1. kubectl.context == "gke_project_region_stage" |
| | | | | 2. Manual approval |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| terraform-apply | dev | terraform apply -var-file vars/dev.tfvars | Terraform apply on dev | 1. terraform.workspace == "dev" |
+ +-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| | stage | terraform apply -var-file vars/stage.tfvars | Terraform apply on stage | 1. terraform.workspace == "stage" |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| terraform-workspace | | terraform workspace show | Current terraform workspace | |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
| test | | go test -v ./... | Run go tests | |
+---------------------+-------------+-----------------------------------------------------------------+-----------------------------+--------------------------------------------------+
```

### Define simple local aliases
Expand Down Expand Up @@ -102,6 +104,7 @@ my-goal:
ref: my-assertion # references another goal
expect: '42'
fix: # CLI on how to fix
- approve: yes # ask user to config execution
cmd: echo
args:
- The Answer to the Ultimate Question of Life, the Universe, and Everything is 42
Expand All @@ -111,6 +114,7 @@ my-goal:

| Tool | Example |
|-----------|------------------------------------------|
| approval | [examples/kubectl](examples/kubectl) |
| kubectl | [examples/kubectl](examples/kubectl) |
| helm | [examples/helm](examples/helm) |
| terraform | [examples/terraform](examples/terraform) |
Expand Down
9 changes: 8 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ var defaultGoals = map[string]lib.YamlGoal{
Assert: []lib.YamlAssert{
{
KubectlContext: "gke_project_region_dev",
}},
},
{
Approval: "yes",
},
},
},
"stage": {
Desc: "kubectl apply on stage",
Expand All @@ -79,6 +83,9 @@ var defaultGoals = map[string]lib.YamlGoal{
{
KubectlContext: "gke_project_region_stage",
},
{
Approval: "yes",
},
},
},
},
Expand Down
23 changes: 23 additions & 0 deletions examples/kubectl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**This example combines three `goal` features:**
1. Environmental executions: `goal run apply --on dev`
2. Built-in `kubectl_context` assertion upon execution
to prevent accidental runs on wrong environment.
3. Built-in `approval` assertion to ask user to config execution.

**List available kubectl contexts with:**

```
$ kubectl config get-contexts
```

**Usage:**

```shell
$ goal run pods --on dev
$ goal run pods --on stage
```

```shell
$ goal run apply --on dev
$ goal run apply --on stage
```
12 changes: 12 additions & 0 deletions examples/simple/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
This example demonstrates simply local alias management. Goals(aliases) defined in this file would be available only in
project directory.

**Usage:**

```shell
$ goal run pods
```

```shell
$ goal run svc
```
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ require (
)

require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
)
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
Expand Down Expand Up @@ -173,6 +174,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
Expand Down Expand Up @@ -344,6 +347,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -381,6 +385,7 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
33 changes: 33 additions & 0 deletions lib/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lib
import (
"errors"
"fmt"
"github.com/manifoldco/promptui"
"strconv"
"strings"
)
Expand All @@ -17,6 +18,7 @@ var availableAssertions = []string{
"terraform_workspace",
"kubectl_context",
"gcloud_project",
"approval",
}

// === CUSTOM
Expand Down Expand Up @@ -63,6 +65,37 @@ func (a RefAssertion) check(c Goals) error {
}
}

// === INTERNAL

// ApproveAssertion asks user whether to proceed to execution
type ApproveAssertion struct{}

func (a ApproveAssertion) describe() string {
return "Manual approval"
}

func (a ApproveAssertion) check(_ Goals) error {

prompt := promptui.Select{
Label: "Proceed?",
Items: []string{"yes", "no"},
}

_, result, err := prompt.Run()

if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return err
}

if result == "yes" {
Info("✅ Proceed approved.")
return nil
} else {
return errors.New("❌ Proceed aborted")
}
}

// === TERRAFORM

// TerraformWorkspaceAssertion checks current Terraform workspace by executing `terraform workspace show`
Expand Down
15 changes: 10 additions & 5 deletions lib/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (c *Goals) Exec(name string, env string) {
if err := assert.check(*c); err != nil {
Fatal(err.Error())
}
Info("✅ Precondition: %s" + assert.describe())
Info("✅ Precondition: %s", assert.describe())
}

cmd := osexec.Command(command.Cmd, command.Args...)
Expand Down Expand Up @@ -118,8 +118,8 @@ func (c *Goals) Render() {
for _, cmd := range c.Commands {
var assertions []string

for _, assert := range cmd.Assert {
assertions = append(assertions, assert.describe())
for idx, assert := range cmd.Assert {
assertions = append(assertions, fmt.Sprintf("%d. %s", idx+1, assert.describe()))
}
table.Append([]string{cmd.Name, cmd.Env, cmd.Cli(), cmd.Desc, strings.Join(assertions, "\n")})
}
Expand Down Expand Up @@ -169,6 +169,8 @@ func mkAssertions(args []YamlAssert) (assertions []Assertion) {
assertions = append(assertions, GcloudProjectAssertion{
Expect: assertion.GcloudProject,
})
} else if assertion.Approval != "" {
assertions = append(assertions, ApproveAssertion{})
}
}
return assertions
Expand All @@ -177,11 +179,14 @@ func mkAssertions(args []YamlAssert) (assertions []Assertion) {

func validateAssert(goal string, env string, idx int, assert YamlAssert) {
var err string
if assert.Ref == "" && assert.TerraformWorkspace == "" && assert.KubectlContext == "" && assert.GcloudProject == "" {
if assert.Ref == "" && assert.TerraformWorkspace == "" && assert.KubectlContext == "" && assert.GcloudProject == "" && assert.Approval == "" {
err = fmt.Sprintf("one of [%s] must be specified for asserion", strings.Join(availableAssertions, ", "))
}
if assert.Approval != "" && assert.Approval != "yes" {
err = fmt.Sprintf("for 'approval' assertion 'yes' must be explicitly set as a value: 'approval: yes', actual: 'approval: %s'", assert.Approval)
}
if assert.Ref != "" && assert.Expect == "" {
err = "for 'ref' assertions specify expected output in 'expect'"
err = "for 'ref' assertion specify expected output in 'expect'"
}

if err == "" {
Expand Down
7 changes: 4 additions & 3 deletions lib/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package lib
import "fmt"

type YamlAssert struct {
Desc string `yaml:"desc"`
Desc string `yaml:"desc,omitempty"`
Ref string `yaml:"ref,omitempty"`
Expect string `yaml:"expect"`
Fix string `yaml:"fix"`
Expect string `yaml:"expect,omitempty"`
Fix string `yaml:"fix,omitempty"`
Approval string `yaml:"approval,omitempty"`
TerraformWorkspace string `yaml:"terraform_workspace,omitempty"`
KubectlContext string `yaml:"kubectl_context,omitempty"`
GcloudProject string `yaml:"gcloud_project,omitempty"`
Expand Down

0 comments on commit 065cea6

Please sign in to comment.