diff --git a/cmd/akashi.go b/cmd/akashi.go index a2b3dba..1bebfff 100644 --- a/cmd/akashi.go +++ b/cmd/akashi.go @@ -1,150 +1,30 @@ package cmd import ( - "fmt" - "io" "os" "github.com/spf13/cobra" - "github.com/drlau/akashi/internal/compare" comparecmd "github.com/drlau/akashi/pkg/cmd/compare" diffcmd "github.com/drlau/akashi/pkg/cmd/diff" versioncmd "github.com/drlau/akashi/pkg/cmd/version" - "github.com/drlau/akashi/pkg/plan" - "github.com/drlau/akashi/pkg/utils" ) var version = "dev" -var ( - file string - versionOutput string - quiet bool - json bool - failedOnly bool - strict bool - noColor bool - errorOnFail bool -) - func NewCommand() *cobra.Command { cmd := &cobra.Command{ Use: "akashi ", Short: "Akashi / 証", Long: `Validate "terraform plan" changes against a customizable ruleset`, - Args: cobra.ExactArgs(1), - RunE: run, SilenceUsage: true, } cmd.SetVersionTemplate(version) - cmd.Flags().StringVarP(&file, "file", "f", "", "read plan output from file") - cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "compare only, and error if there is a failing rule") - cmd.Flags().BoolVar(&failedOnly, "failed-only", false, "only output failing lines") - cmd.Flags().BoolVarP(&strict, "strict", "s", false, "require all resources to match a comparer") - cmd.Flags().BoolVar(&noColor, "no-color", false, "disable color output") - cmd.Flags().BoolVarP(&errorOnFail, "error-on-fail", "e", false, "for non-quiet runs, make akashi return exit code 1 on fails") - cmd.Flags().BoolVarP(&json, "json", "j", false, "read the contents as the output from 'terraform state show -json'") - // TODO - // cmd.Flags().BoolVarP(&verbose, "verbose", "V", false, "enable verbose output") - cmd.AddCommand(comparecmd.NewCmdCompare()) cmd.AddCommand(diffcmd.NewCmdDiff()) cmd.AddCommand(versioncmd.NewCmdVersion(os.Stdout, version)) return cmd } - -func run(_ *cobra.Command, args []string) error { - comparers, err := compare.NewComparerSet(args[0]) - if err != nil { - return err - } - - in, err := plan.NewResourcePlans(file, json) - if err != nil { - return err - } - - if quiet { - fmt.Fprintln(os.Stderr, `[WARN] -q is deprecated. Please run "akashi compare" instead.`) - os.Exit(runCompare(in, comparers)) - } - out := utils.NewOutput(noColor) - - fmt.Fprintln(os.Stderr, `[WARN] no command is deprecated. Please run "akashi diff" instead.`) - os.Exit(runDiff(out, in, comparers)) - return nil -} - -func runCompare(rc []plan.ResourcePlan, comparers compare.ComparerSet) int { - createComparer := comparers.CreateComparer - destroyComparer := comparers.DestroyComparer - updateComparer := comparers.UpdateComparer - - for _, r := range rc { - if r.IsCreate() && createComparer != nil { - if !createComparer.Compare(r) { - return 1 - } - } else if r.IsDelete() && destroyComparer != nil { - if !destroyComparer.Compare(r) { - return 1 - } - } else if r.IsUpdate() && updateComparer != nil { - if !updateComparer.Compare(r) { - return 1 - } - } else if strict { - return 1 - } - } - - return 0 -} - -func runDiff(out io.Writer, rc []plan.ResourcePlan, comparers compare.ComparerSet) int { - exitCode := 0 - createComparer := comparers.CreateComparer - destroyComparer := comparers.DestroyComparer - updateComparer := comparers.UpdateComparer - - for _, r := range rc { - diff := "" - pass := true - if r.IsCreate() && createComparer != nil { - diff, pass = createComparer.Diff(r) - } else if r.IsDelete() && destroyComparer != nil { - diff, pass = destroyComparer.Diff(r) - } else if r.IsUpdate() && updateComparer != nil { - diff, pass = updateComparer.Diff(r) - } else { - if !strict { - continue - } - - if errorOnFail { - exitCode = 1 - } - fmt.Fprintln(out, fmt.Sprintf("%s %s (no matching comparer)", utils.Yellow("?"), r.GetAddress())) - continue - } - if pass { - if failedOnly { - continue - } - - fmt.Fprintln(out, diff) - continue - } - - fmt.Fprintln(out, diff) - if errorOnFail { - exitCode = 1 - } - } - - return exitCode -} diff --git a/cmd/akashi_test.go b/cmd/akashi_test.go index 1fd0536..1d619dd 100644 --- a/cmd/akashi_test.go +++ b/cmd/akashi_test.go @@ -1,341 +1 @@ package cmd - -import ( - "bytes" - "strings" - "testing" - - "github.com/drlau/akashi/internal/compare" - comparefakes "github.com/drlau/akashi/internal/compare/fakes" - "github.com/drlau/akashi/pkg/plan" - planfakes "github.com/drlau/akashi/pkg/plan/fakes" -) - -func TestRunCompare(t *testing.T) { - cases := map[string]struct { - comparers compare.ComparerSet - resourceChange []plan.ResourcePlan - expected int - }{ - "create returns false with create resource": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - CompareReturns: false, - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 1, - }, - "create returns true with create resource": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - CompareReturns: true, - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 0, - }, - "create returns false with non-create resource": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - CompareReturns: false, - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 0, - }, - "create returns true with multiple resources": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - CompareReturns: true, - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - NameReturns: "name", - TypeReturns: "type", - }, - &planfakes.FakeResourcePlan{ - CreateReturns: true, - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 0, - }, - "fails if there is at least 1 failure": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - CompareReturns: false, - }, - DestroyComparer: &comparefakes.FakeComparer{ - CompareReturns: true, - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - NameReturns: "name", - TypeReturns: "type", - }, - &planfakes.FakeResourcePlan{ - DeleteReturns: true, - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 1, - }, - // TODO: test case to ensure comparers are called correctly(matching type and number of calls) - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - if got := runCompare(tc.resourceChange, tc.comparers); got != tc.expected { - t.Errorf("Expected: %v but got %v", tc.expected, got) - } - }) - } -} - -// TODO: there must be a better way to do pre and post hooks -// ginkgo comes to mind -func TestRunDiff(t *testing.T) { - cases := map[string]struct { - comparers compare.ComparerSet - resourceChange []plan.ResourcePlan - preHook func() - expected int - expectedOutput []string - }{ - "create returns false with create resource": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - DiffReturns: false, - DiffOutput: "comparer fail", - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - AddressReturns: "address", - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 0, - expectedOutput: []string{"comparer fail"}, - }, - "create returns true with create resource": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - DiffReturns: true, - DiffOutput: "comparer ok", - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - AddressReturns: "address", - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 0, - expectedOutput: []string{"comparer ok"}, - }, - "no matching comparer": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - DiffReturns: false, - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - AddressReturns: "address", - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 0, - expectedOutput: []string{""}, - }, - "no matching comparer with strict enabled": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - DiffReturns: false, - DiffOutput: "comparer fail", - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - AddressReturns: "address", - NameReturns: "name", - TypeReturns: "type", - }, - }, - preHook: func() { - strict = true - }, - expected: 0, - expectedOutput: []string{"?", "address (no matching comparer)"}, - }, - "create returns true with multiple resources": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - DiffReturns: true, - DiffOutput: "comparer ok", - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - AddressReturns: "address1", - NameReturns: "name", - TypeReturns: "type", - }, - &planfakes.FakeResourcePlan{ - CreateReturns: true, - AddressReturns: "address2", - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 0, - expectedOutput: []string{"comparer ok\ncomparer ok"}, - }, - "fails if there is at least 1 failure": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - DiffReturns: false, - DiffOutput: "comparer fail", - }, - DestroyComparer: &comparefakes.FakeComparer{ - DiffReturns: true, - DiffOutput: "comparer ok", - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - AddressReturns: "address1", - NameReturns: "name", - TypeReturns: "type", - }, - &planfakes.FakeResourcePlan{ - DeleteReturns: true, - AddressReturns: "address2", - NameReturns: "name", - TypeReturns: "type", - }, - }, - expected: 0, - expectedOutput: []string{"comparer fail", "comparer ok"}, - }, - "returns 1 if there is at least 1 failure and errorOnFail is set": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - DiffReturns: false, - DiffOutput: "comparer fail", - }, - DestroyComparer: &comparefakes.FakeComparer{ - DiffReturns: true, - DiffOutput: "comparer ok", - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - AddressReturns: "address1", - NameReturns: "name", - TypeReturns: "type", - }, - &planfakes.FakeResourcePlan{ - DeleteReturns: true, - AddressReturns: "address2", - NameReturns: "name", - TypeReturns: "type", - }, - }, - preHook: func() { - errorOnFail = true - }, - expected: 1, - expectedOutput: []string{"comparer fail", "comparer ok"}, - }, - "only outputs failed with failedOnly": { - comparers: compare.ComparerSet{ - CreateComparer: &comparefakes.FakeComparer{ - DiffReturns: false, - DiffOutput: "comparer fail", - }, - DestroyComparer: &comparefakes.FakeComparer{ - DiffReturns: true, - DiffOutput: "comparer ok", - }, - }, - resourceChange: []plan.ResourcePlan{ - &planfakes.FakeResourcePlan{ - CreateReturns: true, - AddressReturns: "address1", - NameReturns: "name", - TypeReturns: "type", - }, - &planfakes.FakeResourcePlan{ - DeleteReturns: true, - AddressReturns: "address2", - NameReturns: "name", - TypeReturns: "type", - }, - }, - preHook: func() { - failedOnly = true - }, - expected: 0, - expectedOutput: []string{"comparer fail"}, - }, - // TODO: test case to ensure comparers are called correctly(matching type and number of calls) - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - // set default vars - errorOnFail = false - strict = false - failedOnly = false - - if tc.preHook != nil { - tc.preHook() - } - - var output bytes.Buffer - if got := runDiff(&output, tc.resourceChange, tc.comparers); got != tc.expected { - t.Errorf("Expected: %v but got %v", tc.expected, got) - } - - for _, s := range tc.expectedOutput { - if !strings.Contains(output.String(), s) { - t.Errorf("Result string did not contain %v", s) - } - } - }) - } -}