Skip to content

Commit

Permalink
Merge pull request #14 from ryan-ph/cmd/validate/require-named
Browse files Browse the repository at this point in the history
[cmd/validate] add a flag to require names for all resources
  • Loading branch information
drlau authored Apr 4, 2024
2 parents 1ff2fb1 + 3cd4577 commit a5434dd
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 10 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ createdResources:
# Default is false.
strict: true

# Set to true if you want to validate the ruleset specifies names for
# created resources.
# Default is false.
requireName: true

# Default compare options to apply to all resources.
# If a resource specifies the same option, the resource's value will be used.
default:
Expand Down Expand Up @@ -150,6 +155,11 @@ updatedResources:
# Default is false.
strict: true

# Set to true if you want to validate the ruleset specifies names for
# updated resources.
# Default is false.
requireName: true

# Default compare options to apply to all resources.
# If a resource specifies the same option, the resource's value will be used.
# All options for created and destroyed resources work here, but also has a few additional options that can be enabled
Expand Down Expand Up @@ -202,4 +212,4 @@ createdResources:
enforced:
zone:
value: us-central1-a
```
```
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"
validatecmd "github.com/drlau/akashi/pkg/cmd/validate"
versioncmd "github.com/drlau/akashi/pkg/cmd/version"
"github.com/drlau/akashi/pkg/plan"
"github.com/drlau/akashi/pkg/utils"
Expand Down Expand Up @@ -52,6 +53,7 @@ func NewCommand() *cobra.Command {

cmd.AddCommand(comparecmd.NewCmdCompare())
cmd.AddCommand(diffcmd.NewCmdDiff())
cmd.AddCommand(validatecmd.NewCmd())
cmd.AddCommand(versioncmd.NewCmdVersion(os.Stdout, version))

return cmd
Expand Down
10 changes: 1 addition & 9 deletions internal/compare/compare.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package compare

import (
"io/ioutil"

"github.com/drlau/akashi/pkg/compare"
"github.com/drlau/akashi/pkg/plan"
"github.com/drlau/akashi/pkg/ruleset"
yaml "gopkg.in/yaml.v2"
)

type Comparer interface {
Expand All @@ -22,13 +19,8 @@ type ComparerSet struct {

func NewComparerSet(path string) (ComparerSet, error) {
result := ComparerSet{}
rulesetFile, err := ioutil.ReadFile(path)
if err != nil {
return result, err
}

var rs ruleset.Ruleset
err = yaml.Unmarshal(rulesetFile, &rs)
rs, err := ruleset.ParseRuleset(path)
if err != nil {
return result, err
}
Expand Down
97 changes: 97 additions & 0 deletions internal/validate/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package validate

import (
"fmt"
"strings"

"github.com/drlau/akashi/pkg/ruleset"
)

// TODO: currently the only static validation we do is to check if names are
// present for all resources in the ruleset if RequireName is set. Ideally we
// should support more validations, so this struct will need to evolve to
// contain more information about _why_ the resource is invalid.
type ValidateResult struct {
InvalidCreatedResources []*ruleset.ResourceIdentifier
InvalidDestroyedResources []*ruleset.ResourceIdentifier
InvalidUpdatedResources []*ruleset.ResourceIdentifier
}

func (r *ValidateResult) fill_defaults() {
if r.InvalidCreatedResources == nil {
r.InvalidCreatedResources = make([]*ruleset.ResourceIdentifier, 0)
}
if r.InvalidDestroyedResources == nil {
r.InvalidDestroyedResources = make([]*ruleset.ResourceIdentifier, 0)
}
if r.InvalidUpdatedResources == nil {
r.InvalidUpdatedResources = make([]*ruleset.ResourceIdentifier, 0)
}
}

func formatResourceIDs(ids []*ruleset.ResourceIdentifier) []string {
var lines []string
for _, id := range ids {
lines = append(lines, fmt.Sprintf("\t- %s", id.String()))
}
return lines
}

func (r *ValidateResult) String() string {
if r.IsValid() {
return "All resources valid!"
}

lines := []string{
"Found invalid resources in the ruleset:",
"---------------------------------------",
}
if len(r.InvalidCreatedResources) != 0 {
lines = append(lines, "Invalid Created Resources:")
lines = append(lines, formatResourceIDs(r.InvalidCreatedResources)...)
}
if len(r.InvalidDestroyedResources) != 0 {
lines = append(lines, "Invalid Destroyed Resources:")
lines = append(lines, formatResourceIDs(r.InvalidDestroyedResources)...)
}
if len(r.InvalidUpdatedResources) != 0 {
lines = append(lines, "Invalid Updated Resources:")
lines = append(lines, formatResourceIDs(r.InvalidUpdatedResources)...)
}
return strings.Join(lines, "\n")
}

func (r *ValidateResult) IsValid() bool {
createdValid := len(r.InvalidCreatedResources) == 0
destroyedValid := len(r.InvalidDestroyedResources) == 0
updatedValid := len(r.InvalidUpdatedResources) == 0
return createdValid && destroyedValid && updatedValid
}

func getUnnamedResources[T ruleset.Resource](rs []T) []*ruleset.ResourceIdentifier {
var res []*ruleset.ResourceIdentifier
for _, r := range rs {
id := r.ID()
if id.Name == "" {
res = append(res, id)
}
}
return res
}

func Validate(rs ruleset.Ruleset) *ValidateResult {
res := &ValidateResult{}
if rs.CreatedResources != nil && rs.CreatedResources.RequireName {
ids := getUnnamedResources(rs.CreatedResources.Resources)
res.InvalidCreatedResources = ids
}
if rs.DestroyedResources != nil && rs.DestroyedResources.RequireName {
ids := getUnnamedResources(rs.DestroyedResources.Resources)
res.InvalidDestroyedResources = ids
}
if rs.UpdatedResources != nil && rs.UpdatedResources.RequireName {
ids := getUnnamedResources(rs.UpdatedResources.Resources)
res.InvalidUpdatedResources = ids
}
return res
}
145 changes: 145 additions & 0 deletions internal/validate/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package validate

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/drlau/akashi/pkg/ruleset"
)

func TestValidate(t *testing.T) {
tests := map[string]struct {
rs ruleset.Ruleset
expected *ValidateResult
}{
"valid ruleset without required names": {
rs: ruleset.Ruleset{
CreatedResources: &ruleset.CreateDeleteResourceChanges{
Strict: false,
RequireName: false,
Resources: []ruleset.CreateDeleteResourceChange{
{
ResourceIdentifier: ruleset.ResourceIdentifier{
Type: "google_project_service",
Name: "api",
},
},
{
ResourceIdentifier: ruleset.ResourceIdentifier{
Type: "google_service_account",
},
},
},
},
},
expected: &ValidateResult{},
},
"valid ruleset with required names": {
rs: ruleset.Ruleset{
CreatedResources: &ruleset.CreateDeleteResourceChanges{
Strict: false,
RequireName: true,
Resources: []ruleset.CreateDeleteResourceChange{
{
ResourceIdentifier: ruleset.ResourceIdentifier{
Type: "google_project_service",
Name: "api",
},
},
{
ResourceIdentifier: ruleset.ResourceIdentifier{
Type: "google_service_account",
Name: "my_service_account",
},
},
},
},
},
expected: &ValidateResult{},
},
"invalid ruleset": {
rs: ruleset.Ruleset{
CreatedResources: &ruleset.CreateDeleteResourceChanges{
Strict: false,
RequireName: true,
Resources: []ruleset.CreateDeleteResourceChange{
{
ResourceIdentifier: ruleset.ResourceIdentifier{
Type: "google_project_service",
},
},
},
},
},
expected: &ValidateResult{
InvalidCreatedResources: []*ruleset.ResourceIdentifier{
{Type: "google_project_service"},
},
},
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
res := Validate(test.rs)
if diff := cmp.Diff(res, test.expected); diff != "" {
t.Errorf("Validate() mismatch (-want +got):\n%s", diff)
}
})
}
}

func TestIsValid(t *testing.T) {
tests := map[string]struct {
res ValidateResult
expected bool
}{
"no invalid resources": {
res: ValidateResult{},
expected: true,
},
"invalid created resources": {
res: ValidateResult{
InvalidCreatedResources: []*ruleset.ResourceIdentifier{
{Type: "fake_created_resource"},
},
},
expected: false,
},
"invalid destroyed resources": {
res: ValidateResult{
InvalidDestroyedResources: []*ruleset.ResourceIdentifier{
{Type: "fake_destroy_resource"},
},
},
expected: false,
},
"invalid updated resources": {
res: ValidateResult{
InvalidUpdatedResources: []*ruleset.ResourceIdentifier{
{Type: "fake_update_resource"},
},
},
expected: false,
},
"multiple invalid resources changes": {
res: ValidateResult{
InvalidCreatedResources: []*ruleset.ResourceIdentifier{
{Type: "fake_created_resource"},
},
InvalidUpdatedResources: []*ruleset.ResourceIdentifier{
{Type: "fake_update_resource"},
},
},
expected: false,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
if test.res.IsValid() != test.expected {
t.Errorf("Expected %t, got %t: %v", test.expected, test.res.IsValid(), test.res)
}
})
}
}
41 changes: 41 additions & 0 deletions pkg/cmd/validate/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package validate

import (
"os"
"fmt"

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

"github.com/spf13/cobra"
)

func NewCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "validate <path to ruleset>",
Short: "Validte the ruleset",
Long: "Validate the ruleset, exiting with code 0 if the ruleset is valid",

// NOTE: We explicitly do not set Args with ExactArgs(1) since that
// will not print the help message.
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
cmd.Help()
os.Exit(1)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
ruleset, err := ruleset.ParseRuleset(args[0])
if err != nil {
return fmt.Errorf("Could not parse ruleset: %v", err)
}
if res := validate.Validate(ruleset); !res.IsValid() {
return fmt.Errorf("%s", res.String())
}
fmt.Println("Ruleset is valid!")
return nil
},
}
return cmd
}
Loading

0 comments on commit a5434dd

Please sign in to comment.