Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support expansion in gator verify #3650

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/gator/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ var (
// ErrNotASyncSet indicates the user-indicated file does not contain a
// SyncSet.
ErrNotAGVKManifest = errors.New("not a GVKManifest")
// ErrNotAnExpansion indicates the user-indicated file does not contain a
David-Jaeyoon-Lee marked this conversation as resolved.
Show resolved Hide resolved
// ErrNotAnExpansion indicates the user-indicated file does not contain an
// ExpansionTemplate.
ErrNotAnExpansion = errors.New("not an Expansion Template")
// ErrAddingTemplate indicates a problem instantiating a Suite's ConstraintTemplate.
ErrAddingTemplate = errors.New("adding template")
// ErrAddingConstraint indicates a problem instantiating a Suite's Constraint.
Expand Down
72 changes: 72 additions & 0 deletions pkg/gator/fixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,34 @@ spec:
}
`

TemplateRestrictCustomField = `
kind: ConstraintTemplate
apiVersion: templates.gatekeeper.sh/v1beta1
metadata:
name: restrictedcustomfield
spec:
crd:
spec:
names:
kind: RestrictedCustomField
validation:
openAPIV3Schema:
type: object
properties:
expectedCustomField:
type: boolean
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package restrictedcustomfield
violation[{"msg": msg}] {
got := input.review.object.spec.customField
expected := input.parameters.expectedCustomField
got == expected
msg := sprintf("foo object has restricted custom field value of %v", [expected])
}
`

ConstraintAlwaysValidate = `
kind: AlwaysValidate
apiVersion: constraints.gatekeeper.sh/v1beta1
Expand Down Expand Up @@ -262,6 +290,22 @@ metadata:
name: other
`

ConstraintRestrictCustomField = `
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: RestrictedCustomField
metadata:
name: restrict-foo-custom-field
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Foo"]
namespaces:
- "default"
parameters:
expectedCustomField: true
`

Object = `
kind: Object
apiVersion: group.sh/v1
Expand Down Expand Up @@ -328,6 +372,17 @@ apiVersion: group.sh/v1
metadata:
name: object`

ObjectFooTemplate = `
apiVersion: apps/v1
kind: FooTemplate
metadata:
name: foo-template
spec:
template:
spec:
customField: true
`

NamespaceSelected = `
kind: Namespace
apiVersion: /v1
Expand Down Expand Up @@ -682,4 +737,21 @@ spec:
- apiGroups: ["*"]
kinds: ["*"]
`

ExpansionRestrictCustomField = `
apiVersion: expansion.gatekeeper.sh/v1alpha1
kind: ExpansionTemplate
metadata:
name: expand-foo
spec:
applyTo:
- groups: [ "apps" ]
kinds: [ "FooTemplate" ]
versions: [ "v1" ]
templateSource: "spec.template"
generatedGVK:
kind: "Foo"
group: ""
version: "v1"
`
)
14 changes: 14 additions & 0 deletions pkg/gator/reader/read_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,20 @@ func ReadConstraint(f fs.FS, path string) (*unstructured.Unstructured, error) {
return u, nil
}

func ReadExpansion(f fs.FS, path string) (*unstructured.Unstructured, error) {
u, err := ReadObject(f, path)
if err != nil {
return nil, err
}

gvk := u.GroupVersionKind()
if gvk.Group != "expansion.gatekeeper.sh" || gvk.Kind != "ExpansionTemplate" {
return nil, gator.ErrNotAnExpansion
}

return u, nil
}

// ReadK8sResources reads JSON or YAML k8s resources from an io.Reader,
// decoding them into Unstructured objects and returning those objects as a
// slice.
Expand Down
80 changes: 73 additions & 7 deletions pkg/gator/verify/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
"github.com/open-policy-agent/frameworks/constraint/pkg/client/reviews"
"github.com/open-policy-agent/frameworks/constraint/pkg/types"
"github.com/open-policy-agent/gatekeeper/v3/apis"
"github.com/open-policy-agent/gatekeeper/v3/pkg/expansion"
"github.com/open-policy-agent/gatekeeper/v3/pkg/gator"
"github.com/open-policy-agent/gatekeeper/v3/pkg/gator/expand"
"github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader"
mutationtypes "github.com/open-policy-agent/gatekeeper/v3/pkg/mutation/types"
"github.com/open-policy-agent/gatekeeper/v3/pkg/target"
Expand Down Expand Up @@ -179,6 +181,20 @@ func (r *Runner) runCases(ctx context.Context, suiteDir string, filter Filter, t
return c, nil
}

newExpander := func() (*expand.Expander, error) {
e, err := r.makeTestExpander(suiteDir, t)
if err != nil {
return nil, err
}

return e, nil
}

_, err := newExpander()
if err != nil {
return nil, err
}

results := make([]CaseResult, len(t.Cases))

for i, c := range t.Cases {
Expand All @@ -187,7 +203,7 @@ func (r *Runner) runCases(ctx context.Context, suiteDir string, filter Filter, t
continue
}

results[i] = r.runCase(ctx, newClient, suiteDir, c)
results[i] = r.runCase(ctx, newClient, newExpander, suiteDir, c)
}

return results, nil
Expand Down Expand Up @@ -216,6 +232,22 @@ func (r *Runner) makeTestClient(ctx context.Context, suiteDir string, t *Test) (
return client, nil
}

func (r *Runner) makeTestExpander(suiteDir string, t *Test) (*expand.Expander, error) {
// Support Mutator logic? Then we need to add support for mutators as well or do we just ignore them?
expansionPath := t.Expansion
if expansionPath == "" {
return nil, nil
}

et, err := reader.ReadExpansion(r.filesystem, path.Join(suiteDir, expansionPath))
if err != nil {
return nil, err
}

er, err := expand.NewExpander([]*unstructured.Unstructured{et})
return er, err
}

func (r *Runner) addConstraint(ctx context.Context, suiteDir, constraintPath string, client gator.Client) error {
if constraintPath == "" {
return fmt.Errorf("%w: missing constraint", gator.ErrInvalidSuite)
Expand Down Expand Up @@ -252,9 +284,9 @@ func (r *Runner) addTemplate(suiteDir, templatePath string, client gator.Client)
}

// RunCase executes a Case and returns the result of the run.
func (r *Runner) runCase(ctx context.Context, newClient func() (gator.Client, error), suiteDir string, tc *Case) CaseResult {
func (r *Runner) runCase(ctx context.Context, newClient func() (gator.Client, error), newExpander func() (*expand.Expander, error), suiteDir string, tc *Case) CaseResult {
start := time.Now()
trace, err := r.checkCase(ctx, newClient, suiteDir, tc)
trace, err := r.checkCase(ctx, newClient, newExpander, suiteDir, tc)

return CaseResult{
Name: tc.Name,
Expand All @@ -264,7 +296,7 @@ func (r *Runner) runCase(ctx context.Context, newClient func() (gator.Client, er
}
}

func (r *Runner) checkCase(ctx context.Context, newClient func() (gator.Client, error), suiteDir string, tc *Case) (trace *string, err error) {
func (r *Runner) checkCase(ctx context.Context, newClient func() (gator.Client, error), newExpander func() (*expand.Expander, error), suiteDir string, tc *Case) (trace *string, err error) {
if tc.Object == "" {
return nil, fmt.Errorf("%w: must define object", gator.ErrInvalidCase)
}
Expand All @@ -274,7 +306,7 @@ func (r *Runner) checkCase(ctx context.Context, newClient func() (gator.Client,
return nil, fmt.Errorf("%w: assertions must be non-empty", gator.ErrInvalidCase)
}

review, err := r.runReview(ctx, newClient, suiteDir, tc)
review, err := r.runReview(ctx, newClient, newExpander, suiteDir, tc)
if err != nil {
return nil, err
}
Expand All @@ -293,12 +325,17 @@ func (r *Runner) checkCase(ctx context.Context, newClient func() (gator.Client,
return trace, nil
}

func (r *Runner) runReview(ctx context.Context, newClient func() (gator.Client, error), suiteDir string, tc *Case) (*types.Responses, error) {
func (r *Runner) runReview(ctx context.Context, newClient func() (gator.Client, error), newExpander func() (*expand.Expander, error), suiteDir string, tc *Case) (*types.Responses, error) {
c, err := newClient()
if err != nil {
return nil, err
}

e, err := newExpander()
if err != nil {
return nil, err
}

toReviewPath := path.Join(suiteDir, tc.Object)
toReviewObjs, err := readObjects(r.filesystem, toReviewPath)
if err != nil {
Expand Down Expand Up @@ -327,7 +364,36 @@ func (r *Runner) runReview(ctx context.Context, newClient func() (gator.Client,
Object: *toReview,
Source: mutationtypes.SourceTypeOriginal,
}
return c.Review(ctx, au, reviews.EnforcementPoint(util.GatorEnforcementPoint))

review, err := c.Review(ctx, au, reviews.EnforcementPoint(util.GatorEnforcementPoint))
David-Jaeyoon-Lee marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("reviewing %v %s/%s: %w",
toReview.GroupVersionKind(), toReview.GetNamespace(), toReview.GetName(), err)
David-Jaeyoon-Lee marked this conversation as resolved.
Show resolved Hide resolved
}

if e != nil {
resultants, err := e.Expand(toReview)
if err != nil {
return nil, fmt.Errorf("expanding resource %s: %w", toReview.GetName(), err)
}

for _, resultant := range resultants {
au := target.AugmentedUnstructured{
Object: *resultant.Obj,
Source: mutationtypes.SourceTypeGenerated,
}
resultantReview, err := c.Review(ctx, au, reviews.EnforcementPoint(util.GatorEnforcementPoint))
David-Jaeyoon-Lee marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("reviewing expanded resource %v %s/%s: %w",
resultant.Obj.GroupVersionKind(), resultant.Obj.GetNamespace(), resultant.Obj.GetName(), err)
}
expansion.OverrideEnforcementAction(resultant.EnforcementAction, resultantReview)
expansion.AggregateResponses(resultant.TemplateName, review, resultantReview)
expansion.AggregateStats(resultant.TemplateName, review, resultantReview)
}
}

return review, err
}

func (r *Runner) validateAndReviewAdmissionReviewRequest(ctx context.Context, c gator.Client, toReview *unstructured.Unstructured) (*types.Responses, error) {
Expand Down
62 changes: 62 additions & 0 deletions pkg/gator/verify/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,68 @@ func TestRunner_Run(t *testing.T) {
},
},
},
{
name: "expansion system",
suite: Suite{
Tests: []Test{
{
Name: "check custom field with expansion system",
Template: "template.yaml",
Constraint: "constraint.yaml",
Expansion: "expansion.yaml",
Cases: []*Case{
{
Name: "Foo Template object",
Object: "foo-template.yaml",
Assertions: []Assertion{{Message: ptr.To[string]("foo object has restricted custom field")}},
},
},
},
{
Name: "check custom field without expansion system",
Template: "template.yaml",
Constraint: "constraint.yaml",
Cases: []*Case{
{
Name: "Foo Template object",
Object: "foo-template.yaml",
Assertions: []Assertion{{Violations: gator.IntStrFromStr("no")}},
},
},
},
},
},
f: fstest.MapFS{
"template.yaml": &fstest.MapFile{
Data: []byte(fixtures.TemplateRestrictCustomField),
},
"constraint.yaml": &fstest.MapFile{
Data: []byte(fixtures.ConstraintRestrictCustomField),
},
"foo-template.yaml": &fstest.MapFile{
Data: []byte(fixtures.ObjectFooTemplate),
},
"expansion.yaml": &fstest.MapFile{
Data: []byte(fixtures.ExpansionRestrictCustomField),
},
},
want: SuiteResult{
TestResults: []TestResult{
{
Name: "check custom field with expansion system",
CaseResults: []CaseResult{
{Name: "Foo Template object"},
},
},
{
Name: "check custom field without expansion system",
CaseResults: []CaseResult{
{Name: "Foo Template object"},
},
},
},
},
},
}

for _, tc := range testCases {
Expand Down
4 changes: 4 additions & 0 deletions pkg/gator/verify/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ type Test struct {
// the Suite. Must be an instance of Template.
Constraint string `json:"constraint"`

// Expansion is the path to the Expansion, relative to the file defining
// the Suite.
Expansion string `json:"expansion"`

// Cases are the test cases to run on the instantiated Constraint.
// Mutually exclusive with Invalid.
Cases []*Case `json:"cases,omitempty"`
Expand Down
8 changes: 8 additions & 0 deletions test/gator/verify/allow_expansion.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: apps/v1
kind: FooTemplate
metadata:
name: foo-template
spec:
template:
foo: bar

8 changes: 8 additions & 0 deletions test/gator/verify/deny_expansion.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: apps/v1
kind: FooTemplate
metadata:
name: foo-template
spec:
template:
foo: qux

15 changes: 15 additions & 0 deletions test/gator/verify/expansion.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: expansion.gatekeeper.sh/v1alpha1
kind: ExpansionTemplate
metadata:
name: expand-foo
spec:
applyTo:
- groups: [ "apps" ]
kinds: [ "FooTemplate" ]
versions: [ "v1" ]
templateSource: "spec.template"
generatedGVK:
kind: "FooIsBar"
group: ""
version: "v1"

Loading
Loading