From 8ff62a2480b10faf152d26ba6487b87d9af6263e Mon Sep 17 00:00:00 2001 From: Roger Welin Date: Fri, 24 Mar 2023 21:53:13 +0100 Subject: [PATCH 01/10] wip: drift detection --- internal/mock/mocksvc.go | 10 ++++++++++ pkg/client/api.go | 30 ++++++++++++++++++++++++++++++ pkg/client/types.go | 2 ++ 3 files changed, 42 insertions(+) diff --git a/internal/mock/mocksvc.go b/internal/mock/mocksvc.go index 26480f6..d15b573 100644 --- a/internal/mock/mocksvc.go +++ b/internal/mock/mocksvc.go @@ -110,3 +110,13 @@ func (m mockAPI) ListExports(ctx context.Context, params *cloudformation.ListExp func (m mockAPI) DeleteStack(ctx context.Context, params *cloudformation.DeleteStackInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) { return &cloudformation.DeleteStackOutput{}, nil } + +// DetectStackDrift returns a mocked response +func (m mockAPI) DetectStackDrift(ctx context.Context, params *cloudformation.DetectStackDriftInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DetectStackDriftOutput, error) { + return nil, nil +} + +// DescribeStackDriftDetectionStatus returns a mocked response +func (m mockAPI) DescribeStackDriftDetectionStatus(ctx context.Context, params *cloudformation.DescribeStackDriftDetectionStatusInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackDriftDetectionStatusOutput, error) { + return nil, nil +} diff --git a/pkg/client/api.go b/pkg/client/api.go index 0e9b953..28126f7 100644 --- a/pkg/client/api.go +++ b/pkg/client/api.go @@ -3,8 +3,10 @@ package client import ( "context" "errors" + "fmt" "io" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudformation" "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" "github.com/aws/smithy-go" @@ -302,3 +304,31 @@ func (c *Cfnctl) DestroyStack() error { } return nil } + +// StackDrift gives information wheter a stack has drifted or not. If in drifted status it gives the output of the drifted resources +// A stack is considered to have drifted if one or more of its resources differ from their expected template configurations +// DetectStackDrift returns a StackDriftDetectionId you can use to monitor the progress of the operation using DescribeStackDriftDetectionStatus. +// Once the drift detection operation has completed, use DescribeStackResourceDrifts to return drift information about the stack and its resources. +func (c *Cfnctl) StackDrift(stackName string) error { + input := &cloudformation.DetectStackDriftInput{ + StackName: aws.String(stackName), + } + out, err := c.Svc.DetectStackDrift(context.TODO(), input) + if err != nil { + return err + } + + statusInput := &cloudformation.DescribeStackDriftDetectionStatusInput{ + StackDriftDetectionId: out.StackDriftDetectionId, + } + + status, err := c.Svc.DescribeStackDriftDetectionStatus(context.TODO(), statusInput) + if err != nil { + return err + } + + fmt.Println(status.DetectionStatus) + + return nil + +} diff --git a/pkg/client/types.go b/pkg/client/types.go index b934f47..8d94348 100644 --- a/pkg/client/types.go +++ b/pkg/client/types.go @@ -21,6 +21,8 @@ type CloudformationAPI interface { ValidateTemplate(ctx context.Context, params *cloudformation.ValidateTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ValidateTemplateOutput, error) ListExports(ctx context.Context, params *cloudformation.ListExportsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListExportsOutput, error) DeleteStack(ctx context.Context, params *cloudformation.DeleteStackInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) + DetectStackDrift(ctx context.Context, params *cloudformation.DetectStackDriftInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DetectStackDriftOutput, error) + DescribeStackDriftDetectionStatus(ctx context.Context, params *cloudformation.DescribeStackDriftDetectionStatusInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackDriftDetectionStatusOutput, error) } // Cfnctl provides access to all actions in the programs lifecycle From 70089f35241d4608da19b574fe00e036f4007a8c Mon Sep 17 00:00:00 2001 From: Roger Welin Date: Fri, 24 Mar 2023 21:56:27 +0100 Subject: [PATCH 02/10] return type not interface --- internal/mock/mocksvc.go | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/internal/mock/mocksvc.go b/internal/mock/mocksvc.go index d15b573..adef8a4 100644 --- a/internal/mock/mocksvc.go +++ b/internal/mock/mocksvc.go @@ -7,18 +7,17 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudformation" "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" "github.com/aws/smithy-go/middleware" - "github.com/rogerwelin/cfnctl/pkg/client" ) -type mockAPI struct{} +type MockAPI struct{} // NewMockAPI returns a new instance of mockAPI -func NewMockAPI() client.CloudformationAPI { - return mockAPI{} +func NewMockAPI() MockAPI { + return MockAPI{} } // ExecuteChangeSet returns a mocked response -func (m mockAPI) ExecuteChangeSet(ctx context.Context, params *cloudformation.ExecuteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ExecuteChangeSetOutput, error) { +func (m MockAPI) ExecuteChangeSet(ctx context.Context, params *cloudformation.ExecuteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ExecuteChangeSetOutput, error) { res := middleware.Metadata{} res.Set("result", "ok") @@ -29,7 +28,7 @@ func (m mockAPI) ExecuteChangeSet(ctx context.Context, params *cloudformation.Ex } // ExecuteChangeSet returns a mocked response -func (m mockAPI) CreateChangeSet(ctx context.Context, params *cloudformation.CreateChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.CreateChangeSetOutput, error) { +func (m MockAPI) CreateChangeSet(ctx context.Context, params *cloudformation.CreateChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.CreateChangeSetOutput, error) { id := "apa" stackID := "123456" @@ -44,7 +43,7 @@ func (m mockAPI) CreateChangeSet(ctx context.Context, params *cloudformation.Cre } // DescribeChangeSet returns a mocked response -func (m mockAPI) DescribeChangeSet(ctx context.Context, params *cloudformation.DescribeChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeChangeSetOutput, error) { +func (m MockAPI) DescribeChangeSet(ctx context.Context, params *cloudformation.DescribeChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeChangeSetOutput, error) { return &cloudformation.DescribeChangeSetOutput{ ChangeSetName: params.ChangeSetName, @@ -55,7 +54,7 @@ func (m mockAPI) DescribeChangeSet(ctx context.Context, params *cloudformation.D } // DeleteChangeSet returns a mocked response -func (m mockAPI) DeleteChangeSet(ctx context.Context, params *cloudformation.DeleteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteChangeSetOutput, error) { +func (m MockAPI) DeleteChangeSet(ctx context.Context, params *cloudformation.DeleteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteChangeSetOutput, error) { res := middleware.Metadata{} res.Set("result", "ok") return &cloudformation.DeleteChangeSetOutput{ @@ -64,19 +63,19 @@ func (m mockAPI) DeleteChangeSet(ctx context.Context, params *cloudformation.Del } // DescribeStacks returns a mocked response -func (m mockAPI) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) { +func (m MockAPI) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) { return &cloudformation.DescribeStacksOutput{ // Stacks: , }, nil } // DescribeStackResources returns a mocked response -func (m mockAPI) DescribeStackResources(ctx context.Context, params *cloudformation.DescribeStackResourcesInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourcesOutput, error) { +func (m MockAPI) DescribeStackResources(ctx context.Context, params *cloudformation.DescribeStackResourcesInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourcesOutput, error) { return &cloudformation.DescribeStackResourcesOutput{}, nil } // ListChangeSets returns a mocked response -func (m mockAPI) ListChangeSets(ctx context.Context, params *cloudformation.ListChangeSetsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListChangeSetsOutput, error) { +func (m MockAPI) ListChangeSets(ctx context.Context, params *cloudformation.ListChangeSetsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListChangeSetsOutput, error) { status := types.ChangeSetSummary{Status: "CREATE_COMPLETE"} sum := []types.ChangeSetSummary{status} @@ -84,17 +83,17 @@ func (m mockAPI) ListChangeSets(ctx context.Context, params *cloudformation.List } // ListStacks returns a mocked response -func (m mockAPI) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) { +func (m MockAPI) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) { return &cloudformation.ListStacksOutput{}, nil } // ValidateTemplate returns a mocked response -func (m mockAPI) ValidateTemplate(ctx context.Context, params *cloudformation.ValidateTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ValidateTemplateOutput, error) { +func (m MockAPI) ValidateTemplate(ctx context.Context, params *cloudformation.ValidateTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ValidateTemplateOutput, error) { return &cloudformation.ValidateTemplateOutput{}, nil } // ListExports returns a mocked response -func (m mockAPI) ListExports(ctx context.Context, params *cloudformation.ListExportsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListExportsOutput, error) { +func (m MockAPI) ListExports(ctx context.Context, params *cloudformation.ListExportsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListExportsOutput, error) { return &cloudformation.ListExportsOutput{ Exports: []types.Export{ { @@ -107,16 +106,16 @@ func (m mockAPI) ListExports(ctx context.Context, params *cloudformation.ListExp } // DeleteStack returns a mocked response -func (m mockAPI) DeleteStack(ctx context.Context, params *cloudformation.DeleteStackInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) { +func (m MockAPI) DeleteStack(ctx context.Context, params *cloudformation.DeleteStackInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) { return &cloudformation.DeleteStackOutput{}, nil } // DetectStackDrift returns a mocked response -func (m mockAPI) DetectStackDrift(ctx context.Context, params *cloudformation.DetectStackDriftInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DetectStackDriftOutput, error) { +func (m MockAPI) DetectStackDrift(ctx context.Context, params *cloudformation.DetectStackDriftInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DetectStackDriftOutput, error) { return nil, nil } // DescribeStackDriftDetectionStatus returns a mocked response -func (m mockAPI) DescribeStackDriftDetectionStatus(ctx context.Context, params *cloudformation.DescribeStackDriftDetectionStatusInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackDriftDetectionStatusOutput, error) { +func (m MockAPI) DescribeStackDriftDetectionStatus(ctx context.Context, params *cloudformation.DescribeStackDriftDetectionStatusInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackDriftDetectionStatusOutput, error) { return nil, nil } From a68b9ed983938774da0423bed3d0cbe177d9ac87 Mon Sep 17 00:00:00 2001 From: Roger Welin Date: Sun, 26 Mar 2023 16:00:23 +0200 Subject: [PATCH 03/10] de-export structs in cli package --- cli/cli.go | 37 +++++++++++++++++---------------- cli/types.go | 52 +++++++++++++++++++++++------------------------ commands/plan.go | 5 +++++ pkg/client/api.go | 30 ++++++++++++++++++--------- 4 files changed, 71 insertions(+), 53 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 69b50f9..247b3c6 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -9,9 +9,12 @@ import ( "github.com/urfave/cli/v2" ) +const ( + VERSION = "0.1.0" +) + var ( - version = "0.1.0" - cmds = []string{"apply", "destroy", "plan", "validate", "version", "output", "help"} + cmds = []string{"apply", "destroy", "plan", "validate", "version", "output", "help"} ) // RunCLI runs a new instance of cfnctl @@ -23,12 +26,12 @@ func RunCLI(args []string) { app.HelpName = "cfnctl" app.EnableBashCompletion = true app.UsageText = "cfntl [global options] [args]" - app.Version = version + app.Version = VERSION app.HideVersion = true app.CommandNotFound = func(c *cli.Context, command string) { res := didyoumean.NameSuggestion(command, cmds) if res == "" { - fmt.Println("apa") // FIX + fmt.Println("") // FIX } else { fmt.Println("Cfnctl has no command named: " + command + ". Did you mean: " + res + "?") fmt.Println("\nToo see all of Cfnctl's top-level commands, run\n\tcfnctl --help") @@ -58,10 +61,10 @@ func RunCLI(args []string) { }, }, Action: func(c *cli.Context) error { - apply := Apply{ - TemplatePath: c.String("template-file"), - ParamFile: c.String("param-file"), - AutoApprove: c.Bool("auto-approve"), + apply := apply{ + templatePath: c.String("template-file"), + paramFile: c.String("param-file"), + autoApprove: c.Bool("auto-approve"), } err := apply.Run() return err @@ -82,9 +85,9 @@ func RunCLI(args []string) { }, }, Action: func(c *cli.Context) error { - plan := Plan{ - TemplatePath: c.String("template-file"), - ParamFile: c.String("param-file"), + plan := plan{ + templatePath: c.String("template-file"), + paramFile: c.String("param-file"), } err := plan.Run() return err @@ -106,9 +109,9 @@ func RunCLI(args []string) { }, }, Action: func(c *cli.Context) error { - destroy := Destroy{ - AutoApprove: c.Bool("auto-approve"), - TemplatePath: c.String("template-file"), + destroy := destroy{ + autoApprove: c.Bool("auto-approve"), + templatePath: c.String("template-file"), } err := destroy.Run() return err @@ -118,7 +121,7 @@ func RunCLI(args []string) { Name: "output", Usage: "Show all exported output values of the selected account and region", Action: func(c *cli.Context) error { - out := Output{} + out := output{} err := out.Run() return err }, @@ -134,7 +137,7 @@ func RunCLI(args []string) { }, }, Action: func(c *cli.Context) error { - v := Validate{TemplatePath: c.String("template-file")} + v := validate{templatePath: c.String("template-file")} err := v.Run() return err }, @@ -143,7 +146,7 @@ func RunCLI(args []string) { Name: "version", Usage: "Show the current Cfnctl version", Action: func(c *cli.Context) error { - v := Version{Version: version} + v := version{version: VERSION} err := v.Run() return err }, diff --git a/cli/types.go b/cli/types.go index 903f976..691acf8 100644 --- a/cli/types.go +++ b/cli/types.go @@ -10,30 +10,30 @@ import ( "github.com/rogerwelin/cfnctl/pkg/client" ) -type Validate struct { - TemplatePath string +type validate struct { + templatePath string } -type Plan struct { - TemplatePath string - ParamFile string +type plan struct { + templatePath string + paramFile string } -type Apply struct { - AutoApprove bool - TemplatePath string - ParamFile string +type apply struct { + autoApprove bool + templatePath string + paramFile string } -type Destroy struct { - AutoApprove bool - TemplatePath string +type destroy struct { + autoApprove bool + templatePath string } -type Output struct{} +type output struct{} -type Version struct { - Version string +type version struct { + version string } // Runner interface simplifies command interaction @@ -42,8 +42,8 @@ type Runner interface { } // Run executes the function receives command -func (p *Plan) Run() error { - ctl, err := commands.CommandBuilder(p.TemplatePath, p.ParamFile, false) +func (p *plan) Run() error { + ctl, err := commands.CommandBuilder(p.templatePath, p.paramFile, false) if err != nil { return err } @@ -54,9 +54,9 @@ func (p *Plan) Run() error { } // Run executes the function receives command -func (v *Validate) Run() error { +func (v *validate) Run() error { greenBold := color.New(color.Bold, color.FgHiGreen).SprintFunc() - err := commands.Validate(v.TemplatePath) + err := commands.Validate(v.templatePath) if err != nil { return err } @@ -65,8 +65,8 @@ func (v *Validate) Run() error { } // Run executes the function receives command -func (a *Apply) Run() error { - ctl, err := commands.CommandBuilder(a.TemplatePath, a.ParamFile, a.AutoApprove) +func (a *apply) Run() error { + ctl, err := commands.CommandBuilder(a.templatePath, a.paramFile, a.autoApprove) if err != nil { return err } @@ -75,8 +75,8 @@ func (a *Apply) Run() error { } // Run executes the function receives command -func (d *Destroy) Run() error { - ctl, err := commands.CommandBuilder(d.TemplatePath, "", d.AutoApprove) +func (d *destroy) Run() error { + ctl, err := commands.CommandBuilder(d.templatePath, "", d.autoApprove) if err != nil { return err } @@ -86,13 +86,13 @@ func (d *Destroy) Run() error { } // Run executes the function receives command -func (v *Version) Run() error { - err := commands.OutputVersion(v.Version, os.Stdout) +func (v *version) Run() error { + err := commands.OutputVersion(v.version, os.Stdout) return err } // Run executes the function receives command -func (o *Output) Run() error { +func (o *output) Run() error { svc, err := aws.NewAWS() if err != nil { return err diff --git a/commands/plan.go b/commands/plan.go index 347b5e3..c39f468 100644 --- a/commands/plan.go +++ b/commands/plan.go @@ -163,6 +163,11 @@ func Plan(ctl *client.Cfnctl, deleteChangeSet bool) (planChanges, error) { pc = planOutput(createEvents, ctl.Output) + err = ctl.StackDrift() + if err != nil { + return planChanges{}, err + } + // clean up changeset if deleteChangeSet { err = ctl.DeleteChangeSet() diff --git a/pkg/client/api.go b/pkg/client/api.go index 28126f7..ca00001 100644 --- a/pkg/client/api.go +++ b/pkg/client/api.go @@ -14,6 +14,7 @@ import ( ) var ErrStackNotFound = errors.New("stack does not exist") +var ErrDriftStatusNotReady = errors.New("drift status not ready") // Option is used to implement Option Pattern on the client type Option func(*Cfnctl) @@ -305,30 +306,39 @@ func (c *Cfnctl) DestroyStack() error { return nil } +func (c *Cfnctl) getDriftStatus(id *string) error { + statusInput := &cloudformation.DescribeStackDriftDetectionStatusInput{ + StackDriftDetectionId: id, + } + status, err := c.Svc.DescribeStackDriftDetectionStatus(context.TODO(), statusInput) + if err != nil { + fmt.Println(err) + return err + } + + fmt.Printf("%+v\n", status) + fmt.Println(status.DetectionStatus) + + return nil +} + // StackDrift gives information wheter a stack has drifted or not. If in drifted status it gives the output of the drifted resources // A stack is considered to have drifted if one or more of its resources differ from their expected template configurations // DetectStackDrift returns a StackDriftDetectionId you can use to monitor the progress of the operation using DescribeStackDriftDetectionStatus. // Once the drift detection operation has completed, use DescribeStackResourceDrifts to return drift information about the stack and its resources. -func (c *Cfnctl) StackDrift(stackName string) error { +func (c *Cfnctl) StackDrift() error { input := &cloudformation.DetectStackDriftInput{ - StackName: aws.String(stackName), + StackName: aws.String(c.StackName), } out, err := c.Svc.DetectStackDrift(context.TODO(), input) if err != nil { return err } - - statusInput := &cloudformation.DescribeStackDriftDetectionStatusInput{ - StackDriftDetectionId: out.StackDriftDetectionId, - } - - status, err := c.Svc.DescribeStackDriftDetectionStatus(context.TODO(), statusInput) + err = c.getDriftStatus(out.StackDriftDetectionId) if err != nil { return err } - fmt.Println(status.DetectionStatus) - return nil } From 3c2872fdbd093db2af5738d0a3dc3506a8f11f6e Mon Sep 17 00:00:00 2001 From: Roger Welin Date: Sun, 26 Mar 2023 16:10:10 +0200 Subject: [PATCH 04/10] make cli methods private --- cli/cli.go | 12 ++++++------ cli/types.go | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 247b3c6..0bc1bec 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -66,7 +66,7 @@ func RunCLI(args []string) { paramFile: c.String("param-file"), autoApprove: c.Bool("auto-approve"), } - err := apply.Run() + err := apply.run() return err }, }, @@ -89,7 +89,7 @@ func RunCLI(args []string) { templatePath: c.String("template-file"), paramFile: c.String("param-file"), } - err := plan.Run() + err := plan.run() return err }, }, @@ -113,7 +113,7 @@ func RunCLI(args []string) { autoApprove: c.Bool("auto-approve"), templatePath: c.String("template-file"), } - err := destroy.Run() + err := destroy.run() return err }, }, @@ -122,7 +122,7 @@ func RunCLI(args []string) { Usage: "Show all exported output values of the selected account and region", Action: func(c *cli.Context) error { out := output{} - err := out.Run() + err := out.run() return err }, }, @@ -138,7 +138,7 @@ func RunCLI(args []string) { }, Action: func(c *cli.Context) error { v := validate{templatePath: c.String("template-file")} - err := v.Run() + err := v.run() return err }, }, @@ -147,7 +147,7 @@ func RunCLI(args []string) { Usage: "Show the current Cfnctl version", Action: func(c *cli.Context) error { v := version{version: VERSION} - err := v.Run() + err := v.run() return err }, }, diff --git a/cli/types.go b/cli/types.go index 691acf8..52069c9 100644 --- a/cli/types.go +++ b/cli/types.go @@ -42,7 +42,7 @@ type Runner interface { } // Run executes the function receives command -func (p *plan) Run() error { +func (p *plan) run() error { ctl, err := commands.CommandBuilder(p.templatePath, p.paramFile, false) if err != nil { return err @@ -54,7 +54,7 @@ func (p *plan) Run() error { } // Run executes the function receives command -func (v *validate) Run() error { +func (v *validate) run() error { greenBold := color.New(color.Bold, color.FgHiGreen).SprintFunc() err := commands.Validate(v.templatePath) if err != nil { @@ -65,7 +65,7 @@ func (v *validate) Run() error { } // Run executes the function receives command -func (a *apply) Run() error { +func (a *apply) run() error { ctl, err := commands.CommandBuilder(a.templatePath, a.paramFile, a.autoApprove) if err != nil { return err @@ -75,7 +75,7 @@ func (a *apply) Run() error { } // Run executes the function receives command -func (d *destroy) Run() error { +func (d *destroy) run() error { ctl, err := commands.CommandBuilder(d.templatePath, "", d.autoApprove) if err != nil { return err @@ -86,13 +86,13 @@ func (d *destroy) Run() error { } // Run executes the function receives command -func (v *version) Run() error { +func (v *version) run() error { err := commands.OutputVersion(v.version, os.Stdout) return err } // Run executes the function receives command -func (o *output) Run() error { +func (o *output) run() error { svc, err := aws.NewAWS() if err != nil { return err From 0a6bdae10b5e5e436667e5864f9f86697ee24eb1 Mon Sep 17 00:00:00 2001 From: Roger Welin Date: Sun, 26 Mar 2023 16:11:54 +0200 Subject: [PATCH 05/10] refactoring --- cli/actions.go | 77 +++++++++++++++++++++++++++++++++++++++++++++++ cli/types.go | 81 -------------------------------------------------- 2 files changed, 77 insertions(+), 81 deletions(-) create mode 100644 cli/actions.go diff --git a/cli/actions.go b/cli/actions.go new file mode 100644 index 0000000..48f4582 --- /dev/null +++ b/cli/actions.go @@ -0,0 +1,77 @@ +package cli + +import ( + "fmt" + "os" + + "github.com/fatih/color" + "github.com/rogerwelin/cfnctl/aws" + "github.com/rogerwelin/cfnctl/commands" + "github.com/rogerwelin/cfnctl/pkg/client" +) + +// Run executes the function receives command +func (p *plan) run() error { + ctl, err := commands.CommandBuilder(p.templatePath, p.paramFile, false) + if err != nil { + return err + } + + _, err = commands.Plan(ctl, true) + + return err +} + +// Run executes the function receives command +func (v *validate) run() error { + greenBold := color.New(color.Bold, color.FgHiGreen).SprintFunc() + err := commands.Validate(v.templatePath) + if err != nil { + return err + } + fmt.Printf("%s The configuration is valid.\n", greenBold("Success!")) + return nil +} + +// Run executes the function receives command +func (a *apply) run() error { + ctl, err := commands.CommandBuilder(a.templatePath, a.paramFile, a.autoApprove) + if err != nil { + return err + } + err = commands.Apply(ctl) + return err +} + +// Run executes the function receives command +func (d *destroy) run() error { + ctl, err := commands.CommandBuilder(d.templatePath, "", d.autoApprove) + if err != nil { + return err + } + + err = commands.Destroy(ctl) + return err +} + +// Run executes the function receives command +func (v *version) run() error { + err := commands.OutputVersion(v.version, os.Stdout) + return err +} + +// Run executes the function receives command +func (o *output) run() error { + svc, err := aws.NewAWS() + if err != nil { + return err + } + + ctl := client.New( + client.WithSvc(svc), + client.WithOutput(os.Stdout), + ) + + err = commands.Output(ctl) + return err +} diff --git a/cli/types.go b/cli/types.go index 52069c9..0abf007 100644 --- a/cli/types.go +++ b/cli/types.go @@ -1,15 +1,5 @@ package cli -import ( - "fmt" - "os" - - "github.com/fatih/color" - "github.com/rogerwelin/cfnctl/aws" - "github.com/rogerwelin/cfnctl/commands" - "github.com/rogerwelin/cfnctl/pkg/client" -) - type validate struct { templatePath string } @@ -35,74 +25,3 @@ type output struct{} type version struct { version string } - -// Runner interface simplifies command interaction -type Runner interface { - Run() error -} - -// Run executes the function receives command -func (p *plan) run() error { - ctl, err := commands.CommandBuilder(p.templatePath, p.paramFile, false) - if err != nil { - return err - } - - _, err = commands.Plan(ctl, true) - - return err -} - -// Run executes the function receives command -func (v *validate) run() error { - greenBold := color.New(color.Bold, color.FgHiGreen).SprintFunc() - err := commands.Validate(v.templatePath) - if err != nil { - return err - } - fmt.Printf("%s The configuration is valid.\n", greenBold("Success!")) - return nil -} - -// Run executes the function receives command -func (a *apply) run() error { - ctl, err := commands.CommandBuilder(a.templatePath, a.paramFile, a.autoApprove) - if err != nil { - return err - } - err = commands.Apply(ctl) - return err -} - -// Run executes the function receives command -func (d *destroy) run() error { - ctl, err := commands.CommandBuilder(d.templatePath, "", d.autoApprove) - if err != nil { - return err - } - - err = commands.Destroy(ctl) - return err -} - -// Run executes the function receives command -func (v *version) run() error { - err := commands.OutputVersion(v.version, os.Stdout) - return err -} - -// Run executes the function receives command -func (o *output) run() error { - svc, err := aws.NewAWS() - if err != nil { - return err - } - - ctl := client.New( - client.WithSvc(svc), - client.WithOutput(os.Stdout), - ) - - err = commands.Output(ctl) - return err -} From 25acc805ae28b4dfb21548e45ba9b9fbdcb32258 Mon Sep 17 00:00:00 2001 From: Roger Welin Date: Sun, 26 Mar 2023 22:38:53 +0200 Subject: [PATCH 06/10] remove un-needed pointers --- cli/actions.go | 18 ++++++------------ cli/types.go | 4 ++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/cli/actions.go b/cli/actions.go index 48f4582..4b3bffe 100644 --- a/cli/actions.go +++ b/cli/actions.go @@ -10,8 +10,7 @@ import ( "github.com/rogerwelin/cfnctl/pkg/client" ) -// Run executes the function receives command -func (p *plan) run() error { +func (p plan) run() error { ctl, err := commands.CommandBuilder(p.templatePath, p.paramFile, false) if err != nil { return err @@ -22,8 +21,7 @@ func (p *plan) run() error { return err } -// Run executes the function receives command -func (v *validate) run() error { +func (v validate) run() error { greenBold := color.New(color.Bold, color.FgHiGreen).SprintFunc() err := commands.Validate(v.templatePath) if err != nil { @@ -33,8 +31,7 @@ func (v *validate) run() error { return nil } -// Run executes the function receives command -func (a *apply) run() error { +func (a apply) run() error { ctl, err := commands.CommandBuilder(a.templatePath, a.paramFile, a.autoApprove) if err != nil { return err @@ -43,8 +40,7 @@ func (a *apply) run() error { return err } -// Run executes the function receives command -func (d *destroy) run() error { +func (d destroy) run() error { ctl, err := commands.CommandBuilder(d.templatePath, "", d.autoApprove) if err != nil { return err @@ -54,14 +50,12 @@ func (d *destroy) run() error { return err } -// Run executes the function receives command -func (v *version) run() error { +func (v version) run() error { err := commands.OutputVersion(v.version, os.Stdout) return err } -// Run executes the function receives command -func (o *output) run() error { +func (o output) run() error { svc, err := aws.NewAWS() if err != nil { return err diff --git a/cli/types.go b/cli/types.go index 0abf007..8d0182a 100644 --- a/cli/types.go +++ b/cli/types.go @@ -25,3 +25,7 @@ type output struct{} type version struct { version string } + +type drift struct { + stackName string +} From 15de8124e9fe23f268c9e766222b857da0f80780 Mon Sep 17 00:00:00 2001 From: Roger Welin Date: Mon, 27 Mar 2023 23:32:47 +0200 Subject: [PATCH 07/10] wip: drift detection --- cli/actions.go | 50 +++++++++++++++++++++++++++++++++ cli/cli.go | 4 +++ cli/types.go | 2 +- commands/plan.go | 5 ---- commands/testdata/template.yaml | 2 ++ go.mod | 4 +++ go.sum | 18 ++++++++++-- internal/mock/mocksvc.go | 4 +++ pkg/client/api.go | 46 +++++++++++++++++------------- pkg/client/types.go | 1 + utils/jsondiff.go | 38 +++++++++++++++++++++++++ 11 files changed, 146 insertions(+), 28 deletions(-) create mode 100644 utils/jsondiff.go diff --git a/cli/actions.go b/cli/actions.go index 4b3bffe..db6b15d 100644 --- a/cli/actions.go +++ b/cli/actions.go @@ -3,11 +3,13 @@ package cli import ( "fmt" "os" + "time" "github.com/fatih/color" "github.com/rogerwelin/cfnctl/aws" "github.com/rogerwelin/cfnctl/commands" "github.com/rogerwelin/cfnctl/pkg/client" + "github.com/rogerwelin/cfnctl/utils" ) func (p plan) run() error { @@ -69,3 +71,51 @@ func (o output) run() error { err = commands.Output(ctl) return err } + +func (d drift) run() error { + svc, err := aws.NewAWS() + if err != nil { + return err + } + stackName := utils.TrimFileSuffix(d.templatePath) + + ctl := client.New( + client.WithSvc(svc), + client.WithStackName(stackName), + client.WithOutput(os.Stdout), + ) + // get drift id + id, err := ctl.StackDriftInit() + + if err != nil { + return err + } + + // poll for completion + ticker := time.NewTicker(1 * time.Second) + for range ticker.C { + status, err := ctl.GetDriftStatus(id) + if err != nil { + return err + } + if status == "DETECTION_COMPLETE" { + break + } + } + + status, err := ctl.GetStackDriftInfo() + if err != nil { + return err + } + + for _, item := range status { + if item.StackResourceDriftStatus != "IN_SYNC" { + err := utils.JsonDiff(*item.ExpectedProperties, *item.ActualProperties, ctl.Output) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/cli/cli.go b/cli/cli.go index 0bc1bec..f311e30 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -89,7 +89,11 @@ func RunCLI(args []string) { templatePath: c.String("template-file"), paramFile: c.String("param-file"), } + drift := drift{ + templatePath: c.String("template-file"), + } err := plan.run() + err = drift.run() return err }, }, diff --git a/cli/types.go b/cli/types.go index 8d0182a..8bb1fcc 100644 --- a/cli/types.go +++ b/cli/types.go @@ -27,5 +27,5 @@ type version struct { } type drift struct { - stackName string + templatePath string } diff --git a/commands/plan.go b/commands/plan.go index c39f468..347b5e3 100644 --- a/commands/plan.go +++ b/commands/plan.go @@ -163,11 +163,6 @@ func Plan(ctl *client.Cfnctl, deleteChangeSet bool) (planChanges, error) { pc = planOutput(createEvents, ctl.Output) - err = ctl.StackDrift() - if err != nil { - return planChanges{}, err - } - // clean up changeset if deleteChangeSet { err = ctl.DeleteChangeSet() diff --git a/commands/testdata/template.yaml b/commands/testdata/template.yaml index 740e683..6daaa9a 100644 --- a/commands/testdata/template.yaml +++ b/commands/testdata/template.yaml @@ -22,6 +22,8 @@ Resources: - arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess + # create iam role here + Bucket: Type: AWS::S3::Bucket diff --git a/go.mod b/go.mod index ef744aa..335a04f 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/manifoldco/promptui v0.8.0 github.com/olekukonko/tablewriter v0.0.5 github.com/urfave/cli/v2 v2.3.0 + github.com/yudai/gojsondiff v1.0.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -37,7 +38,10 @@ require ( github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b // indirect github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 // indirect + github.com/sergi/go-diff v1.3.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect + github.com/yudai/pp v2.0.1+incompatible // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index 1cdc3fa..9f43d4e 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,9 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWs github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 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.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -104,9 +105,13 @@ github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b h1:jUK33OXuZP/l6b github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b/go.mod h1:8458kAagoME2+LN5//WxE71ysZ3B7r22fdgb7qVmXSY= github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 h1:fOCp11H0yuyAt2wqlbJtbyPzSgaxHTv8uN1pMpkG1t8= github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522/go.mod h1:tQTYKOQgxoH3v6dEmdHiz4JG+nbxWwM5fgPQUpSZqVQ= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= @@ -116,6 +121,12 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56 h1:yhqBHs09SmmUoNOHc9jgK4a60T3XFRtPAkYxVnqgY50= github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -166,15 +177,18 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/mock/mocksvc.go b/internal/mock/mocksvc.go index adef8a4..6440bfb 100644 --- a/internal/mock/mocksvc.go +++ b/internal/mock/mocksvc.go @@ -119,3 +119,7 @@ func (m MockAPI) DetectStackDrift(ctx context.Context, params *cloudformation.De func (m MockAPI) DescribeStackDriftDetectionStatus(ctx context.Context, params *cloudformation.DescribeStackDriftDetectionStatusInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackDriftDetectionStatusOutput, error) { return nil, nil } + +func (m MockAPI) DescribeStackResourceDrifts(ctx context.Context, params *cloudformation.DescribeStackResourceDriftsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourceDriftsOutput, error) { + return nil, nil +} diff --git a/pkg/client/api.go b/pkg/client/api.go index ca00001..2a5662b 100644 --- a/pkg/client/api.go +++ b/pkg/client/api.go @@ -306,39 +306,45 @@ func (c *Cfnctl) DestroyStack() error { return nil } -func (c *Cfnctl) getDriftStatus(id *string) error { - statusInput := &cloudformation.DescribeStackDriftDetectionStatusInput{ +// StackDrift gives information wheter a stack has drifted or not. If in drifted status it gives the output of the drifted resources +// A stack is considered to have drifted if one or more of its resources differ from their expected template configurations +// DetectStackDrift returns a StackDriftDetectionId you can use to monitor the progress of the operation using DescribeStackDriftDetectionStatus. +// Once the drift detection operation has completed, use DescribeStackResourceDrifts to return drift information about the stack and its resources. +func (c *Cfnctl) StackDriftInit() (*string, error) { + input := &cloudformation.DetectStackDriftInput{ + StackName: aws.String(c.StackName), + } + out, err := c.Svc.DetectStackDrift(context.TODO(), input) + if err != nil { + return nil, err + } + return out.StackDriftDetectionId, nil +} + +func (c *Cfnctl) GetDriftStatus(id *string) (types.StackDriftDetectionStatus, error) { + input := &cloudformation.DescribeStackDriftDetectionStatusInput{ StackDriftDetectionId: id, } - status, err := c.Svc.DescribeStackDriftDetectionStatus(context.TODO(), statusInput) + status, err := c.Svc.DescribeStackDriftDetectionStatus(context.TODO(), input) if err != nil { fmt.Println(err) - return err + return "", err } - fmt.Printf("%+v\n", status) fmt.Println(status.DetectionStatus) - return nil + return status.DetectionStatus, nil } -// StackDrift gives information wheter a stack has drifted or not. If in drifted status it gives the output of the drifted resources -// A stack is considered to have drifted if one or more of its resources differ from their expected template configurations -// DetectStackDrift returns a StackDriftDetectionId you can use to monitor the progress of the operation using DescribeStackDriftDetectionStatus. -// Once the drift detection operation has completed, use DescribeStackResourceDrifts to return drift information about the stack and its resources. -func (c *Cfnctl) StackDrift() error { - input := &cloudformation.DetectStackDriftInput{ +func (c *Cfnctl) GetStackDriftInfo() ([]types.StackResourceDrift, error) { + input := &cloudformation.DescribeStackResourceDriftsInput{ StackName: aws.String(c.StackName), } - out, err := c.Svc.DetectStackDrift(context.TODO(), input) - if err != nil { - return err - } - err = c.getDriftStatus(out.StackDriftDetectionId) + out, err := c.Svc.DescribeStackResourceDrifts(context.TODO(), input) + if err != nil { - return err + return nil, err } - return nil - + return out.StackResourceDrifts, nil } diff --git a/pkg/client/types.go b/pkg/client/types.go index 8d94348..412108b 100644 --- a/pkg/client/types.go +++ b/pkg/client/types.go @@ -23,6 +23,7 @@ type CloudformationAPI interface { DeleteStack(ctx context.Context, params *cloudformation.DeleteStackInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) DetectStackDrift(ctx context.Context, params *cloudformation.DetectStackDriftInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DetectStackDriftOutput, error) DescribeStackDriftDetectionStatus(ctx context.Context, params *cloudformation.DescribeStackDriftDetectionStatusInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackDriftDetectionStatusOutput, error) + DescribeStackResourceDrifts(ctx context.Context, params *cloudformation.DescribeStackResourceDriftsInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourceDriftsOutput, error) } // Cfnctl provides access to all actions in the programs lifecycle diff --git a/utils/jsondiff.go b/utils/jsondiff.go new file mode 100644 index 0000000..4bbc4f2 --- /dev/null +++ b/utils/jsondiff.go @@ -0,0 +1,38 @@ +package utils + +import ( + "encoding/json" + "fmt" + "io" + + diff "github.com/yudai/gojsondiff" + "github.com/yudai/gojsondiff/formatter" +) + +func JsonDiff(expected, actual string, writer io.Writer) error { + // convert to byte slice + ex := []byte(expected) + ac := []byte(actual) + + differ := diff.New() + d, err := differ.Compare(ex, ac) + if err != nil { + return err + } + + // Output the result + var diffString string + var aJson map[string]interface{} + json.Unmarshal(ex, &aJson) + + config := formatter.AsciiFormatterConfig{ + ShowArrayIndex: true, + Coloring: true, + } + + formatter := formatter.NewAsciiFormatter(aJson, config) + diffString, err = formatter.Format(d) + fmt.Fprintln(writer, diffString) + + return nil +} From dbb2e19f8353c36f3654dfc7fdede2183872dff1 Mon Sep 17 00:00:00 2001 From: "roger.welin" Date: Fri, 19 Jan 2024 09:28:55 +0100 Subject: [PATCH 08/10] lint fixes --- utils/jsondiff.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/utils/jsondiff.go b/utils/jsondiff.go index 4bbc4f2..b436277 100644 --- a/utils/jsondiff.go +++ b/utils/jsondiff.go @@ -23,7 +23,10 @@ func JsonDiff(expected, actual string, writer io.Writer) error { // Output the result var diffString string var aJson map[string]interface{} - json.Unmarshal(ex, &aJson) + err = json.Unmarshal(ex, &aJson) + if err != nil { + return err + } config := formatter.AsciiFormatterConfig{ ShowArrayIndex: true, @@ -32,6 +35,10 @@ func JsonDiff(expected, actual string, writer io.Writer) error { formatter := formatter.NewAsciiFormatter(aJson, config) diffString, err = formatter.Format(d) + if err != nil { + return err + } + fmt.Fprintln(writer, diffString) return nil From 0eb6c01c6110e4b6aa7c9989b1444b1f640ca203 Mon Sep 17 00:00:00 2001 From: "roger.welin" Date: Fri, 19 Jan 2024 09:33:07 +0100 Subject: [PATCH 09/10] lint fixes --- cli/cli.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/cli.go b/cli/cli.go index f311e30..20f0d01 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -93,6 +93,10 @@ func RunCLI(args []string) { templatePath: c.String("template-file"), } err := plan.run() + if err != nil { + return err + } + err = drift.run() return err }, From cf251f60582a54a4ba823eecffbdf4783167ec1e Mon Sep 17 00:00:00 2001 From: "roger.welin" Date: Fri, 19 Jan 2024 09:34:32 +0100 Subject: [PATCH 10/10] ci tweaks --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc68eb6..f798ddd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: true matrix: - go: ['1.20.x'] + go: ['1.21.x'] steps: - name: Checkout @@ -43,7 +43,7 @@ jobs: run: go mod verify - name: Lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v3 with: version: latest