Skip to content

Commit

Permalink
Merge branch 'master' into workaround_helm_template
Browse files Browse the repository at this point in the history
  • Loading branch information
ritazh authored Sep 7, 2021
2 parents 562342f + 63b51d3 commit 140a003
Show file tree
Hide file tree
Showing 8 changed files with 756 additions and 177 deletions.
140 changes: 140 additions & 0 deletions pkg/gktest/assertion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package gktest

import (
"fmt"
"regexp"
"sync"

"github.com/open-policy-agent/frameworks/constraint/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
)

// An Assertion is a declaration about the data returned by running an object
// against a Constraint.
type Assertion struct {
// Violations, if set, indicates either whether there are violations, or how
// many violations match this assertion.
//
// The value may be either an integer, of a string. If an integer, exactly
// this number of violations must otherwise match this Assertion. If a string,
// must be either "yes" or "no". If "yes" at least one violation must match
// the Assertion to be satisfied. If "no", there must be zero violations
// matching the Assertion to be satisfied.
//
// Defaults to "yes".
Violations *intstr.IntOrString `json:"violations,omitempty"`

// Message is a regular expression which matches the Msg field of individual
// violations.
//
// If unset, has no effect and all violations match this Assertion.
Message *string `json:"message,omitempty"`

onceMsgRegex sync.Once
msgRegex *regexp.Regexp
}

func (a *Assertion) Run(results []*types.Result) error {
matching := int32(0)
var messages []string

for _, r := range results {
messages = append(messages, r.Msg)

matches, err := a.matches(r)
if err != nil {
return err
}

if matches {
matching++
}
}

// Default to assuming the object fails validation.
if a.Violations == nil {
a.Violations = intStrFromStr("yes")
}

err := a.matchesCount(matching)
if err != nil {
return fmt.Errorf("%w: got messages %v", err, messages)
}

return nil
}

func (a *Assertion) matchesCount(matching int32) error {
switch a.Violations.Type {
case intstr.Int:
return a.matchesCountInt(matching)
case intstr.String:
return a.matchesCountStr(matching)
default:
// Requires a bug in intstr unmarshalling code, or a misuse of the IntOrStr
// type in Go code.
return fmt.Errorf("%w: assertion.violations improperly parsed to type %d",
ErrInvalidYAML, a.Violations.Type)
}
}

func (a *Assertion) matchesCountInt(matching int32) error {
wantMatching := a.Violations.IntVal
if wantMatching != matching {
return fmt.Errorf("%w: got %d violations but want exactly %d",
ErrNumViolations, matching, wantMatching)
}

return nil
}

func (a *Assertion) matchesCountStr(matching int32) error {
switch a.Violations.StrVal {
case "yes":
if matching == 0 {
return fmt.Errorf("%w: got %d violations but want at least %d",
ErrNumViolations, matching, 1)
}

return nil
case "no":
if matching > 0 {
return fmt.Errorf("%w: got %d violations but want none",
ErrNumViolations, matching)
}

return nil
default:
return fmt.Errorf(`%w: assertion.violation, if set, must be an integer, "yes", or "no"`,
ErrInvalidYAML)
}
}

func (a *Assertion) matches(result *types.Result) (bool, error) {
r, err := a.getMsgRegex()
if err != nil {
return false, err
}

if r != nil {
return r.MatchString(result.Msg), nil
}

return true, nil
}

func (a *Assertion) getMsgRegex() (*regexp.Regexp, error) {
if a.Message == nil {
return nil, nil
}

var err error
a.onceMsgRegex.Do(func() {
a.msgRegex, err = regexp.Compile(*a.Message)
})
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidRegex, err)
}

return a.msgRegex, nil
}
12 changes: 6 additions & 6 deletions pkg/gktest/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ var (
ErrCreatingClient = errors.New("creating client")
// ErrInvalidCase indicates a Case cannot be run due to not being configured properly.
ErrInvalidCase = errors.New("invalid Case")
// ErrUnexpectedAllow indicates a Case failed because it was expected to get
// violations, but did not get any.
ErrUnexpectedAllow = errors.New("got no violations")
// ErrUnexpectedDeny indicates a Case failed because it got violations, but
// was not expected to get any.
ErrUnexpectedDeny = errors.New("got violations")
// ErrNumViolations indicates an Object did not get the expected number of
// violations.
ErrNumViolations = errors.New("unexpected number of violations")
// ErrInvalidRegex indicates a Case specified a Violation regex that could not
// be compiled.
ErrInvalidRegex = errors.New("message contains invalid regular expression")
)
13 changes: 13 additions & 0 deletions pkg/gktest/intstr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package gktest

import "k8s.io/apimachinery/pkg/util/intstr"

func intStrFromInt(val int) *intstr.IntOrString {
result := intstr.FromInt(val)
return &result
}

func intStrFromStr(val string) *intstr.IntOrString {
result := intstr.FromString(val)
return &result
}
13 changes: 10 additions & 3 deletions pkg/gktest/read_suites.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package gktest

import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"path/filepath"

"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

Expand Down Expand Up @@ -167,10 +168,16 @@ func readSuite(f fs.FS, path string) (*Suite, error) {
return nil, nil
}

// We must pass through JSON as IntOrStr does not unmarshal from YAML properly.
jsn, err := json.Marshal(u.Object)
if err != nil {
return nil, fmt.Errorf("%w: marshaling yaml to json: %v", ErrInvalidYAML, err)
}

suite := Suite{}
err = yaml.Unmarshal(bytes, &suite)
err = json.Unmarshal(jsn, &suite)
if err != nil {
return nil, fmt.Errorf("parsing Test %q into Suite: %w", path, err)
return nil, fmt.Errorf("%w: parsing Test %q into Suite: %v", ErrInvalidYAML, path, err)
}
return &suite, nil
}
71 changes: 65 additions & 6 deletions pkg/gktest/read_suites_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"k8s.io/utils/pointer"
)

func TestReadSuites(t *testing.T) {
Expand Down Expand Up @@ -264,8 +265,11 @@ tests:
- template: template.yaml
constraint: constraint.yaml
cases:
- allow: allow.yaml
- deny: deny.yaml
- object: allow.yaml
- object: deny.yaml
assertions:
- violations: 2
message: "some message"
`),
},
},
Expand All @@ -274,9 +278,49 @@ tests:
Template: "template.yaml",
Constraint: "constraint.yaml",
Cases: []Case{{
Allow: "allow.yaml",
Object: "allow.yaml",
}, {
Deny: "deny.yaml",
Object: "deny.yaml",
Assertions: []Assertion{{
Violations: intStrFromInt(2),
Message: pointer.StringPtr("some message"),
}},
}},
}},
}},
wantErr: nil,
},
{
name: "suite with empty assertions",
target: "test.yaml",
recursive: false,
fileSystem: fstest.MapFS{
"test.yaml": &fstest.MapFile{
Data: []byte(`
kind: Suite
apiVersion: test.gatekeeper.sh/v1alpha1
tests:
- template: template.yaml
constraint: constraint.yaml
cases:
- object: allow.yaml
- object: deny.yaml
assertions:
- violations: "yes"
`),
},
},
want: map[string]*Suite{"test.yaml": {
Tests: []Test{{
Template: "template.yaml",
Constraint: "constraint.yaml",
Cases: []Case{{
Object: "allow.yaml",
}, {
Object: "deny.yaml",
Assertions: []Assertion{{
Violations: intStrFromStr("yes"),
}},
}},
}},
}},
Expand Down Expand Up @@ -305,17 +349,32 @@ tests:
}},
wantErr: nil,
},
{
name: "invalid suite",
target: "test.yaml",
recursive: false,
fileSystem: fstest.MapFS{
"test.yaml": &fstest.MapFile{
Data: []byte(`
kind: Suite
apiVersion: test.gatekeeper.sh/v1alpha1
tests: {}
`),
},
},
wantErr: ErrInvalidYAML,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, gotErr := ReadSuites(tc.fileSystem, tc.target, tc.recursive)
if !errors.Is(gotErr, tc.wantErr) {
t.Errorf("got error %v, want error %v",
t.Fatalf("got error %v, want error %v",
gotErr, tc.wantErr)
}

if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(Assertion{})); diff != "" {
t.Error(diff)
}
})
Expand Down
Loading

0 comments on commit 140a003

Please sign in to comment.