From db919206448322a6afee7346dfe53c52a6405abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=B6nthal?= Date: Mon, 6 Nov 2023 20:37:36 +0100 Subject: [PATCH] taint command, simplify error handling --- README.md | 8 +++++--- cmd/apply.go | 14 ++++++++----- cmd/destroy.go | 8 ++------ cmd/import.go | 8 ++------ cmd/init.go | 8 ++------ cmd/plan.go | 9 ++++---- cmd/remove.go | 8 ++------ cmd/root.go | 1 + cmd/root_test.go | 15 ++++++++++++-- cmd/taint.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ cmd/untaint.go | 8 ++------ lib/terraform.go | 20 +++++++++++++----- 12 files changed, 110 insertions(+), 50 deletions(-) create mode 100644 cmd/taint.go diff --git a/README.md b/README.md index 3058072..855222f 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ download the binary matching your OS from [here](https://github.com/terrarium-tf ## Command ``` -$ ./terrarium +$ terrarium Builds Terraform Commands, easing these steps: * collects defined var-files * switches to the given workspace (can create new one) @@ -55,7 +55,7 @@ Usage: terrarium [command] Examples: -terrarium [command] workspace path/to/stack -v -t echo +terrarium plan production path/to/stack -v Available Commands: apply Apply a given Terraform Stack @@ -66,6 +66,8 @@ Available Commands: init initializes a stack with optional remote state plan Creates a diff between remote and local state and prints the upcoming changes remove Removes a remote resource from the terraform state + taint Taints a given Terraform Resource from a State + untaint Untaints a given Terraform Resource from a State Flags: -h, --help help for terrarium @@ -244,7 +246,7 @@ $ go run main.go To build and distribute the binary: ```shell script -$ goreleaser build --snapshot --rm-dist +$ goreleaser build --snapshot --clean $ cp ./dist/terrarium_xxx/terrarium /usr/local/bin/terrarium $ chmod a+x /usr/local/bin/terrarium ``` diff --git a/cmd/apply.go b/cmd/apply.go index 4dea20b..4f99443 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -41,7 +41,7 @@ func NewApplyCommand(root *cobra.Command) { Long: `Creates a plan file (which might be uploaded to CI-Artifacts for auditing) and applies this exact plan file.`, Args: lib.ArgsValidator, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { tf, ctx, files, _ := lib.Executor(*cmd, args[0], args[1], true) planFile := fmt.Sprintf("%s-%s.tfplan", strings.Replace(time.Now().Format(time.RFC3339), ":", "-", -1), args[0]) @@ -51,17 +51,21 @@ func NewApplyCommand(root *cobra.Command) { _, err := tf.Plan(ctx, buildPlanOptions(files, args, planFile)...) if err != nil { - os.Exit(1) + return err } + //apply err = tf.Apply(ctx, tfexec.DirOrPlan(planFile)) if err != nil { - os.Exit(1) + return err } - if os.Getenv("TF_IN_AUTOMATION") == "" { - _ = os.Remove(planFile) + // if we are not in automation remove the maybe existing planfile + if _, err := os.Stat(planFile); err == nil && os.Getenv("TF_IN_AUTOMATION") == "" { + return os.Remove(planFile) } + + return nil }, } diff --git a/cmd/destroy.go b/cmd/destroy.go index 9c79949..1b92f14 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -27,7 +27,6 @@ import ( "github.com/hashicorp/terraform-exec/tfexec" "github.com/spf13/cobra" "github.com/terrarium-tf/cli/lib" - "os" ) func NewDestroyCommand(root *cobra.Command) { @@ -36,13 +35,10 @@ func NewDestroyCommand(root *cobra.Command) { Short: "Destroy a given Terraform stack", Args: lib.ArgsValidator, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { tf, ctx, files, _ := lib.Executor(*cmd, args[0], args[1], true) - err := tf.Destroy(ctx, buildDestroyOptions(files, args)...) - if err != nil { - os.Exit(1) - } + return tf.Destroy(ctx, buildDestroyOptions(files, args)...) }, } diff --git a/cmd/import.go b/cmd/import.go index 53bef43..c6d8070 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -28,7 +28,6 @@ import ( "github.com/hashicorp/terraform-exec/tfexec" "github.com/spf13/cobra" "github.com/terrarium-tf/cli/lib" - "os" ) func NewImportCommand(root *cobra.Command) { @@ -37,13 +36,10 @@ func NewImportCommand(root *cobra.Command) { Short: "Import a remote resource into a local terraform resource", Example: "import prod path/to/stack aws_s3_bucket.example some_aws_bucket_name", Args: importArgsValidator, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { tf, ctx, files, _ := lib.Executor(*cmd, args[0], args[1], true) - err := tf.Import(ctx, args[2], args[3], buildImportOptions(files, args)...) - if err != nil { - os.Exit(1) - } + return tf.Import(ctx, args[2], args[3], buildImportOptions(files, args)...) }, } diff --git a/cmd/init.go b/cmd/init.go index 878103b..00b8d7f 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -57,14 +57,10 @@ These variables can be defined by your *.tfvars.json or through command options `, Example: "init workspace path/to/stack --state-bucket=my_own_bucket_id --state-dynamo=my_dynamo_table --state-region=us-east-1 --state-account=4711 --state-name=my_state_entry_name", Args: lib.ArgsValidator, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { tf, ctx, _, mergedVars := lib.Executor(*cmd, args[0], args[1], false) - //init - err := tf.Init(ctx, buildInitOptions(*cmd, mergedVars, args)...) - if err != nil { - os.Exit(1) - } + return tf.Init(ctx, buildInitOptions(*cmd, mergedVars, args)...) }, } diff --git a/cmd/plan.go b/cmd/plan.go index e1bda62..550efc2 100644 --- a/cmd/plan.go +++ b/cmd/plan.go @@ -37,7 +37,7 @@ func NewPlanCommand(root *cobra.Command) { Short: "Creates a diff between remote and local state and prints the upcoming changes", Args: lib.ArgsValidator, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { tf, ctx, files, _ := lib.Executor(*cmd, args[0], args[1], true) //plan @@ -47,19 +47,18 @@ func NewPlanCommand(root *cobra.Command) { } diff, err := tf.Plan(ctx, buildPlanOptions(files, args, planFile)...) + // behave exactly like terraform: /* 0 = Succeeded with empty diff (no changes) 1 = Error 2 = Succeeded with non-empty diff (changes present) */ - if err != nil { - os.Exit(1) - } - if diff { os.Exit(2) } + + return err }, } diff --git a/cmd/remove.go b/cmd/remove.go index 757effd..0115847 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -26,7 +26,6 @@ import ( "errors" "github.com/spf13/cobra" "github.com/terrarium-tf/cli/lib" - "os" ) func NewRemoveCommand(root *cobra.Command) { @@ -35,13 +34,10 @@ func NewRemoveCommand(root *cobra.Command) { Short: "Removes a remote resource from the terraform state", Example: "remove prod path/to/stack aws_s3_bucket.example", Args: removeArgsValidator, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { tf, ctx, _, _ := lib.Executor(*cmd, args[0], args[1], true) - err := tf.StateRm(ctx, args[2]) - if err != nil { - os.Exit(1) - } + return tf.StateRm(ctx, args[2]) }, } diff --git a/cmd/root.go b/cmd/root.go index 71a1306..3b23b5f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -67,4 +67,5 @@ func AddChildCommands(rootCmd *cobra.Command) { NewPlanCommand(rootCmd) NewRemoveCommand(rootCmd) NewUntaintCommand(rootCmd) + NewTaintCommand(rootCmd) } diff --git a/cmd/root_test.go b/cmd/root_test.go index 67debbb..86f0942 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -104,6 +104,17 @@ func TestInitCommandAzure(t *testing.T) { } } +func TestTaintCommand(t *testing.T) { + t.Skip("test not yet fully working due to terrafrom version checks") + args := []string{"taint", "dev", "../example/stack", "-t", "echo", "aws_s3_bucket.test"} + out := runCommand(t, args) + t.Log(out) + + if !strings.Contains(out, "taint aws_s3_bucket.test") { + t.Errorf("invalid taint command") + } +} + func TestApplyCommand(t *testing.T) { args := []string{"apply", "dev", "../example/stack", "-t", "echo"} now := strings.Replace(time.Now().Format(time.RFC3339), ":", "-", -1) @@ -124,7 +135,7 @@ func TestApplyCommand(t *testing.T) { t.Errorf("invalid plan command") } if !strings.Contains(out, fmt.Sprintf("apply -auto-approve -input=false -lock=true -parallelism=10 -refresh=true %s/%s-dev.tfplan", root, now)) { - t.Errorf("invalid apply command") + t.Errorf("invalid apply command: %s", out) } } @@ -215,7 +226,7 @@ func TestRemoveCommandWithVerbose(t *testing.T) { } func TestUntaintCommand(t *testing.T) { - t.Skip("test not yet fully working") + t.Skip("test not yet fully working due to terrafrom version checks") args := []string{"untaint", "dev", "../example/stack", "-t", "echo", "aws_s3_bucket.test"} out := runCommand(t, args) t.Log(out) diff --git a/cmd/taint.go b/cmd/taint.go new file mode 100644 index 0000000..fde30f3 --- /dev/null +++ b/cmd/taint.go @@ -0,0 +1,53 @@ +// Package cmd +/* +Copyright © 2022 Robert Schönthal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "errors" + "github.com/spf13/cobra" + "github.com/terrarium-tf/cli/lib" +) + +func NewTaintCommand(root *cobra.Command) { + var untaintCmd = &cobra.Command{ + Use: "taint workspace path/to/stack tf_resource", + Short: "Taints a given Terraform Resource from a State", + Args: taintArgsValidator, + + RunE: func(cmd *cobra.Command, args []string) error { + tf, ctx, _, _ := lib.Executor(*cmd, args[0], args[1], true) + + return tf.Taint(ctx, args[2]) + }, + } + + root.AddCommand(untaintCmd) +} + +func taintArgsValidator(cmd *cobra.Command, args []string) error { + if len(args) < 3 { + return errors.New("requires a workspace,a stack path and a tf resource") + } + + return lib.ArgsValidator(cmd, args) +} diff --git a/cmd/untaint.go b/cmd/untaint.go index ce2a6e6..3e01bc3 100644 --- a/cmd/untaint.go +++ b/cmd/untaint.go @@ -26,7 +26,6 @@ import ( "errors" "github.com/spf13/cobra" "github.com/terrarium-tf/cli/lib" - "os" ) func NewUntaintCommand(root *cobra.Command) { @@ -35,13 +34,10 @@ func NewUntaintCommand(root *cobra.Command) { Short: "Untaints a given Terraform Resource from a State", Args: untaintArgsValidator, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { tf, ctx, _, _ := lib.Executor(*cmd, args[0], args[1], true) - err := tf.Untaint(ctx, args[2]) - if err != nil { - os.Exit(1) - } + return tf.Untaint(ctx, args[2]) }, } diff --git a/lib/terraform.go b/lib/terraform.go index 9e1dc3e..a9c4096 100644 --- a/lib/terraform.go +++ b/lib/terraform.go @@ -29,14 +29,14 @@ func Executor(cmd cobra.Command, workspace string, path string, switchWorkspace binary, err := cmd.Parent().PersistentFlags().GetString("terraform") if err != nil { - log.Fatal("cant find terraform flag", err) + cmd.PrintErr("cant find terraform flag", err) } tf, err := tfexec.NewTerraform(path, binary) tf.SetColor(true) if err != nil { - log.Fatal("cant create terraform instance", err) + cmd.PrintErr(err.Error()) } tf.SetStdout(cmd.OutOrStdout()) @@ -55,9 +55,13 @@ func Executor(cmd cobra.Command, workspace string, path string, switchWorkspace func Workspace(tf *tfexec.Terraform, ctx context.Context, cmd cobra.Command, name string) { tf.SetStdout(nil) - workspaces, current, _ := tf.WorkspaceList(ctx) + workspaces, current, err := tf.WorkspaceList(ctx) tf.SetStdout(cmd.OutOrStdout()) + if err != nil { + cmd.PrintErr(err.Error()) + } + exists := false for _, ws := range workspaces { if ws == name { @@ -65,10 +69,16 @@ func Workspace(tf *tfexec.Terraform, ctx context.Context, cmd cobra.Command, nam } } if !exists { - _ = tf.WorkspaceNew(ctx, name) + err := tf.WorkspaceNew(ctx, name) + if err != nil { + cmd.PrintErr(err.Error()) + } } if current != name { - _ = tf.WorkspaceSelect(ctx, name) + err := tf.WorkspaceSelect(ctx, name) + if err != nil { + cmd.PrintErr(err.Error()) + } } }