diff --git a/README.md b/README.md index 8441f9c..29c975e 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ schemaLocations: # Optional: For Custom Resources - vapTestSuites: - policy: # ValidatingAdmissionPolicy's name + binding: # Optional: ValidatingAdmissionPolicyBinding's name tests: - object: group: # Optional @@ -114,6 +115,7 @@ vapTestSuites: expect: mapTestSuites: - policy: # MutatingAdmissionPolicy's name + binding: # Optional: MutatingAdmissionPolicyBinding's name tests: - object: group: # Optional diff --git a/internal/tester/loader.go b/internal/tester/loader.go index 2a6a14a..db81e5b 100644 --- a/internal/tester/loader.go +++ b/internal/tester/loader.go @@ -37,18 +37,22 @@ import ( ) type ResourceLoader struct { - Vaps map[string]*v1.ValidatingAdmissionPolicy - Maps map[string]*v1alpha1.MutatingAdmissionPolicy - Resources map[NameWithGVK]*unstructured.Unstructured - validator validator.Validator + Vaps map[string]*v1.ValidatingAdmissionPolicy + VapBindings map[string]*v1.ValidatingAdmissionPolicyBinding + Maps map[string]*v1alpha1.MutatingAdmissionPolicy + MapBindings map[string]*v1alpha1.MutatingAdmissionPolicyBinding + Resources map[NameWithGVK]*unstructured.Unstructured + validator validator.Validator } func NewResourceLoader(validator validator.Validator) *ResourceLoader { return &ResourceLoader{ - Vaps: map[string]*v1.ValidatingAdmissionPolicy{}, - Maps: map[string]*v1alpha1.MutatingAdmissionPolicy{}, - Resources: map[NameWithGVK]*unstructured.Unstructured{}, - validator: validator, + Vaps: map[string]*v1.ValidatingAdmissionPolicy{}, + VapBindings: map[string]*v1.ValidatingAdmissionPolicyBinding{}, + Maps: map[string]*v1alpha1.MutatingAdmissionPolicy{}, + MapBindings: map[string]*v1alpha1.MutatingAdmissionPolicyBinding{}, + Resources: map[NameWithGVK]*unstructured.Unstructured{}, + validator: validator, } } @@ -102,6 +106,13 @@ func (r *ResourceLoader) LoadPolicies(paths []string) { } vap := obj.(*v1.ValidatingAdmissionPolicy) r.Vaps[vap.Name] = vap + case "ValidatingAdmissionPolicyBinding": + if gvk.Version != "v1" { + slog.Warn("only v1 ValidatingAdmissionPolicyBinding is supported", "version", gvk.Version) + continue + } + vb := obj.(*v1.ValidatingAdmissionPolicyBinding) + r.VapBindings[vb.Name] = vb case "MutatingAdmissionPolicy": if gvk.Version != "v1alpha1" { slog.Warn("only v1alpha1 MutatingAdmissionPolicy is supported", "version", gvk.Version) @@ -111,6 +122,13 @@ func (r *ResourceLoader) LoadPolicies(paths []string) { // Ensure nil labelSelectors to be matching everything defaultingMAPPolicy(m) r.Maps[m.Name] = m + case "MutatingAdmissionPolicyBinding": + if gvk.Version != "v1alpha1" { + slog.Warn("only v1alpha1 MutatingAdmissionPolicyBinding is supported", "version", gvk.Version) + continue + } + mb := obj.(*v1alpha1.MutatingAdmissionPolicyBinding) + r.MapBindings[mb.Name] = mb default: slog.Warn("unexpected manifest", "kind", gvk.Kind) } diff --git a/internal/tester/manifest.go b/internal/tester/manifest.go index cbb6946..bff5d10 100644 --- a/internal/tester/manifest.go +++ b/internal/tester/manifest.go @@ -49,8 +49,9 @@ func (t TestManifests) IsValid() (bool, string) { // TestsForSingleVapPolicy is a struct to aggregate multiple test cases for a single policy. type TestsForSingleVapPolicy struct { - Policy string `yaml:"policy"` - Tests []VAPTestCase `yaml:"tests"` + Policy string `yaml:"policy"` + Binding string `yaml:"binding,omitempty"` + Tests []VAPTestCase `yaml:"tests"` } type PolicyDecisionExpect string @@ -109,8 +110,9 @@ func (tc VAPTestCase) SummaryLine(pass bool, policy string, result string) strin } type TestsForSingleMapPolicy struct { - Policy string `yaml:"policy"` - Tests []MAPTestCase `yaml:"tests"` + Policy string `yaml:"policy"` + Binding string `yaml:"binding,omitempty"` + Tests []MAPTestCase `yaml:"tests"` } type MAPTestCase struct { diff --git a/internal/tester/result.go b/internal/tester/result.go index 665bbdb..56f1485 100644 --- a/internal/tester/result.go +++ b/internal/tester/result.go @@ -154,6 +154,26 @@ func (r *policyNotFoundResult) String(verbose bool) string { return fmt.Sprintf("FAIL: %s ==> POLICY NOT FOUND", r.Policy) } +type bindingNotFoundResult struct { + Binding string +} + +var _ testResult = &bindingNotFoundResult{} + +func newBindingNotFoundResult(binding string) *bindingNotFoundResult { + return &bindingNotFoundResult{ + Binding: binding, + } +} + +func (r *bindingNotFoundResult) Pass() bool { + return false +} + +func (r *bindingNotFoundResult) String(verbose bool) string { + return fmt.Sprintf("FAIL: %s ==> BINDING NOT FOUND", r.Binding) +} + type setupErrorResult struct { Policy string TestCase TestCase diff --git a/internal/tester/testdata/map-with-params.test/kaptest.yaml b/internal/tester/testdata/map-with-params.test/kaptest.yaml index 3921f96..1f389c7 100644 --- a/internal/tester/testdata/map-with-params.test/kaptest.yaml +++ b/internal/tester/testdata/map-with-params.test/kaptest.yaml @@ -10,6 +10,7 @@ mapTestSuites: name: small namespace: foo param: + namespace: hoge name: config1 expect: mutate expectObject: @@ -21,5 +22,21 @@ mapTestSuites: name: ok namespace: foo param: + namespace: hoge name: config1 expect: skip +- policy: deployment-replicas + binding: deployment-replicas-binding + tests: + - object: + kind: Deployment + name: small + namespace: foo + param: + namespace: foo + name: config1 + expect: mutate + expectObject: + kind: Deployment + name: ok-large + namespace: foo \ No newline at end of file diff --git a/internal/tester/testdata/map-with-params.test/resources.yaml b/internal/tester/testdata/map-with-params.test/resources.yaml index 2d2fa61..f9404fb 100644 --- a/internal/tester/testdata/map-with-params.test/resources.yaml +++ b/internal/tester/testdata/map-with-params.test/resources.yaml @@ -21,6 +21,27 @@ spec: --- apiVersion: apps/v1 kind: Deployment +metadata: + name: ok-large + namespace: foo + labels: + app: ok-deployment +spec: + replicas: 10 + selector: + matchLabels: + app: ok-deployment + template: + metadata: + labels: + app: ok-deployment + spec: + containers: + - name: nginx + image: nginx +--- +apiVersion: apps/v1 +kind: Deployment metadata: name: small namespace: foo @@ -47,3 +68,11 @@ metadata: namespace: hoge data: maxReplicas: "5" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config1 + namespace: foo +data: + maxReplicas: "10" diff --git a/internal/tester/testdata/map-with-params.yaml b/internal/tester/testdata/map-with-params.yaml index 5870684..de3bb16 100644 --- a/internal/tester/testdata/map-with-params.yaml +++ b/internal/tester/testdata/map-with-params.yaml @@ -32,3 +32,13 @@ spec: replicas: variables.maxReplicas } } +--- +apiVersion: admissionregistration.k8s.io/v1alpha1 +kind: MutatingAdmissionPolicyBinding +metadata: + name: deployment-replicas-binding +spec: + policyName: deployment-replicas + paramRef: + name: config1 + namespace: foo diff --git a/internal/tester/tester.go b/internal/tester/tester.go index abbc4cc..eba63b6 100644 --- a/internal/tester/tester.go +++ b/internal/tester/tester.go @@ -155,25 +155,35 @@ func runEach(cfg TesterCmdConfig, manifestPath string) testResultSummary { // Run test cases for VAP one by one for _, tt := range manifests.VapTestSuites { // Create Validator - vap, ok := loader.Vaps[tt.Policy] + policy, ok := loader.Vaps[tt.Policy] if !ok { results = append(results, newPolicyNotFoundResult(tt.Policy)) continue } - validator := kaptest.NewValidator(vap) + var validator *kaptest.Validator + if tt.Binding != "" { + binding, ok := loader.MapBindings[tt.Binding] + if !ok { + results = append(results, newBindingNotFoundResult(tt.Binding)) + continue + } + validator = kaptest.NewValidatorWithBinding(policy, binding) + } else { + validator = kaptest.NewValidator(policy) + } for _, tc := range tt.Tests { slog.Debug("SETUP: ", "policy", tt.Policy, "expect", tc.Expect, "object", tc.Object.String(), "oldObject", tc.OldObject.String(), "param", tc.Param.String()) // Setup params for validation - given, errs := newValidationParams(vap, tc, loader) + given, errs := newValidationParams(policy, tc, loader) if len(errs) > 0 { results = append(results, newSetupErrorResult(tt.Policy, tc, errs)) continue } // Run EvalMatchConditions - if vap.Spec.MatchConditions != nil { + if policy.Spec.MatchConditions != nil { matchResult := validator.EvalMatchCondition(given) if matchResult.Error != nil { results = append(results, newPolicyEvalErrorResult(tt.Policy, tc, []error{matchResult.Error})) @@ -200,7 +210,18 @@ func runEach(cfg TesterCmdConfig, manifestPath string) testResultSummary { continue } - mutator, err := kaptest.NewMutator(policy) + var mutator *kaptest.Mutator + var err error + if tt.Binding != "" { + binding, ok := loader.MapBindings[tt.Binding] + if !ok { + results = append(results, newBindingNotFoundResult(tt.Binding)) + continue + } + mutator, err = kaptest.NewMutatorWithBinding(policy, binding) + } else { + mutator, err = kaptest.NewMutator(policy) + } if err != nil { panic(err) } diff --git a/mutation.go b/mutation.go index 199553b..b39dd01 100644 --- a/mutation.go +++ b/mutation.go @@ -54,6 +54,7 @@ type MutatorInterface interface { type Mutator struct { policy *v1alpha1.MutatingAdmissionPolicy + binding *v1alpha1.MutatingAdmissionPolicyBinding evaluator mutating.PolicyEvaluator } @@ -107,6 +108,15 @@ func NewMutator(policy *v1alpha1.MutatingAdmissionPolicy) (*Mutator, error) { }, nil } +func NewMutatorWithBinding(policy *v1alpha1.MutatingAdmissionPolicy, binding *v1alpha1.MutatingAdmissionPolicyBinding) (*Mutator, error) { + m, err := NewMutator(policy) + if err != nil { + return nil, err + } + m.binding = binding + return m, nil +} + type mutatorContext struct { tcm patch.TypeConverterManager auth authorizer.Authorizer @@ -280,21 +290,25 @@ func (m *Mutator) dispatchImpl(p MutationParams, dispatcherFactory func(mCtx *mu return nil, fmt.Errorf("failed to initialize mutatorContext: %w", err) } - binding := &v1alpha1.MutatingAdmissionPolicyBinding{ - Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ - ParamRef: &v1alpha1.ParamRef{}, - MatchResources: &v1alpha1.MatchResources{ - MatchPolicy: ptr.To(v1alpha1.Equivalent), - ObjectSelector: &metav1.LabelSelector{}, - NamespaceSelector: &metav1.LabelSelector{}, + bindingGenerated := false + if m.binding == nil { + m.binding = &v1alpha1.MutatingAdmissionPolicyBinding{ + Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ + ParamRef: &v1alpha1.ParamRef{}, + MatchResources: &v1alpha1.MatchResources{ + MatchPolicy: ptr.To(v1alpha1.Equivalent), + ObjectSelector: &metav1.LabelSelector{}, + NamespaceSelector: &metav1.LabelSelector{}, + }, }, - }, + } + bindingGenerated = true } hook := mutating.PolicyHook{ Policy: m.policy, Evaluator: m.evaluator, - Bindings: []*mutating.PolicyBinding{binding}, + Bindings: []*mutating.PolicyBinding{m.binding}, } if m.policy.Spec.ParamKind != nil { @@ -322,8 +336,10 @@ func (m *Mutator) dispatchImpl(p MutationParams, dispatcherFactory func(mCtx *mu if err != nil { return nil, fmt.Errorf("failed to access paramObj: %w", err) } - binding.Spec.ParamRef.Name = metaAcc.GetName() - binding.Spec.ParamRef.Namespace = metaAcc.GetNamespace() + if bindingGenerated { + m.binding.Spec.ParamRef.Name = metaAcc.GetName() + m.binding.Spec.ParamRef.Namespace = metaAcc.GetNamespace() + } } // Start informers diff --git a/validation.go b/validation.go index 94845f4..b43b747 100644 --- a/validation.go +++ b/validation.go @@ -43,6 +43,7 @@ type ValidatorInterface interface { type Validator struct { policy *v1.ValidatingAdmissionPolicy + binding *v1.ValidatingAdmissionPolicyBinding validator validating.Validator matcher matchconditions.Matcher } @@ -74,6 +75,13 @@ func NewValidator(policy *v1.ValidatingAdmissionPolicy) *Validator { return &Validator{validator: v, policy: policy, matcher: m} } +// NewValidatorWithBinding compiles the provided ValidatingAdmissionPolicy and generates Validator with provided binding. +func NewValidatorWithBinding(policy *v1.ValidatingAdmissionPolicy, binding *v1.ValidatingAdmissionPolicyBinding) *Validator { + v := NewValidator(policy) + v.binding = binding + return v +} + // Original: https://github.com/kubernetes/apiserver/blob/v0.32.1/pkg/admission/plugin/policy/validating/plugin.go func compilePolicy(policy *v1.ValidatingAdmissionPolicy) (validating.Validator, matchconditions.Matcher) { hasParam := false