diff --git a/cmd/akashi.go b/cmd/akashi.go index c5aa3de..908733d 100644 --- a/cmd/akashi.go +++ b/cmd/akashi.go @@ -10,6 +10,7 @@ import ( "github.com/drlau/akashi/internal/compare" comparecmd "github.com/drlau/akashi/pkg/cmd/compare" diffcmd "github.com/drlau/akashi/pkg/cmd/diff" + matchcmd "github.com/drlau/akashi/pkg/cmd/match" validatecmd "github.com/drlau/akashi/pkg/cmd/validate" versioncmd "github.com/drlau/akashi/pkg/cmd/version" "github.com/drlau/akashi/pkg/plan" @@ -53,6 +54,7 @@ func NewCommand() *cobra.Command { cmd.AddCommand(comparecmd.NewCmdCompare()) cmd.AddCommand(diffcmd.NewCmdDiff()) + cmd.AddCommand(matchcmd.NewCmdMatch()) cmd.AddCommand(validatecmd.NewCmd()) cmd.AddCommand(versioncmd.NewCmdVersion(os.Stdout, version)) diff --git a/internal/validate/validate_test.go b/internal/validate/validate_test.go index da9a1db..8370ab9 100644 --- a/internal/validate/validate_test.go +++ b/internal/validate/validate_test.go @@ -3,8 +3,8 @@ package validate import ( "testing" - "github.com/google/go-cmp/cmp" "github.com/drlau/akashi/pkg/ruleset" + "github.com/google/go-cmp/cmp" ) func TestValidate(t *testing.T) { diff --git a/pkg/cmd/match/match.go b/pkg/cmd/match/match.go new file mode 100644 index 0000000..23bebfe --- /dev/null +++ b/pkg/cmd/match/match.go @@ -0,0 +1,77 @@ +package match + +import ( + "fmt" + "io" + "strings" + + "github.com/drlau/akashi/internal/compare" + "github.com/drlau/akashi/pkg/plan" + "github.com/drlau/akashi/pkg/utils" + + "github.com/spf13/cobra" +) + +type MatchOptions struct { + File string + JSON bool + Invert bool + Separator string +} + +func NewCmdMatch() *cobra.Command { + opts := &MatchOptions{} + cmd := &cobra.Command{ + Use: "match ", + Short: "Outputs resource paths which match the ruleset", + Long: `Outputs resource paths from "terraform plan" which are defined in the ruleset`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + comparers, err := compare.NewComparerSet(args[0]) + if err != nil { + return err + } + + plan, err := plan.NewResourcePlans(opts.File, opts.JSON) + if err != nil { + return err + } + + out := utils.NewOutput(true) + cmd.SilenceErrors = true + runMatch(out, plan, comparers, opts) + + return nil + }, + } + + cmd.Flags().StringVarP(&opts.File, "file", "f", "", "read plan output from file") + cmd.Flags().BoolVarP(&opts.JSON, "json", "j", false, "read the contents as the output from 'terraform state show -json'") + cmd.Flags().BoolVarP(&opts.Invert, "invert", "i", false, "outputs resources which do not match the ruleset") + cmd.Flags().StringVarP(&opts.Separator, "separator", "s", "\n", "separator between resource paths") + + return cmd +} + +func runMatch(out io.Writer, rc []plan.ResourcePlan, comparers compare.ComparerSet, opts *MatchOptions) { + createComparer := comparers.CreateComparer + destroyComparer := comparers.DestroyComparer + updateComparer := comparers.UpdateComparer + + var matches []string + for _, r := range rc { + var match bool + if r.IsCreate() && createComparer != nil { + match = createComparer.Compare(r) + } else if r.IsDelete() && destroyComparer != nil { + match = destroyComparer.Compare(r) + } else if r.IsUpdate() && updateComparer != nil { + match = updateComparer.Compare(r) + } + if (!opts.Invert && match) || (opts.Invert && !match) { + matches = append(matches, r.GetAddress()) + } + } + + fmt.Fprintln(out, strings.Join(matches, opts.Separator)) +} diff --git a/pkg/cmd/match/match_test.go b/pkg/cmd/match/match_test.go new file mode 100644 index 0000000..10ac4b9 --- /dev/null +++ b/pkg/cmd/match/match_test.go @@ -0,0 +1,128 @@ +package match + +import ( + "bytes" + "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 TestRunMatch(t *testing.T) { + cases := map[string]struct { + comparers compare.ComparerSet + resourcePlan []plan.ResourcePlan + opts *MatchOptions + expectedOutput string + }{ + "outputs all matching resources from the ruleset": { + comparers: compare.ComparerSet{ + CreateComparer: &comparefakes.FakeComparer{ + CompareReturns: true, + }, + DestroyComparer: &comparefakes.FakeComparer{ + CompareReturns: false, + }, + }, + resourcePlan: []plan.ResourcePlan{ + &planfakes.FakeResourcePlan{ + CreateReturns: true, + AddressReturns: "address1", + NameReturns: "name", + TypeReturns: "type", + }, + &planfakes.FakeResourcePlan{ + CreateReturns: true, + AddressReturns: "address2", + NameReturns: "name", + TypeReturns: "type", + }, + &planfakes.FakeResourcePlan{ + DeleteReturns: true, + AddressReturns: "address3", + NameReturns: "name", + TypeReturns: "type", + }, + }, + opts: &MatchOptions{Separator: "\n"}, + expectedOutput: "address1\naddress2\n", + }, + "outputs all non-matching resources from ruleset when inverted": { + comparers: compare.ComparerSet{ + CreateComparer: &comparefakes.FakeComparer{ + CompareReturns: true, + }, + DestroyComparer: &comparefakes.FakeComparer{ + CompareReturns: false, + }, + }, + resourcePlan: []plan.ResourcePlan{ + &planfakes.FakeResourcePlan{ + CreateReturns: true, + AddressReturns: "address1", + NameReturns: "name", + TypeReturns: "type", + }, + &planfakes.FakeResourcePlan{ + CreateReturns: true, + AddressReturns: "address2", + NameReturns: "name", + TypeReturns: "type", + }, + &planfakes.FakeResourcePlan{ + DeleteReturns: true, + AddressReturns: "address3", + NameReturns: "name", + TypeReturns: "type", + }, + }, + opts: &MatchOptions{Separator: "\n", Invert: true}, + expectedOutput: "address3\n", + }, + "outputs all resources using custom separator": { + comparers: compare.ComparerSet{ + CreateComparer: &comparefakes.FakeComparer{ + CompareReturns: true, + }, + DestroyComparer: &comparefakes.FakeComparer{ + CompareReturns: false, + }, + }, + resourcePlan: []plan.ResourcePlan{ + &planfakes.FakeResourcePlan{ + CreateReturns: true, + AddressReturns: "address1", + NameReturns: "name", + TypeReturns: "type", + }, + &planfakes.FakeResourcePlan{ + CreateReturns: true, + AddressReturns: "address2", + NameReturns: "name", + TypeReturns: "type", + }, + &planfakes.FakeResourcePlan{ + DeleteReturns: true, + AddressReturns: "address3", + NameReturns: "name", + TypeReturns: "type", + }, + }, + opts: &MatchOptions{Separator: ","}, + expectedOutput: "address1,address2\n", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + var output bytes.Buffer + runMatch(&output, tc.resourcePlan, tc.comparers, tc.opts) + + if output.String() != tc.expectedOutput { + t.Errorf("Expected: %q\nGot: %q\n", tc.expectedOutput, output.String()) + } + }) + } +} diff --git a/pkg/cmd/validate/validate.go b/pkg/cmd/validate/validate.go index b67b28d..e80f2c1 100644 --- a/pkg/cmd/validate/validate.go +++ b/pkg/cmd/validate/validate.go @@ -1,8 +1,8 @@ package validate import ( - "os" "fmt" + "os" "github.com/drlau/akashi/internal/validate" "github.com/drlau/akashi/pkg/ruleset"