Skip to content
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ schemaLocations: # Optional: For Custom Resources
- <path/to/crd/schema.json>
vapTestSuites:
- policy: <name> # ValidatingAdmissionPolicy's name
binding: <name> # Optional: ValidatingAdmissionPolicyBinding's name
tests:
- object:
group: <group> # Optional
Expand All @@ -114,6 +115,7 @@ vapTestSuites:
expect: <allow|deny|skip|error>
mapTestSuites:
- policy: <name> # MutatingAdmissionPolicy's name
binding: <name> # Optional: MutatingAdmissionPolicyBinding's name
tests:
- object:
group: <group> # Optional
Expand Down
34 changes: 26 additions & 8 deletions internal/tester/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
10 changes: 6 additions & 4 deletions internal/tester/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions internal/tester/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions internal/tester/testdata/map-with-params.test/kaptest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mapTestSuites:
name: small
namespace: foo
param:
namespace: hoge
name: config1
expect: mutate
expectObject:
Expand All @@ -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
29 changes: 29 additions & 0 deletions internal/tester/testdata/map-with-params.test/resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,3 +68,11 @@ metadata:
namespace: hoge
data:
maxReplicas: "5"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: config1
namespace: foo
data:
maxReplicas: "10"
10 changes: 10 additions & 0 deletions internal/tester/testdata/map-with-params.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 26 additions & 5 deletions internal/tester/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}))
Expand All @@ -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)
}
Expand Down
38 changes: 27 additions & 11 deletions mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type MutatorInterface interface {

type Mutator struct {
policy *v1alpha1.MutatingAdmissionPolicy
binding *v1alpha1.MutatingAdmissionPolicyBinding
evaluator mutating.PolicyEvaluator
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type ValidatorInterface interface {

type Validator struct {
policy *v1.ValidatingAdmissionPolicy
binding *v1.ValidatingAdmissionPolicyBinding
validator validating.Validator
matcher matchconditions.Matcher
}
Expand Down Expand Up @@ -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
Expand Down
Loading