Skip to content

Commit

Permalink
[cmd/match] add new match command
Browse files Browse the repository at this point in the history
This new command is similar to `akashi diff`, except it outputs resource
paths without any extra metadata (i.e. emojis, explanations, etc.).
Specifically, it will output any resource paths from the plan output
which match the Akashi ruleset (or do not match if the `--invert` flag
is provided).

As Akashi currently does not support specifying full addresses for
modules, this will be useful in parsing full addresses to use in
conjunction with the `-target` flag for resource targeting. The
functionality is distinct enough from `akashi diff`, that it seems like
it justifies creating a distinct command rather than refactoring and
piggybacking on any existing commands.

Test Plan
---------

Unit tests

Create a TF directory with the following resources:
```terraform
resource "google_storage_bucket" "auto-expire" {
  name          = "no-public-access-bucket"
  location      = "US"
  project       = "my-proj"
  force_destroy = true

  public_access_prevention = "enforced"
}

resource "google_storage_bucket" "nonexist-auto-expire" {
  name          = "no-public-access-bucket"
  location      = "US"
  project       = "my-proj"
  force_destroy = true

  public_access_prevention = "enforced"
}

resource "google_storage_bucket" "exist-auto-expire" {
  name          = "no-public-access-bucket"
  location      = "US"
  project       = "my-proj"
  force_destroy = true

  public_access_prevention = "enforced"
}
```

This should produce the following plan output:
```
$ terraform plan -no-color
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_storage_bucket.auto-expire will be created
  + resource "google_storage_bucket" "auto-expire" {
      + effective_labels            = (known after apply)
      + force_destroy               = true
      + id                          = (known after apply)
      + location                    = "US"
      + name                        = "no-public-access-bucket"
      + project                     = "kouzoh-p-ryanpham"
      + project_number              = (known after apply)
      + public_access_prevention    = "enforced"
      + rpo                         = (known after apply)
      + self_link                   = (known after apply)
      + storage_class               = "STANDARD"
      + terraform_labels            = (known after apply)
      + uniform_bucket_level_access = (known after apply)
      + url                         = (known after apply)

      + soft_delete_policy (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # google_storage_bucket.exist-auto-expire will be created
  + resource "google_storage_bucket" "exist-auto-expire" {
      + effective_labels            = (known after apply)
      + force_destroy               = true
      + id                          = (known after apply)
      + location                    = "US"
      + name                        = "no-public-access-bucket"
      + project                     = "kouzoh-p-ryanpham"
      + project_number              = (known after apply)
      + public_access_prevention    = "enforced"
      + rpo                         = (known after apply)
      + self_link                   = (known after apply)
      + storage_class               = "STANDARD"
      + terraform_labels            = (known after apply)
      + uniform_bucket_level_access = (known after apply)
      + url                         = (known after apply)

      + soft_delete_policy (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # google_storage_bucket.nonexist-auto-expire will be created
  + resource "google_storage_bucket" "nonexist-auto-expire" {
      + effective_labels            = (known after apply)
      + force_destroy               = true
      + id                          = (known after apply)
      + location                    = "US"
      + name                        = "no-public-access-bucket"
      + project                     = "kouzoh-p-ryanpham"
      + project_number              = (known after apply)
      + public_access_prevention    = "enforced"
      + rpo                         = (known after apply)
      + self_link                   = (known after apply)
      + storage_class               = "STANDARD"
      + terraform_labels            = (known after apply)
      + uniform_bucket_level_access = (known after apply)
      + url                         = (known after apply)

      + soft_delete_policy (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.
```

Specify the following Akashi policy
```yaml
createdResources:
  strict: true
  requireName: true
  default:
    enforceAll: true
    ignoreExtraArgs: true
  resources:
    - type: google_storage_bucket
      name: auto-expire
    - type: google_storage_bucket
      name: exist-auto-expire

updatedResources:
  strict: true
  requireName: true
  default:
    enforceAll: true
    ignoreExtraArgs: true
  resources:
    - type: google_storage_bucket
      name: nonexist-auto-expire
```

Running `akashi match` produces the following:
```
google_storage_bucket.auto-expire
google_storage_bucket.exist-auto-expire

$ ./akashi match /tmp/tf-test/akashi.yaml -f /tmp/tf-test/plan.state --invert
google_storage_bucket.nonexist-auto-expire

$ ./akashi match /tmp/tf-test/akashi.yaml -f /tmp/tf-test/plan.state -s ,
google_storage_bucket.auto-expire,google_storage_bucket.exist-auto-expire
```
  • Loading branch information
ryan-ph committed Oct 28, 2024
1 parent b6355b2 commit 325d22b
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 2 deletions.
2 changes: 2 additions & 0 deletions cmd/akashi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))

Expand Down
2 changes: 1 addition & 1 deletion internal/validate/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
77 changes: 77 additions & 0 deletions pkg/cmd/match/match.go
Original file line number Diff line number Diff line change
@@ -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 <path to ruleset>",
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))
}
128 changes: 128 additions & 0 deletions pkg/cmd/match/match_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/cmd/validate/validate.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package validate

import (
"os"
"fmt"
"os"

"github.com/drlau/akashi/internal/validate"
"github.com/drlau/akashi/pkg/ruleset"
Expand Down

0 comments on commit 325d22b

Please sign in to comment.