mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 02:08:13 +00:00 
			
		
		
		
	Reorganize and expand unit test coverage
Also apply reviewer feedback
This commit is contained in:
		| @@ -33,7 +33,7 @@ import ( | |||||||
| 	utilvalidation "k8s.io/apimachinery/pkg/util/validation" | 	utilvalidation "k8s.io/apimachinery/pkg/util/validation" | ||||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||||
| 	plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" | 	plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" | ||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/policy/mutating" | 	"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" | ||||||
| 	validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" | 	validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" | ||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" | 	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" | ||||||
| 	"k8s.io/apiserver/pkg/cel" | 	"k8s.io/apiserver/pkg/cel" | ||||||
| @@ -1493,7 +1493,7 @@ func validateApplyConfiguration(compiler plugincel.Compiler, applyConfig *admiss | |||||||
| 		if opts.preexistingExpressions.applyConfigurationExpressions.Has(applyConfig.Expression) { | 		if opts.preexistingExpressions.applyConfigurationExpressions.Has(applyConfig.Expression) { | ||||||
| 			envType = environment.StoredExpressions | 			envType = environment.StoredExpressions | ||||||
| 		} | 		} | ||||||
| 		accessor := &mutating.ApplyConfigurationCondition{ | 		accessor := &patch.ApplyConfigurationCondition{ | ||||||
| 			Expression: trimmedExpression, | 			Expression: trimmedExpression, | ||||||
| 		} | 		} | ||||||
| 		opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} | 		opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} | ||||||
| @@ -1516,7 +1516,7 @@ func validateJSONPatch(compiler plugincel.Compiler, jsonPatch *admissionregistra | |||||||
| 		if opts.preexistingExpressions.applyConfigurationExpressions.Has(jsonPatch.Expression) { | 		if opts.preexistingExpressions.applyConfigurationExpressions.Has(jsonPatch.Expression) { | ||||||
| 			envType = environment.StoredExpressions | 			envType = environment.StoredExpressions | ||||||
| 		} | 		} | ||||||
| 		accessor := &mutating.JSONPatchCondition{ | 		accessor := &patch.JSONPatchCondition{ | ||||||
| 			Expression: trimmedExpression, | 			Expression: trimmedExpression, | ||||||
| 		} | 		} | ||||||
| 		opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} | 		opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} | ||||||
|   | |||||||
| @@ -202,7 +202,7 @@ func newValidatingAdmissionPolicy(name string) *admissionregistration.Validating | |||||||
| } | } | ||||||
|  |  | ||||||
| func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { | func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { | ||||||
| 	return newStorage(t, nil, replicaLimitsResolver) | 	return newStorage(t, nil, resolver.ResourceResolverFunc(replicaLimitsResolver)) | ||||||
| } | } | ||||||
|  |  | ||||||
| func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) { | func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) { | ||||||
| @@ -227,7 +227,7 @@ func TestCategories(t *testing.T) { | |||||||
| 	registrytest.AssertCategories(t, storage, expected) | 	registrytest.AssertCategories(t, storage, expected) | ||||||
| } | } | ||||||
|  |  | ||||||
| var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { | func replicaLimitsResolver(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { | ||||||
| 	return schema.GroupVersionResource{ | 	return schema.GroupVersionResource{ | ||||||
| 		Group:    "rules.example.com", | 		Group:    "rules.example.com", | ||||||
| 		Version:  "v1", | 		Version:  "v1", | ||||||
|   | |||||||
| @@ -95,7 +95,7 @@ func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator]( | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Start implements generic.Dispatcher Start. It loops through all active hooks | // Dispatch implements generic.Dispatcher. It loops through all active hooks | ||||||
| // (policy x binding pairs) and selects those which are active for the current | // (policy x binding pairs) and selects those which are active for the current | ||||||
| // request. It then resolves all params and creates an Invocation for each | // request. It then resolves all params and creates an Invocation for each | ||||||
| // matching policy-binding-param tuple. The delegate is then called with the | // matching policy-binding-param tuple. The delegate is then called with the | ||||||
| @@ -413,5 +413,5 @@ func (c PolicyError) Error() string { | |||||||
| 		return fmt.Sprintf("policy '%s' with binding '%s' denied request: %s", c.Policy.GetName(), c.Binding.GetName(), c.Message.Error()) | 		return fmt.Sprintf("policy '%s' with binding '%s' denied request: %s", c.Policy.GetName(), c.Binding.GetName(), c.Message.Error()) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return fmt.Sprintf("policy '%s' denied request: %s", c.Policy.GetName(), c.Message.Error()) | 	return fmt.Sprintf("policy %q denied request: %s", c.Policy.GetName(), c.Message.Error()) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -64,13 +64,13 @@ func compilePolicy(policy *Policy) PolicyEvaluator { | |||||||
| 		switch m.PatchType { | 		switch m.PatchType { | ||||||
| 		case v1alpha1.PatchTypeJSONPatch: | 		case v1alpha1.PatchTypeJSONPatch: | ||||||
| 			if m.JSONPatch != nil { | 			if m.JSONPatch != nil { | ||||||
| 				accessor := &JSONPatchCondition{Expression: m.JSONPatch.Expression} | 				accessor := &patch.JSONPatchCondition{Expression: m.JSONPatch.Expression} | ||||||
| 				compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions) | 				compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions) | ||||||
| 				patchers = append(patchers, patch.NewJSONPatcher(compileResult)) | 				patchers = append(patchers, patch.NewJSONPatcher(compileResult)) | ||||||
| 			} | 			} | ||||||
| 		case v1alpha1.PatchTypeApplyConfiguration: | 		case v1alpha1.PatchTypeApplyConfiguration: | ||||||
| 			if m.ApplyConfiguration != nil { | 			if m.ApplyConfiguration != nil { | ||||||
| 				accessor := &ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression} | 				accessor := &patch.ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression} | ||||||
| 				compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions) | 				compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions) | ||||||
| 				patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult)) | 				patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult)) | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -21,16 +21,17 @@ import ( | |||||||
| 	"github.com/google/go-cmp/cmp" | 	"github.com/google/go-cmp/cmp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"k8s.io/api/admissionregistration/v1alpha1" | 	"k8s.io/api/admissionregistration/v1alpha1" | ||||||
| 	appsv1 "k8s.io/api/apps/v1" | 	appsv1 "k8s.io/api/apps/v1" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/api/equality" | 	"k8s.io/apimachinery/pkg/api/equality" | ||||||
| 	"k8s.io/apimachinery/pkg/api/meta" | 	"k8s.io/apimachinery/pkg/api/meta" | ||||||
| 	"k8s.io/apimachinery/pkg/api/resource" |  | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
| 	"k8s.io/apimachinery/pkg/runtime/schema" | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
| 	"k8s.io/apiserver/pkg/admission" | 	"k8s.io/apiserver/pkg/admission" | ||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/cel" | 	"k8s.io/apiserver/pkg/admission/plugin/cel" | ||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" | 	"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" | ||||||
| @@ -45,6 +46,7 @@ import ( | |||||||
| // on the results. | // on the results. | ||||||
| func TestCompilation(t *testing.T) { | func TestCompilation(t *testing.T) { | ||||||
| 	deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} | 	deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} | ||||||
|  | 	deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"} | ||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
| 		name           string | 		name           string | ||||||
| 		policy         *Policy | 		policy         *Policy | ||||||
| @@ -56,429 +58,6 @@ func TestCompilation(t *testing.T) { | |||||||
| 		expectedErr    string | 		expectedErr    string | ||||||
| 		expectedResult runtime.Object | 		expectedResult runtime.Object | ||||||
| 	}{ | 	}{ | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch with false test operation", |  | ||||||
| 			policy: jsonPatches(policy("d1"), |  | ||||||
| 				v1alpha1.JSONPatch{ |  | ||||||
| 					Expression: `[ |  | ||||||
| 						JSONPatch{op: "test", path: "/spec/replicas", value: 100},  |  | ||||||
| 						JSONPatch{op: "replace", path: "/spec/replicas", value: 3}, |  | ||||||
| 					]`, |  | ||||||
| 				}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch with true test operation", |  | ||||||
| 			policy: jsonPatches(policy("d1"), |  | ||||||
| 				v1alpha1.JSONPatch{ |  | ||||||
| 					Expression: `[ |  | ||||||
| 						JSONPatch{op: "test", path: "/spec/replicas", value: 1},  |  | ||||||
| 						JSONPatch{op: "replace", path: "/spec/replicas", value: 3}, |  | ||||||
| 					]`, |  | ||||||
| 				}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch remove to unset field", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "remove", path: "/spec/replicas"},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
|  |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch remove map entry by key", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "remove", path: "/metadata/labels/y"},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1", "y": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch remove element in list", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "remove", path: "/spec/template/spec/containers/1"},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}, {Name: "c"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch copy map entry by key", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "copy", from: "/metadata/labels/x", path: "/metadata/labels/y"},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1", "y": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch copy first element to end of list", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "copy", from: "/spec/template/spec/containers/0", path: "/spec/template/spec/containers/-"},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch move map entry by key", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "move", from: "/metadata/labels/x", path: "/metadata/labels/y"},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch move first element to end of list", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "move", from: "/spec/template/spec/containers/0", path: "/spec/template/spec/containers/-"},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "b"}, {Name: "c"}, {Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch add map entry by key and value", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "add", path: "/metadata/labels/x", value: "2"},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1", "x": "2"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch add map value to field", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "add", path: "/metadata/labels", value: {"y": "2"}},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch add map to existing map", // performs a replacement |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "add", path: "/metadata/labels", value: {"y": "2"}},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch add to start of list", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "add", path: "/spec/template/spec/containers/0", value: {"name": "x"}},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "x"}, {Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch add to end of list", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "add", path: "/spec/template/spec/containers/-", value: {"name": "x"}},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}, {Name: "x"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch replace key in map", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "replace", path: "/metadata/labels/x", value: "2"},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1", "x": "2"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch replace map value of unset field", // adds the field value |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "replace", path: "/metadata/labels", value: {"y": "2"}},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch replace map value of set field", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "replace", path: "/metadata/labels", value: {"y": "2"}},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch replace first element in list", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "replace", path: "/spec/template/spec/containers/0", value: {"name": "x"}},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "x"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch replace end of list with - not allowed", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "replace", path: "/spec/template/spec/containers/-", value: {"name": "x"}},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedErr: "JSON Patch: replace operation does not apply: doc is missing key: /spec/template/spec/containers/-: missing value", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch replace with variable", |  | ||||||
| 			policy: jsonPatches(variables(policy("d1"), v1alpha1.Variable{Name: "desired", Expression: "10"}), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "replace", path: "/spec/replicas", value: variables.desired + 1},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](11)}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch with CEL initializer", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "add", path: "/spec/template/spec/containers/-", value: Object.spec.template.spec.containers{ |  | ||||||
| 							name: "x", |  | ||||||
| 							ports: [Object.spec.template.spec.containers.ports{containerPort: 8080}], |  | ||||||
| 						} |  | ||||||
| 					},  |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}, {Name: "x", Ports: []corev1.ContainerPort{{ContainerPort: 8080}}}}, |  | ||||||
| 			}}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch invalid CEL initializer field", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{ |  | ||||||
| 						op: "add", path: "/spec/template/spec/containers/-",  |  | ||||||
| 						value: Object.spec.template.spec.containers{ |  | ||||||
| 							name: "x", |  | ||||||
| 							ports: [Object.spec.template.spec.containers.ports{containerPortZ: 8080}] |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedErr: "strict decoding error: unknown field \"spec.template.spec.containers[1].ports[0].containerPortZ\"", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch invalid CEL initializer type", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{ |  | ||||||
| 						op: "add", path: "/spec/template/spec/containers/-",  |  | ||||||
| 						value: Object.spec.template.spec.containers{ |  | ||||||
| 							name: "x", |  | ||||||
| 							ports: [Object.spec.template.spec.containers.portsZ{containerPort: 8080}] |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ |  | ||||||
| 				Containers: []corev1.Container{{Name: "a"}}, |  | ||||||
| 			}}}}, |  | ||||||
| 			expectedErr: " mismatch: unexpected type name \"Object.spec.template.spec.containers.portsZ\", expected \"Object.spec.template.spec.containers.ports\", which matches field name path from root Object type", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "jsonPatch add map entry by key and value", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{op: "add", path: "/spec", value: Object.spec{selector: Object.spec.selector{}, replicas: 10}} |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Selector: &metav1.LabelSelector{}, Replicas: ptr.To[int32](10)}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "JSONPatch patch type has field access", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{ |  | ||||||
| 						op: "add", path: "/metadata/labels", |  | ||||||
| 						value: { |  | ||||||
| 							"op": JSONPatch{op: "opValue"}.op, |  | ||||||
| 							"path": JSONPatch{path: "pathValue"}.path, |  | ||||||
| 							"from": JSONPatch{from: "fromValue"}.from, |  | ||||||
| 							"value": string(JSONPatch{value: "valueValue"}.value), |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:    deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ |  | ||||||
| 				"op":    "opValue", |  | ||||||
| 				"path":  "pathValue", |  | ||||||
| 				"from":  "fromValue", |  | ||||||
| 				"value": "valueValue", |  | ||||||
| 			}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "JSONPatch patch type has field testing", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{ |  | ||||||
| 						op: "add", path: "/metadata/labels", |  | ||||||
| 						value: { |  | ||||||
| 							"op": string(has(JSONPatch{op: "opValue"}.op)), |  | ||||||
| 							"path": string(has(JSONPatch{path: "pathValue"}.path)), |  | ||||||
| 							"from": string(has(JSONPatch{from: "fromValue"}.from)), |  | ||||||
| 							"value": string(has(JSONPatch{value: "valueValue"}.value)), |  | ||||||
| 							"op-unset": string(has(JSONPatch{}.op)), |  | ||||||
| 							"path-unset": string(has(JSONPatch{}.path)), |  | ||||||
| 							"from-unset": string(has(JSONPatch{}.from)), |  | ||||||
| 							"value-unset": string(has(JSONPatch{}.value)), |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:    deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ |  | ||||||
| 				"op":          "true", |  | ||||||
| 				"path":        "true", |  | ||||||
| 				"from":        "true", |  | ||||||
| 				"value":       "true", |  | ||||||
| 				"op-unset":    "false", |  | ||||||
| 				"path-unset":  "false", |  | ||||||
| 				"from-unset":  "false", |  | ||||||
| 				"value-unset": "false", |  | ||||||
| 			}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "JSONPatch patch type equality", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{ |  | ||||||
| 						op: "add", path: "/metadata/labels", |  | ||||||
| 						value: { |  | ||||||
| 							"empty": string(JSONPatch{} == JSONPatch{}), |  | ||||||
| 							"partial": string(JSONPatch{op: "add"} == JSONPatch{op: "add"}), |  | ||||||
| 							"same-all": string(JSONPatch{op: "add", path: "path", from: "from", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}), |  | ||||||
| 							"different-op": string(JSONPatch{op: "add"} == JSONPatch{op: "remove"}), |  | ||||||
| 							"different-path": string(JSONPatch{op: "add", path: "x", from: "from", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}), |  | ||||||
| 							"different-from": string(JSONPatch{op: "add", path: "path", from: "x", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}), |  | ||||||
| 							"different-value": string(JSONPatch{op: "add", path: "path", from: "from", value: "1"} == JSONPatch{op: "add", path: "path", from: "from", value: 1}), |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:    deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ |  | ||||||
| 				"empty":           "true", |  | ||||||
| 				"partial":         "true", |  | ||||||
| 				"same-all":        "true", |  | ||||||
| 				"different-op":    "false", |  | ||||||
| 				"different-path":  "false", |  | ||||||
| 				"different-from":  "false", |  | ||||||
| 				"different-value": "false", |  | ||||||
| 			}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "JSONPatch key escaping", |  | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ |  | ||||||
| 				Expression: `[ |  | ||||||
| 					JSONPatch{ |  | ||||||
| 						op: "add", path: "/metadata/labels", value: {} |  | ||||||
| 					}, |  | ||||||
| 					JSONPatch{ |  | ||||||
| 						op: "add", path: "/metadata/labels/" + jsonpatch.escapeKey("k8s.io/x~y"), value: "true" |  | ||||||
| 					} |  | ||||||
| 				]`, |  | ||||||
| 			}), |  | ||||||
| 			gvr:    deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ |  | ||||||
| 				"k8s.io/x~y": "true", |  | ||||||
| 			}}}, |  | ||||||
| 		}, |  | ||||||
| 		{ | 		{ | ||||||
| 			name: "applyConfiguration then jsonPatch", | 			name: "applyConfiguration then jsonPatch", | ||||||
| 			policy: mutations(policy("d1"), v1alpha1.Mutation{ | 			policy: mutations(policy("d1"), v1alpha1.Mutation{ | ||||||
| @@ -529,92 +108,15 @@ func TestCompilation(t *testing.T) { | |||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](111)}}, | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](111)}}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "apply configuration add to listType=map", | 			name: "jsonPatch with variable", | ||||||
| 			policy: applyConfigurations(policy("d1"), | 			policy: jsonPatches(variables(policy("d1"), v1alpha1.Variable{Name: "desired", Expression: "10"}), v1alpha1.JSONPatch{ | ||||||
| 				`Object{ | 				Expression: `[ | ||||||
| 					spec: Object.spec{ | 					JSONPatch{op: "replace", path: "/spec/replicas", value: variables.desired + 1},  | ||||||
| 						template: Object.spec.template{ | 				]`, | ||||||
| 							spec: Object.spec.template.spec{ | 			}), | ||||||
| 								volumes: [Object.spec.template.spec.volumes{ |  | ||||||
| 									name: "y" |  | ||||||
| 								}] |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				}`), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ |  | ||||||
| 				Template: corev1.PodTemplateSpec{ |  | ||||||
| 					Spec: corev1.PodSpec{ |  | ||||||
| 						Volumes: []corev1.Volume{{Name: "x"}}, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ |  | ||||||
| 				Template: corev1.PodTemplateSpec{ |  | ||||||
| 					Spec: corev1.PodSpec{ |  | ||||||
| 						Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}}, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "apply configuration update listType=map entry", |  | ||||||
| 			policy: applyConfigurations(policy("d1"), |  | ||||||
| 				`Object{ |  | ||||||
| 					spec: Object.spec{ |  | ||||||
| 						template: Object.spec.template{ |  | ||||||
| 							spec: Object.spec.template.spec{ |  | ||||||
| 								volumes: [Object.spec.template.spec.volumes{ |  | ||||||
| 									name: "y", |  | ||||||
| 									hostPath: Object.spec.template.spec.volumes.hostPath{ |  | ||||||
| 										path: "a" |  | ||||||
| 									} |  | ||||||
| 								}] |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				}`), |  | ||||||
| 			gvr: deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ |  | ||||||
| 				Template: corev1.PodTemplateSpec{ |  | ||||||
| 					Spec: corev1.PodSpec{ |  | ||||||
| 						Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}}, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ |  | ||||||
| 				Template: corev1.PodTemplateSpec{ |  | ||||||
| 					Spec: corev1.PodSpec{ |  | ||||||
| 						Volumes: []corev1.Volume{{Name: "x"}, {Name: "y", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "a"}}}}, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "apply configuration with conditionals", |  | ||||||
| 			policy: applyConfigurations(policy("d1"), ` |  | ||||||
| 				Object{ |  | ||||||
| 					spec: Object.spec{ |  | ||||||
| 						replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas |  | ||||||
| 					} |  | ||||||
| 				}`), |  | ||||||
| 			gvr:            deploymentGVR, |  | ||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "apply configuration with old object", |  | ||||||
| 			policy: applyConfigurations(policy("d1"), |  | ||||||
| 				`Object{ |  | ||||||
| 					spec: Object.spec{ |  | ||||||
| 						replicas: oldObject.spec.replicas % 2 == 0?oldObject.spec.replicas + 1:oldObject.spec.replicas |  | ||||||
| 					} |  | ||||||
| 				}`), |  | ||||||
| 			gvr:            deploymentGVR, | 			gvr:            deploymentGVR, | ||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
| 			oldObject:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}}, | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](11)}}, | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "apply configuration with variable", | 			name: "apply configuration with variable", | ||||||
| @@ -641,95 +143,6 @@ func TestCompilation(t *testing.T) { | |||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](100)}}, | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](100)}}, | ||||||
| 		}, | 		}, | ||||||
| 		{ |  | ||||||
| 			name: "complex apply configuration initialization", |  | ||||||
| 			policy: applyConfigurations(policy("d1"), |  | ||||||
| 				`Object{ |  | ||||||
| 					spec: Object.spec{ |  | ||||||
| 						replicas: 1, |  | ||||||
| 						template: Object.spec.template{ |  | ||||||
| 							metadata: Object.spec.template.metadata{ |  | ||||||
| 								labels: {"app": "nginx"} |  | ||||||
| 							}, |  | ||||||
| 							spec: Object.spec.template.spec{ |  | ||||||
| 								containers: [Object.spec.template.spec.containers{ |  | ||||||
| 									name: "nginx", |  | ||||||
| 									image: "nginx:1.14.2", |  | ||||||
| 									ports: [Object.spec.template.spec.containers.ports{ |  | ||||||
| 										containerPort: 80 |  | ||||||
| 									}], |  | ||||||
| 									resources: Object.spec.template.spec.containers.resources{ |  | ||||||
| 										limits: {"cpu": "128M"}, |  | ||||||
| 									} |  | ||||||
| 								}] |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				}`), |  | ||||||
|  |  | ||||||
| 			gvr:    deploymentGVR, |  | ||||||
| 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, |  | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ |  | ||||||
| 				Replicas: ptr.To[int32](1), |  | ||||||
| 				Template: corev1.PodTemplateSpec{ |  | ||||||
| 					ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 						Labels: map[string]string{"app": "nginx"}, |  | ||||||
| 					}, |  | ||||||
| 					Spec: corev1.PodSpec{ |  | ||||||
| 						Containers: []corev1.Container{{ |  | ||||||
| 							Name:  "nginx", |  | ||||||
| 							Image: "nginx:1.14.2", |  | ||||||
| 							Ports: []corev1.ContainerPort{ |  | ||||||
| 								{ContainerPort: 80}, |  | ||||||
| 							}, |  | ||||||
| 							Resources: corev1.ResourceRequirements{ |  | ||||||
| 								Limits: corev1.ResourceList{corev1.ResourceName("cpu"): resource.MustParse("128M")}, |  | ||||||
| 							}, |  | ||||||
| 						}}, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "apply configuration with invalid type name", |  | ||||||
| 			policy: applyConfigurations(policy("d1"), |  | ||||||
| 				`Object{ |  | ||||||
| 					spec: Object.specx{ |  | ||||||
| 						replicas: 1 |  | ||||||
| 					} |  | ||||||
| 				}`), |  | ||||||
| 			gvr:         deploymentGVR, |  | ||||||
| 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 			expectedErr: "type mismatch: unexpected type name \"Object.specx\", expected \"Object.spec\", which matches field name path from root Object type", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "apply configuration with invalid field name", |  | ||||||
| 			policy: applyConfigurations(policy("d1"), |  | ||||||
| 				`Object{ |  | ||||||
| 					spec: Object.spec{ |  | ||||||
| 						replicasx: 1 |  | ||||||
| 					} |  | ||||||
| 				}`), |  | ||||||
| 			gvr:         deploymentGVR, |  | ||||||
| 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 			expectedErr: "error applying patch: failed to convert patch object to typed object: .spec.replicasx: field not declared in schema", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "apply configuration with invalid return type", |  | ||||||
| 			policy: applyConfigurations(policy("d1"), |  | ||||||
| 				`"I'm a teapot!"`), |  | ||||||
| 			gvr:         deploymentGVR, |  | ||||||
| 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 			expectedErr: "must evaluate to Object but got string", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "apply configuration with invalid initializer return type", |  | ||||||
| 			policy: applyConfigurations(policy("d1"), |  | ||||||
| 				`Object.spec.metadata{}`), |  | ||||||
| 			gvr:         deploymentGVR, |  | ||||||
| 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 			expectedErr: "must evaluate to Object but got Object.spec.metadata", |  | ||||||
| 		}, |  | ||||||
| 		{ | 		{ | ||||||
| 			name: "jsonPatch with excessive cost", | 			name: "jsonPatch with excessive cost", | ||||||
| 			policy: jsonPatches(variables(policy("d1"), v1alpha1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}), v1alpha1.JSONPatch{ | 			policy: jsonPatches(variables(policy("d1"), v1alpha1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}), v1alpha1.JSONPatch{ | ||||||
| @@ -793,20 +206,6 @@ func TestCompilation(t *testing.T) { | |||||||
| 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](10)}}, | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](10)}}, | ||||||
| 		}, | 		}, | ||||||
| 		{ |  | ||||||
| 			name: "apply configuration with change to atomic", |  | ||||||
| 			policy: applyConfigurations(policy("d1"), |  | ||||||
| 				`Object{ |  | ||||||
| 					spec: Object.spec{ |  | ||||||
| 						selector: Object.spec.selector{ |  | ||||||
| 							matchLabels: {"l": "v"} |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				}`), |  | ||||||
| 			gvr:         deploymentGVR, |  | ||||||
| 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, |  | ||||||
| 			expectedErr: "error applying patch: invalid ApplyConfiguration: may not mutate atomic arrays, maps or structs: .spec.selector", |  | ||||||
| 		}, |  | ||||||
| 		{ | 		{ | ||||||
| 			name: "object type has field access", | 			name: "object type has field access", | ||||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ | 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ | ||||||
| @@ -901,10 +300,18 @@ func TestCompilation(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
| 	defer cancel() | 	t.Cleanup(cancel) | ||||||
| 	tcManager := patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient()) | 	tcManager := patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient()) | ||||||
| 	go tcManager.Run(ctx) | 	go tcManager.Run(ctx) | ||||||
|  |  | ||||||
|  | 	err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) { | ||||||
|  | 		converter := tcManager.GetTypeConverter(deploymentGVK) | ||||||
|  | 		return converter != nil, nil | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for _, tc := range testCases { | 	for _, tc := range testCases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
| 			var gvk schema.GroupVersionKind | 			var gvk schema.GroupVersionKind | ||||||
| @@ -939,7 +346,7 @@ func TestCompilation(t *testing.T) { | |||||||
|  |  | ||||||
| 			for _, patcher := range policyEvaluator.Mutators { | 			for _, patcher := range policyEvaluator.Mutators { | ||||||
| 				attrs := admission.NewAttributesRecord(obj, tc.oldObject, gvk, | 				attrs := admission.NewAttributesRecord(obj, tc.oldObject, gvk, | ||||||
| 					metaAccessor.GetName(), metaAccessor.GetNamespace(), tc.gvr, | 					metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr, | ||||||
| 					"", admission.Create, &metav1.CreateOptions{}, false, nil) | 					"", admission.Create, &metav1.CreateOptions{}, false, nil) | ||||||
| 				vAttrs := &admission.VersionedAttributes{ | 				vAttrs := &admission.VersionedAttributes{ | ||||||
| 					Attributes:         attrs, | 					Attributes:         attrs, | ||||||
| @@ -1038,6 +445,11 @@ func mutations(policy *v1alpha1.MutatingAdmissionPolicy, mutations ...v1alpha1.M | |||||||
| 	return policy | 	return policy | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func matchConstraints(policy *v1alpha1.MutatingAdmissionPolicy, matchConstraints *v1alpha1.MatchResources) *v1alpha1.MutatingAdmissionPolicy { | ||||||
|  | 	policy.Spec.MatchConstraints = matchConstraints | ||||||
|  | 	return policy | ||||||
|  | } | ||||||
|  |  | ||||||
| type fakeAuthorizer struct{} | type fakeAuthorizer struct{} | ||||||
|  |  | ||||||
| func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { | func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { | ||||||
|   | |||||||
| @@ -42,9 +42,8 @@ import ( | |||||||
|  |  | ||||||
| func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher, tcm patch.TypeConverterManager) generic.Dispatcher[PolicyHook] { | func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher, tcm patch.TypeConverterManager) generic.Dispatcher[PolicyHook] { | ||||||
| 	res := &dispatcher{ | 	res := &dispatcher{ | ||||||
| 		matcher: m, | 		matcher:              m, | ||||||
| 		authz:   a, | 		authz:                a, | ||||||
| 		//!TODO: pass in static type converter to reduce network calls |  | ||||||
| 		typeConverterManager: tcm, | 		typeConverterManager: tcm, | ||||||
| 	} | 	} | ||||||
| 	res.Dispatcher = generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator]( | 	res.Dispatcher = generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator]( | ||||||
| @@ -138,7 +137,7 @@ func (d *dispatcher) dispatchInvocations( | |||||||
| 			// This would be a bug. The compiler should always return exactly as | 			// This would be a bug. The compiler should always return exactly as | ||||||
| 			// many evaluators as there are mutations | 			// many evaluators as there are mutations | ||||||
| 			return nil, k8serrors.NewInternalError(fmt.Errorf("expected %v compiled evaluators for policy %v, got %v", | 			return nil, k8serrors.NewInternalError(fmt.Errorf("expected %v compiled evaluators for policy %v, got %v", | ||||||
| 				invocation.Policy.Name, len(invocation.Policy.Spec.Mutations), len(invocation.Evaluator.Mutators))) | 				len(invocation.Policy.Spec.Mutations), invocation.Policy.Name, len(invocation.Evaluator.Mutators))) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		versionedAttr, err := versionedAttributes.VersionedAttribute(invocation.Kind) | 		versionedAttr, err := versionedAttributes.VersionedAttribute(invocation.Kind) | ||||||
| @@ -152,6 +151,7 @@ func (d *dispatcher) dispatchInvocations( | |||||||
| 			matchResults := invocation.Evaluator.Matcher.Match(ctx, versionedAttr, invocation.Param, authz) | 			matchResults := invocation.Evaluator.Matcher.Match(ctx, versionedAttr, invocation.Param, authz) | ||||||
| 			if matchResults.Error != nil { | 			if matchResults.Error != nil { | ||||||
| 				addConfigError(matchResults.Error, invocation, metav1.StatusReasonInvalid) | 				addConfigError(matchResults.Error, invocation, metav1.StatusReasonInvalid) | ||||||
|  | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// if preconditions are not met, then skip mutations | 			// if preconditions are not met, then skip mutations | ||||||
|   | |||||||
| @@ -0,0 +1,675 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2024 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package mutating | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"github.com/google/go-cmp/cmp" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	admissionregistrationv1 "k8s.io/api/admissionregistration/v1" | ||||||
|  | 	"k8s.io/api/admissionregistration/v1alpha1" | ||||||
|  | 	appsv1 "k8s.io/api/apps/v1" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/equality" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/meta" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"k8s.io/apiserver/pkg/admission" | ||||||
|  | 	"k8s.io/apiserver/pkg/admission/plugin/policy/generic" | ||||||
|  | 	"k8s.io/apiserver/pkg/admission/plugin/policy/matching" | ||||||
|  | 	"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" | ||||||
|  | 	"k8s.io/client-go/informers" | ||||||
|  | 	"k8s.io/client-go/kubernetes/fake" | ||||||
|  | 	"k8s.io/client-go/openapi/openapitest" | ||||||
|  | 	"k8s.io/utils/ptr" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestDispatcher(t *testing.T) { | ||||||
|  | 	deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"} | ||||||
|  | 	deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name              string | ||||||
|  | 		object, oldObject runtime.Object | ||||||
|  | 		gvk               schema.GroupVersionKind | ||||||
|  | 		gvr               schema.GroupVersionResource | ||||||
|  | 		params            []runtime.Object // All params are expected to be ConfigMap for this test. | ||||||
|  | 		policyHooks       []PolicyHook | ||||||
|  | 		expect            runtime.Object | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "simple patch", | ||||||
|  | 			gvk:  deploymentGVK, | ||||||
|  | 			gvr:  deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{ | ||||||
|  | 				TypeMeta: metav1.TypeMeta{ | ||||||
|  | 					Kind:       "Deployment", | ||||||
|  | 					APIVersion: "apps/v1", | ||||||
|  | 				}, | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Replicas: ptr.To[int32](1), | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 			policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ | ||||||
|  | 				{ | ||||||
|  | 					Policy: mutations(matchConstraints(policy("policy1"), &v1alpha1.MatchResources{ | ||||||
|  | 						MatchPolicy:       ptr.To(v1alpha1.Equivalent), | ||||||
|  | 						NamespaceSelector: &metav1.LabelSelector{}, | ||||||
|  | 						ObjectSelector:    &metav1.LabelSelector{}, | ||||||
|  | 						ResourceRules: []v1alpha1.NamedRuleWithOperations{ | ||||||
|  | 							{ | ||||||
|  | 								RuleWithOperations: v1alpha1.RuleWithOperations{ | ||||||
|  | 									Rule: v1alpha1.Rule{ | ||||||
|  | 										APIGroups:   []string{"apps"}, | ||||||
|  | 										APIVersions: []string{"v1"}, | ||||||
|  | 										Resources:   []string{"deployments"}, | ||||||
|  | 									}, | ||||||
|  | 									Operations: []admissionregistrationv1.OperationType{"*"}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}), v1alpha1.Mutation{ | ||||||
|  | 						PatchType: v1alpha1.PatchTypeApplyConfiguration, | ||||||
|  | 						ApplyConfiguration: &v1alpha1.ApplyConfiguration{ | ||||||
|  | 							Expression: `Object{ | ||||||
|  | 									spec: Object.spec{ | ||||||
|  | 										replicas: object.spec.replicas + 100 | ||||||
|  | 									} | ||||||
|  | 								}`, | ||||||
|  | 						}}), | ||||||
|  | 					Bindings: []*PolicyBinding{{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{Name: "binding"}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ | ||||||
|  | 							PolicyName: "policy1", | ||||||
|  | 						}, | ||||||
|  | 					}}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			expect: &appsv1.Deployment{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Replicas: ptr.To[int32](101), | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "with param", | ||||||
|  | 			gvk:  deploymentGVK, | ||||||
|  | 			gvr:  deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{ | ||||||
|  | 				TypeMeta: metav1.TypeMeta{ | ||||||
|  | 					Kind:       "Deployment", | ||||||
|  | 					APIVersion: "apps/v1", | ||||||
|  | 				}, | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Replicas: ptr.To[int32](1), | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 			params: []runtime.Object{ | ||||||
|  | 				&corev1.ConfigMap{ | ||||||
|  | 					TypeMeta: metav1.TypeMeta{ | ||||||
|  | 						APIVersion: "v1", | ||||||
|  | 						Kind:       "ConfigMap", | ||||||
|  | 					}, | ||||||
|  | 					ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 						Name:      "cm1", | ||||||
|  | 						Namespace: "default", | ||||||
|  | 					}, | ||||||
|  | 					Data: map[string]string{ | ||||||
|  | 						"key": "10", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ | ||||||
|  | 				{ | ||||||
|  | 					Policy: paramKind(mutations(matchConstraints(policy("policy1"), &v1alpha1.MatchResources{ | ||||||
|  | 						MatchPolicy:       ptr.To(v1alpha1.Equivalent), | ||||||
|  | 						NamespaceSelector: &metav1.LabelSelector{}, | ||||||
|  | 						ObjectSelector:    &metav1.LabelSelector{}, | ||||||
|  | 						ResourceRules: []v1alpha1.NamedRuleWithOperations{ | ||||||
|  | 							{ | ||||||
|  | 								RuleWithOperations: v1alpha1.RuleWithOperations{ | ||||||
|  | 									Rule: v1alpha1.Rule{ | ||||||
|  | 										APIGroups:   []string{"apps"}, | ||||||
|  | 										APIVersions: []string{"v1"}, | ||||||
|  | 										Resources:   []string{"deployments"}, | ||||||
|  | 									}, | ||||||
|  | 									Operations: []admissionregistrationv1.OperationType{"*"}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}}), | ||||||
|  | 						v1alpha1.Mutation{ | ||||||
|  | 							PatchType: v1alpha1.PatchTypeApplyConfiguration, | ||||||
|  | 							ApplyConfiguration: &v1alpha1.ApplyConfiguration{ | ||||||
|  | 								Expression: `Object{ | ||||||
|  | 									spec: Object.spec{ | ||||||
|  | 										replicas: object.spec.replicas + int(params.data['key']) | ||||||
|  | 									} | ||||||
|  | 								}`, | ||||||
|  | 							}}), | ||||||
|  | 						&v1alpha1.ParamKind{ | ||||||
|  | 							APIVersion: "v1", | ||||||
|  | 							Kind:       "ConfigMap", | ||||||
|  | 						}), | ||||||
|  | 					Bindings: []*PolicyBinding{{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{Name: "binding"}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ | ||||||
|  | 							PolicyName: "policy1", | ||||||
|  | 							ParamRef:   &v1alpha1.ParamRef{Name: "cm1", Namespace: "default"}, | ||||||
|  | 						}, | ||||||
|  | 					}}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			expect: &appsv1.Deployment{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Replicas: ptr.To[int32](11), | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "both policies reinvoked", | ||||||
|  | 			gvk:  deploymentGVK, | ||||||
|  | 			gvr:  deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{ | ||||||
|  | 				TypeMeta: metav1.TypeMeta{ | ||||||
|  | 					Kind:       "Deployment", | ||||||
|  | 					APIVersion: "apps/v1", | ||||||
|  | 				}, | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 			policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ | ||||||
|  | 				{ | ||||||
|  | 					Policy: mutations(matchConstraints(policy("policy1"), &v1alpha1.MatchResources{ | ||||||
|  | 						MatchPolicy:       ptr.To(v1alpha1.Equivalent), | ||||||
|  | 						NamespaceSelector: &metav1.LabelSelector{}, | ||||||
|  | 						ObjectSelector:    &metav1.LabelSelector{}, | ||||||
|  | 						ResourceRules: []v1alpha1.NamedRuleWithOperations{ | ||||||
|  | 							{ | ||||||
|  | 								RuleWithOperations: v1alpha1.RuleWithOperations{ | ||||||
|  | 									Rule: v1alpha1.Rule{ | ||||||
|  | 										APIGroups:   []string{"apps"}, | ||||||
|  | 										APIVersions: []string{"v1"}, | ||||||
|  | 										Resources:   []string{"deployments"}, | ||||||
|  | 									}, | ||||||
|  | 									Operations: []admissionregistrationv1.OperationType{"*"}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}), v1alpha1.Mutation{ | ||||||
|  | 						PatchType: v1alpha1.PatchTypeApplyConfiguration, | ||||||
|  | 						ApplyConfiguration: &v1alpha1.ApplyConfiguration{ | ||||||
|  | 							Expression: `Object{ | ||||||
|  | 									metadata: Object.metadata{ | ||||||
|  | 										labels: {"policy1": string(int(object.?metadata.labels["count"].orValue("1")) + 1)} | ||||||
|  | 									} | ||||||
|  | 								}`, | ||||||
|  | 						}}), | ||||||
|  | 					Bindings: []*PolicyBinding{{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{Name: "binding"}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ | ||||||
|  | 							PolicyName: "policy1", | ||||||
|  | 						}, | ||||||
|  | 					}}, | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					Policy: mutations(matchConstraints(policy("policy2"), &v1alpha1.MatchResources{ | ||||||
|  | 						MatchPolicy:       ptr.To(v1alpha1.Equivalent), | ||||||
|  | 						NamespaceSelector: &metav1.LabelSelector{}, | ||||||
|  | 						ObjectSelector:    &metav1.LabelSelector{}, | ||||||
|  | 						ResourceRules: []v1alpha1.NamedRuleWithOperations{ | ||||||
|  | 							{ | ||||||
|  | 								RuleWithOperations: v1alpha1.RuleWithOperations{ | ||||||
|  | 									Rule: v1alpha1.Rule{ | ||||||
|  | 										APIGroups:   []string{"apps"}, | ||||||
|  | 										APIVersions: []string{"v1"}, | ||||||
|  | 										Resources:   []string{"deployments"}, | ||||||
|  | 									}, | ||||||
|  | 									Operations: []admissionregistrationv1.OperationType{"*"}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}), v1alpha1.Mutation{ | ||||||
|  | 						PatchType: v1alpha1.PatchTypeApplyConfiguration, | ||||||
|  | 						ApplyConfiguration: &v1alpha1.ApplyConfiguration{ | ||||||
|  | 							Expression: `Object{ | ||||||
|  | 									metadata: Object.metadata{ | ||||||
|  | 										labels: {"policy2": string(int(object.?metadata.labels["count"].orValue("1")) + 1)} | ||||||
|  | 									} | ||||||
|  | 								}`, | ||||||
|  | 						}}), | ||||||
|  | 					Bindings: []*PolicyBinding{{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{Name: "binding"}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ | ||||||
|  | 							PolicyName: "policy2", | ||||||
|  | 						}, | ||||||
|  | 					}}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			expect: &appsv1.Deployment{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 					Labels: map[string]string{ | ||||||
|  | 						"policy1": "2", | ||||||
|  | 						"policy2": "2", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "1st policy sets match condition that 2nd policy matches", | ||||||
|  | 			gvk:  deploymentGVK, | ||||||
|  | 			gvr:  deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{ | ||||||
|  | 				TypeMeta: metav1.TypeMeta{ | ||||||
|  | 					Kind:       "Deployment", | ||||||
|  | 					APIVersion: "apps/v1", | ||||||
|  | 				}, | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 			policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ | ||||||
|  | 				{ | ||||||
|  | 					Policy: &v1alpha1.MutatingAdmissionPolicy{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 							Name: "policy1", | ||||||
|  | 						}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicySpec{ | ||||||
|  | 							MatchConstraints: &v1alpha1.MatchResources{ | ||||||
|  | 								MatchPolicy:       ptr.To(v1alpha1.Equivalent), | ||||||
|  | 								NamespaceSelector: &metav1.LabelSelector{}, | ||||||
|  | 								ObjectSelector:    &metav1.LabelSelector{}, | ||||||
|  | 								ResourceRules: []v1alpha1.NamedRuleWithOperations{ | ||||||
|  | 									{ | ||||||
|  | 										RuleWithOperations: v1alpha1.RuleWithOperations{ | ||||||
|  | 											Rule: v1alpha1.Rule{ | ||||||
|  | 												APIGroups:   []string{"apps"}, | ||||||
|  | 												APIVersions: []string{"v1"}, | ||||||
|  | 												Resources:   []string{"deployments"}, | ||||||
|  | 											}, | ||||||
|  | 											Operations: []admissionregistrationv1.OperationType{"*"}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							Mutations: []v1alpha1.Mutation{{ | ||||||
|  | 								PatchType: v1alpha1.PatchTypeApplyConfiguration, | ||||||
|  | 								ApplyConfiguration: &v1alpha1.ApplyConfiguration{ | ||||||
|  | 									Expression: `Object{ | ||||||
|  | 									metadata: Object.metadata{ | ||||||
|  | 										labels: {"environment": "production"} | ||||||
|  | 									} | ||||||
|  | 								}`}}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					Bindings: []*PolicyBinding{{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{Name: "binding"}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ | ||||||
|  | 							PolicyName: "policy1", | ||||||
|  | 						}, | ||||||
|  | 					}}, | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					Policy: &v1alpha1.MutatingAdmissionPolicy{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 							Name: "policy2", | ||||||
|  | 						}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicySpec{ | ||||||
|  | 							MatchConstraints: &v1alpha1.MatchResources{ | ||||||
|  | 								MatchPolicy:       ptr.To(v1alpha1.Equivalent), | ||||||
|  | 								NamespaceSelector: &metav1.LabelSelector{}, | ||||||
|  | 								ObjectSelector:    &metav1.LabelSelector{}, | ||||||
|  | 								ResourceRules: []v1alpha1.NamedRuleWithOperations{ | ||||||
|  | 									{ | ||||||
|  | 										RuleWithOperations: v1alpha1.RuleWithOperations{ | ||||||
|  | 											Rule: v1alpha1.Rule{ | ||||||
|  | 												APIGroups:   []string{"apps"}, | ||||||
|  | 												APIVersions: []string{"v1"}, | ||||||
|  | 												Resources:   []string{"deployments"}, | ||||||
|  | 											}, | ||||||
|  | 											Operations: []admissionregistrationv1.OperationType{"*"}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							MatchConditions: []v1alpha1.MatchCondition{ | ||||||
|  | 								{ | ||||||
|  | 									Name:       "prodonly", | ||||||
|  | 									Expression: `object.?metadata.labels["environment"].orValue("") == "production"`, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							Mutations: []v1alpha1.Mutation{{ | ||||||
|  | 								PatchType: v1alpha1.PatchTypeApplyConfiguration, | ||||||
|  | 								ApplyConfiguration: &v1alpha1.ApplyConfiguration{ | ||||||
|  | 									Expression: `Object{ | ||||||
|  | 									metadata: Object.metadata{ | ||||||
|  | 										labels: {"policy1invoked": "true"} | ||||||
|  | 									} | ||||||
|  | 								}`}}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					Bindings: []*PolicyBinding{{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{Name: "binding"}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ | ||||||
|  | 							PolicyName: "policy2", | ||||||
|  | 						}, | ||||||
|  | 					}}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			expect: &appsv1.Deployment{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 					Labels: map[string]string{ | ||||||
|  | 						"environment":    "production", | ||||||
|  | 						"policy1invoked": "true", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// TODO: This behavior pre-exists with webhook match conditions but should be reconsidered | ||||||
|  | 			name: "1st policy still does not match after 2nd policy sets match condition", | ||||||
|  | 			gvk:  deploymentGVK, | ||||||
|  | 			gvr:  deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{ | ||||||
|  | 				TypeMeta: metav1.TypeMeta{ | ||||||
|  | 					Kind:       "Deployment", | ||||||
|  | 					APIVersion: "apps/v1", | ||||||
|  | 				}, | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 			policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ | ||||||
|  | 				{ | ||||||
|  | 					Policy: &v1alpha1.MutatingAdmissionPolicy{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 							Name: "policy1", | ||||||
|  | 						}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicySpec{ | ||||||
|  | 							MatchConstraints: &v1alpha1.MatchResources{ | ||||||
|  | 								MatchPolicy:       ptr.To(v1alpha1.Equivalent), | ||||||
|  | 								NamespaceSelector: &metav1.LabelSelector{}, | ||||||
|  | 								ObjectSelector:    &metav1.LabelSelector{}, | ||||||
|  | 								ResourceRules: []v1alpha1.NamedRuleWithOperations{ | ||||||
|  | 									{ | ||||||
|  | 										RuleWithOperations: v1alpha1.RuleWithOperations{ | ||||||
|  | 											Rule: v1alpha1.Rule{ | ||||||
|  | 												APIGroups:   []string{"apps"}, | ||||||
|  | 												APIVersions: []string{"v1"}, | ||||||
|  | 												Resources:   []string{"deployments"}, | ||||||
|  | 											}, | ||||||
|  | 											Operations: []admissionregistrationv1.OperationType{"*"}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							MatchConditions: []v1alpha1.MatchCondition{ | ||||||
|  | 								{ | ||||||
|  | 									Name:       "prodonly", | ||||||
|  | 									Expression: `object.?metadata.labels["environment"].orValue("") == "production"`, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							Mutations: []v1alpha1.Mutation{{ | ||||||
|  | 								PatchType: v1alpha1.PatchTypeApplyConfiguration, | ||||||
|  | 								ApplyConfiguration: &v1alpha1.ApplyConfiguration{ | ||||||
|  | 									Expression: `Object{ | ||||||
|  | 									metadata: Object.metadata{ | ||||||
|  | 										labels: {"policy1invoked": "true"} | ||||||
|  | 									} | ||||||
|  | 								}`}}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					Bindings: []*PolicyBinding{{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{Name: "binding"}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ | ||||||
|  | 							PolicyName: "policy1", | ||||||
|  | 						}, | ||||||
|  | 					}}, | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					Policy: &v1alpha1.MutatingAdmissionPolicy{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 							Name: "policy2", | ||||||
|  | 						}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicySpec{ | ||||||
|  | 							MatchConstraints: &v1alpha1.MatchResources{ | ||||||
|  | 								MatchPolicy:       ptr.To(v1alpha1.Equivalent), | ||||||
|  | 								NamespaceSelector: &metav1.LabelSelector{}, | ||||||
|  | 								ObjectSelector:    &metav1.LabelSelector{}, | ||||||
|  | 								ResourceRules: []v1alpha1.NamedRuleWithOperations{ | ||||||
|  | 									{ | ||||||
|  | 										RuleWithOperations: v1alpha1.RuleWithOperations{ | ||||||
|  | 											Rule: v1alpha1.Rule{ | ||||||
|  | 												APIGroups:   []string{"apps"}, | ||||||
|  | 												APIVersions: []string{"v1"}, | ||||||
|  | 												Resources:   []string{"deployments"}, | ||||||
|  | 											}, | ||||||
|  | 											Operations: []admissionregistrationv1.OperationType{"*"}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							Mutations: []v1alpha1.Mutation{{ | ||||||
|  | 								PatchType: v1alpha1.PatchTypeApplyConfiguration, | ||||||
|  | 								ApplyConfiguration: &v1alpha1.ApplyConfiguration{ | ||||||
|  | 									Expression: `Object{ | ||||||
|  | 									metadata: Object.metadata{ | ||||||
|  | 										labels: {"environment": "production"} | ||||||
|  | 									} | ||||||
|  | 								}`}}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					Bindings: []*PolicyBinding{{ | ||||||
|  | 						ObjectMeta: metav1.ObjectMeta{Name: "binding"}, | ||||||
|  | 						Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ | ||||||
|  | 							PolicyName: "policy2", | ||||||
|  | 						}, | ||||||
|  | 					}}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			expect: &appsv1.Deployment{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      "d1", | ||||||
|  | 					Namespace: "default", | ||||||
|  | 					Labels: map[string]string{ | ||||||
|  | 						"environment": "production", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				Spec: appsv1.DeploymentSpec{ | ||||||
|  | 					Template: corev1.PodTemplateSpec{ | ||||||
|  | 						Spec: corev1.PodSpec{ | ||||||
|  | 							Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	defer cancel() | ||||||
|  | 	tcManager := patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient()) | ||||||
|  | 	go tcManager.Run(ctx) | ||||||
|  |  | ||||||
|  | 	err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) { | ||||||
|  | 		converter := tcManager.GetTypeConverter(deploymentGVK) | ||||||
|  | 		return converter != nil, nil | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	scheme := runtime.NewScheme() | ||||||
|  | 	err = appsv1.AddToScheme(scheme) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	err = corev1.AddToScheme(scheme) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	objectInterfaces := admission.NewObjectInterfacesFromScheme(scheme) | ||||||
|  |  | ||||||
|  | 	for _, tc := range testCases { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			client := fake.NewClientset(tc.params...) | ||||||
|  |  | ||||||
|  | 			// always include default namespace | ||||||
|  | 			err := client.Tracker().Add(&corev1.Namespace{ | ||||||
|  | 				TypeMeta: metav1.TypeMeta{ | ||||||
|  | 					APIVersion: "v1", | ||||||
|  | 					Kind:       "Namespace", | ||||||
|  | 				}, | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "default", | ||||||
|  | 				}, | ||||||
|  | 				Spec: corev1.NamespaceSpec{}, | ||||||
|  | 			}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			informerFactory := informers.NewSharedInformerFactory(client, 0) | ||||||
|  | 			matcher := matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client) | ||||||
|  | 			paramInformer, err := informerFactory.ForResource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			informerFactory.WaitForCacheSync(ctx.Done()) | ||||||
|  | 			informerFactory.Start(ctx.Done()) | ||||||
|  | 			for i, h := range tc.policyHooks { | ||||||
|  | 				tc.policyHooks[i].ParamInformer = paramInformer | ||||||
|  | 				tc.policyHooks[i].ParamScope = testParamScope{} | ||||||
|  | 				tc.policyHooks[i].Evaluator = compilePolicy(h.Policy) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			dispatcher := NewDispatcher(fakeAuthorizer{}, matcher, tcManager) | ||||||
|  | 			err = dispatcher.Start(ctx) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("error starting dispatcher: %v", err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			metaAccessor, err := meta.Accessor(tc.object) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			attrs := admission.NewAttributesRecord(tc.object, tc.oldObject, tc.gvk, | ||||||
|  | 				metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr, | ||||||
|  | 				"", admission.Create, &metav1.CreateOptions{}, false, nil) | ||||||
|  | 			vAttrs := &admission.VersionedAttributes{ | ||||||
|  | 				Attributes:         attrs, | ||||||
|  | 				VersionedKind:      tc.gvk, | ||||||
|  | 				VersionedObject:    tc.object, | ||||||
|  | 				VersionedOldObject: tc.oldObject, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			err = dispatcher.Dispatch(ctx, vAttrs, objectInterfaces, tc.policyHooks) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("error dispatching policy hooks: %v", err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			obj := vAttrs.VersionedObject | ||||||
|  | 			if !equality.Semantic.DeepEqual(obj, tc.expect) { | ||||||
|  | 				t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(tc.expect, obj)) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type testParamScope struct{} | ||||||
|  |  | ||||||
|  | func (t testParamScope) Name() meta.RESTScopeName { | ||||||
|  | 	return meta.RESTScopeNameNamespace | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ meta.RESTScope = testParamScope{} | ||||||
| @@ -1,60 +0,0 @@ | |||||||
| /* |  | ||||||
| Copyright 2024 The Kubernetes Authors. |  | ||||||
|  |  | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
| you may not use this file except in compliance with the License. |  | ||||||
| You may obtain a copy of the License at |  | ||||||
|  |  | ||||||
|     http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
|  |  | ||||||
| Unless required by applicable law or agreed to in writing, software |  | ||||||
| distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
| See the License for the specific language governing permissions and |  | ||||||
| limitations under the License. |  | ||||||
| */ |  | ||||||
|  |  | ||||||
| package mutating |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	celgo "github.com/google/cel-go/cel" |  | ||||||
| 	celtypes "github.com/google/cel-go/common/types" |  | ||||||
|  |  | ||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/cel" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var _ cel.ExpressionAccessor = &ApplyConfigurationCondition{} |  | ||||||
|  |  | ||||||
| // ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression |  | ||||||
| // that returns an apply configuration |  | ||||||
| type ApplyConfigurationCondition struct { |  | ||||||
| 	Expression string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (v *ApplyConfigurationCondition) GetExpression() string { |  | ||||||
| 	return v.Expression |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type { |  | ||||||
| 	return []*celgo.Type{applyConfigObjectType} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var applyConfigObjectType = celtypes.NewObjectType("Object") |  | ||||||
|  |  | ||||||
| var _ cel.ExpressionAccessor = &JSONPatchCondition{} |  | ||||||
|  |  | ||||||
| // JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression |  | ||||||
| // that returns a JSON patch value. |  | ||||||
| type JSONPatchCondition struct { |  | ||||||
| 	Expression string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (v *JSONPatchCondition) GetExpression() string { |  | ||||||
| 	return v.Expression |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type { |  | ||||||
| 	return []*celgo.Type{celgo.ListType(jsonPatchType)} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var jsonPatchType = celtypes.NewObjectType("JSONPatch") |  | ||||||
| @@ -29,6 +29,8 @@ import ( | |||||||
|  |  | ||||||
| // Patcher provides a patch function to perform a mutation to an object in the admission chain. | // Patcher provides a patch function to perform a mutation to an object in the admission chain. | ||||||
| type Patcher interface { | type Patcher interface { | ||||||
|  | 	// Patch returns a copy of the object in the request, modified to change specified by the patch. | ||||||
|  | 	// The original object in the request MUST NOT be modified in-place. | ||||||
| 	Patch(ctx context.Context, request Request, runtimeCELCostBudget int64) (runtime.Object, error) | 	Patch(ctx context.Context, request Request, runtimeCELCostBudget int64) (runtime.Object, error) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ import ( | |||||||
| 	gojson "encoding/json" | 	gojson "encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	celgo "github.com/google/cel-go/cel" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  |  | ||||||
| @@ -41,6 +42,24 @@ import ( | |||||||
| 	pointer "k8s.io/utils/ptr" | 	pointer "k8s.io/utils/ptr" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression | ||||||
|  | // that returns a JSON patch value. | ||||||
|  | type JSONPatchCondition struct { | ||||||
|  | 	Expression string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ plugincel.ExpressionAccessor = &JSONPatchCondition{} | ||||||
|  |  | ||||||
|  | func (v *JSONPatchCondition) GetExpression() string { | ||||||
|  | 	return v.Expression | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type { | ||||||
|  | 	return []*celgo.Type{celgo.ListType(jsonPatchType)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var jsonPatchType = types.NewObjectType("JSONPatch") | ||||||
|  |  | ||||||
| // NewJSONPatcher creates a patcher that performs a JSON Patch mutation. | // NewJSONPatcher creates a patcher that performs a JSON Patch mutation. | ||||||
| func NewJSONPatcher(patchEvaluator plugincel.MutatingEvaluator) Patcher { | func NewJSONPatcher(patchEvaluator plugincel.MutatingEvaluator) Patcher { | ||||||
| 	return &jsonPatcher{patchEvaluator} | 	return &jsonPatcher{patchEvaluator} | ||||||
|   | |||||||
| @@ -1 +1,486 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2024 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
| package patch | package patch | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"github.com/google/go-cmp/cmp" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	appsv1 "k8s.io/api/apps/v1" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/equality" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/meta" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | 	"k8s.io/apiserver/pkg/admission" | ||||||
|  | 	"k8s.io/apiserver/pkg/admission/plugin/cel" | ||||||
|  | 	celconfig "k8s.io/apiserver/pkg/apis/cel" | ||||||
|  | 	"k8s.io/apiserver/pkg/cel/environment" | ||||||
|  | 	"k8s.io/utils/ptr" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestJSONPatch(t *testing.T) { | ||||||
|  | 	deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name              string | ||||||
|  | 		expression        string | ||||||
|  | 		gvr               schema.GroupVersionResource | ||||||
|  | 		object, oldObject runtime.Object | ||||||
|  | 		expectedResult    runtime.Object | ||||||
|  | 		expectedErr       string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch with false test operation", | ||||||
|  | 			expression: `[ | ||||||
|  | 						JSONPatch{op: "test", path: "/spec/replicas", value: 100},  | ||||||
|  | 						JSONPatch{op: "replace", path: "/spec/replicas", value: 3}, | ||||||
|  | 					]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch with false test operation", | ||||||
|  | 			expression: `[ | ||||||
|  | 						JSONPatch{op: "test", path: "/spec/replicas", value: 100},  | ||||||
|  | 						JSONPatch{op: "replace", path: "/spec/replicas", value: 3}, | ||||||
|  | 					]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch with true test operation", | ||||||
|  | 			expression: `[ | ||||||
|  | 						JSONPatch{op: "test", path: "/spec/replicas", value: 1},  | ||||||
|  | 						JSONPatch{op: "replace", path: "/spec/replicas", value: 3}, | ||||||
|  | 					]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch remove to unset field", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "remove", path: "/spec/replicas"},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch remove map entry by key", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "remove", path: "/metadata/labels/y"},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1", "y": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch remove element in list", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "remove", path: "/spec/template/spec/containers/1"},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}, {Name: "c"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch copy map entry by key", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "copy", from: "/metadata/labels/x", path: "/metadata/labels/y"},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1", "y": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch copy first element to end of list", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "copy", from: "/spec/template/spec/containers/0", path: "/spec/template/spec/containers/-"},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch move map entry by key", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "move", from: "/metadata/labels/x", path: "/metadata/labels/y"},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch move first element to end of list", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "move", from: "/spec/template/spec/containers/0", path: "/spec/template/spec/containers/-"},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "b"}, {Name: "c"}, {Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch add map entry by key and value", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "add", path: "/metadata/labels/x", value: "2"},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1", "x": "2"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch add map value to field", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "add", path: "/metadata/labels", value: {"y": "2"}},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch add map to existing map", // performs a replacement | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "add", path: "/metadata/labels", value: {"y": "2"}},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch add to start of list", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "add", path: "/spec/template/spec/containers/0", value: {"name": "x"}},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "x"}, {Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch add to end of list", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "add", path: "/spec/template/spec/containers/-", value: {"name": "x"}},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}, {Name: "x"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch replace key in map", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "replace", path: "/metadata/labels/x", value: "2"},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1", "x": "2"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch replace map value of unset field", // adds the field value | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "replace", path: "/metadata/labels", value: {"y": "2"}},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch replace map value of set field", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "replace", path: "/metadata/labels", value: {"y": "2"}},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch replace first element in list", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "replace", path: "/spec/template/spec/containers/0", value: {"name": "x"}},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "x"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch add map entry by key and value", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "add", path: "/spec", value: Object.spec{selector: Object.spec.selector{}, replicas: 10}} | ||||||
|  | 				]`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Selector: &metav1.LabelSelector{}, Replicas: ptr.To[int32](10)}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "JSONPatch patch type has field access", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{ | ||||||
|  | 						op: "add", path: "/metadata/labels", | ||||||
|  | 						value: { | ||||||
|  | 							"op": JSONPatch{op: "opValue"}.op, | ||||||
|  | 							"path": JSONPatch{path: "pathValue"}.path, | ||||||
|  | 							"from": JSONPatch{from: "fromValue"}.from, | ||||||
|  | 							"value": string(JSONPatch{value: "valueValue"}.value), | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				]`, | ||||||
|  | 			gvr:    deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ | ||||||
|  | 				"op":    "opValue", | ||||||
|  | 				"path":  "pathValue", | ||||||
|  | 				"from":  "fromValue", | ||||||
|  | 				"value": "valueValue", | ||||||
|  | 			}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "JSONPatch patch type has field testing", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{ | ||||||
|  | 						op: "add", path: "/metadata/labels", | ||||||
|  | 						value: { | ||||||
|  | 							"op": string(has(JSONPatch{op: "opValue"}.op)), | ||||||
|  | 							"path": string(has(JSONPatch{path: "pathValue"}.path)), | ||||||
|  | 							"from": string(has(JSONPatch{from: "fromValue"}.from)), | ||||||
|  | 							"value": string(has(JSONPatch{value: "valueValue"}.value)), | ||||||
|  | 							"op-unset": string(has(JSONPatch{}.op)), | ||||||
|  | 							"path-unset": string(has(JSONPatch{}.path)), | ||||||
|  | 							"from-unset": string(has(JSONPatch{}.from)), | ||||||
|  | 							"value-unset": string(has(JSONPatch{}.value)), | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				]`, | ||||||
|  | 			gvr:    deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ | ||||||
|  | 				"op":          "true", | ||||||
|  | 				"path":        "true", | ||||||
|  | 				"from":        "true", | ||||||
|  | 				"value":       "true", | ||||||
|  | 				"op-unset":    "false", | ||||||
|  | 				"path-unset":  "false", | ||||||
|  | 				"from-unset":  "false", | ||||||
|  | 				"value-unset": "false", | ||||||
|  | 			}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "JSONPatch patch type equality", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{ | ||||||
|  | 						op: "add", path: "/metadata/labels", | ||||||
|  | 						value: { | ||||||
|  | 							"empty": string(JSONPatch{} == JSONPatch{}), | ||||||
|  | 							"partial": string(JSONPatch{op: "add"} == JSONPatch{op: "add"}), | ||||||
|  | 							"same-all": string(JSONPatch{op: "add", path: "path", from: "from", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}), | ||||||
|  | 							"different-op": string(JSONPatch{op: "add"} == JSONPatch{op: "remove"}), | ||||||
|  | 							"different-path": string(JSONPatch{op: "add", path: "x", from: "from", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}), | ||||||
|  | 							"different-from": string(JSONPatch{op: "add", path: "path", from: "x", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}), | ||||||
|  | 							"different-value": string(JSONPatch{op: "add", path: "path", from: "from", value: "1"} == JSONPatch{op: "add", path: "path", from: "from", value: 1}), | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				]`, | ||||||
|  | 			gvr:    deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ | ||||||
|  | 				"empty":           "true", | ||||||
|  | 				"partial":         "true", | ||||||
|  | 				"same-all":        "true", | ||||||
|  | 				"different-op":    "false", | ||||||
|  | 				"different-path":  "false", | ||||||
|  | 				"different-from":  "false", | ||||||
|  | 				"different-value": "false", | ||||||
|  | 			}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "JSONPatch key escaping", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{ | ||||||
|  | 						op: "add", path: "/metadata/labels", value: {} | ||||||
|  | 					}, | ||||||
|  | 					JSONPatch{ | ||||||
|  | 						op: "add", path: "/metadata/labels/" + jsonpatch.escapeKey("k8s.io/x~y"), value: "true" | ||||||
|  | 					} | ||||||
|  | 				]`, | ||||||
|  | 			gvr:    deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ | ||||||
|  | 				"k8s.io/x~y": "true", | ||||||
|  | 			}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch with CEL initializer", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "add", path: "/spec/template/spec/containers/-", value: Object.spec.template.spec.containers{ | ||||||
|  | 							name: "x", | ||||||
|  | 							ports: [Object.spec.template.spec.containers.ports{containerPort: 8080}], | ||||||
|  | 						} | ||||||
|  | 					},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}, {Name: "x", Ports: []corev1.ContainerPort{{ContainerPort: 8080}}}}, | ||||||
|  | 			}}}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch invalid CEL initializer field", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{ | ||||||
|  | 						op: "add", path: "/spec/template/spec/containers/-",  | ||||||
|  | 						value: Object.spec.template.spec.containers{ | ||||||
|  | 							name: "x", | ||||||
|  | 							ports: [Object.spec.template.spec.containers.ports{containerPortZ: 8080}] | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedErr: "strict decoding error: unknown field \"spec.template.spec.containers[1].ports[0].containerPortZ\"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch invalid CEL initializer type", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{ | ||||||
|  | 						op: "add", path: "/spec/template/spec/containers/-",  | ||||||
|  | 						value: Object.spec.template.spec.containers{ | ||||||
|  | 							name: "x", | ||||||
|  | 							ports: [Object.spec.template.spec.containers.portsZ{containerPort: 8080}] | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedErr: " mismatch: unexpected type name \"Object.spec.template.spec.containers.portsZ\", expected \"Object.spec.template.spec.containers.ports\", which matches field name path from root Object type", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "jsonPatch replace end of list with - not allowed", | ||||||
|  | 			expression: `[ | ||||||
|  | 					JSONPatch{op: "replace", path: "/spec/template/spec/containers/-", value: {"name": "x"}},  | ||||||
|  | 				]`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}}, | ||||||
|  | 			}}}}, | ||||||
|  | 			expectedErr: "JSON Patch: replace operation does not apply: doc is missing key: /spec/template/spec/containers/-: missing value", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	for _, tc := range tests { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			accessor := &JSONPatchCondition{Expression: tc.expression} | ||||||
|  | 			compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{StrictCost: true, HasPatchTypes: true}, environment.StoredExpressions) | ||||||
|  |  | ||||||
|  | 			patcher := jsonPatcher{PatchEvaluator: compileResult} | ||||||
|  |  | ||||||
|  | 			scheme := runtime.NewScheme() | ||||||
|  | 			err := appsv1.AddToScheme(scheme) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			var gvk schema.GroupVersionKind | ||||||
|  | 			gvks, _, err := scheme.ObjectKinds(tc.object) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			if len(gvks) == 1 { | ||||||
|  | 				gvk = gvks[0] | ||||||
|  | 			} else { | ||||||
|  | 				t.Fatalf("Failed to find gvk for type: %T", tc.object) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			metaAccessor, err := meta.Accessor(tc.object) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			attrs := admission.NewAttributesRecord(tc.object, tc.oldObject, gvk, | ||||||
|  | 				metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr, | ||||||
|  | 				"", admission.Create, &metav1.CreateOptions{}, false, nil) | ||||||
|  | 			vAttrs := &admission.VersionedAttributes{ | ||||||
|  | 				Attributes:         attrs, | ||||||
|  | 				VersionedKind:      gvk, | ||||||
|  | 				VersionedObject:    tc.object, | ||||||
|  | 				VersionedOldObject: tc.oldObject, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			r := Request{ | ||||||
|  | 				MatchedResource:     tc.gvr, | ||||||
|  | 				VersionedAttributes: vAttrs, | ||||||
|  | 				ObjectInterfaces:    admission.NewObjectInterfacesFromScheme(scheme), | ||||||
|  | 				OptionalVariables:   cel.OptionalVariableBindings{}, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			got, err := patcher.Patch(context.Background(), r, celconfig.RuntimeCELCostBudget) | ||||||
|  | 			if len(tc.expectedErr) > 0 { | ||||||
|  | 				if err == nil { | ||||||
|  | 					t.Fatalf("expected error: %s", tc.expectedErr) | ||||||
|  | 				} else { | ||||||
|  | 					if !strings.Contains(err.Error(), tc.expectedErr) { | ||||||
|  | 						t.Fatalf("expected error: %s, got: %s", tc.expectedErr, err.Error()) | ||||||
|  | 					} | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if err != nil && len(tc.expectedErr) == 0 { | ||||||
|  | 				t.Fatalf("unexpected error: %v", err) | ||||||
|  | 			} | ||||||
|  | 			if !equality.Semantic.DeepEqual(tc.expectedResult, got) { | ||||||
|  | 				t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(tc.expectedResult, got)) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -20,6 +20,8 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	celgo "github.com/google/cel-go/cel" | ||||||
|  | 	celtypes "github.com/google/cel-go/common/types" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"sigs.k8s.io/structured-merge-diff/v4/fieldpath" | 	"sigs.k8s.io/structured-merge-diff/v4/fieldpath" | ||||||
| @@ -35,6 +37,24 @@ import ( | |||||||
| 	"k8s.io/apiserver/pkg/cel/mutation/dynamic" | 	"k8s.io/apiserver/pkg/cel/mutation/dynamic" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression | ||||||
|  | // that returns an apply configuration | ||||||
|  | type ApplyConfigurationCondition struct { | ||||||
|  | 	Expression string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ plugincel.ExpressionAccessor = &ApplyConfigurationCondition{} | ||||||
|  |  | ||||||
|  | func (v *ApplyConfigurationCondition) GetExpression() string { | ||||||
|  | 	return v.Expression | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type { | ||||||
|  | 	return []*celgo.Type{applyConfigObjectType} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var applyConfigObjectType = celtypes.NewObjectType("Object") | ||||||
|  |  | ||||||
| // NewApplyConfigurationPatcher creates a patcher that performs an applyConfiguration mutation. | // NewApplyConfigurationPatcher creates a patcher that performs an applyConfiguration mutation. | ||||||
| func NewApplyConfigurationPatcher(expressionEvaluator plugincel.MutatingEvaluator) Patcher { | func NewApplyConfigurationPatcher(expressionEvaluator plugincel.MutatingEvaluator) Patcher { | ||||||
| 	return &applyConfigPatcher{expressionEvaluator: expressionEvaluator} | 	return &applyConfigPatcher{expressionEvaluator: expressionEvaluator} | ||||||
| @@ -147,6 +167,9 @@ func ApplyStructuredMergeDiff( | |||||||
|  |  | ||||||
| // validatePatch searches an apply configuration for any arrays, maps or structs elements that are atomic and returns | // validatePatch searches an apply configuration for any arrays, maps or structs elements that are atomic and returns | ||||||
| // an error if any are found. | // an error if any are found. | ||||||
|  | // This prevents accidental removal of fields that can occur when the user intends to modify some | ||||||
|  | // fields in an atomic type, not realizing that all fields not explicitly set in the new value | ||||||
|  | // of the atomic will be removed. | ||||||
| func validatePatch(v *typed.TypedValue) error { | func validatePatch(v *typed.TypedValue) error { | ||||||
| 	atomics := findAtomics(nil, v.Schema(), v.TypeRef(), v.AsValue()) | 	atomics := findAtomics(nil, v.Schema(), v.TypeRef(), v.AsValue()) | ||||||
| 	if len(atomics) > 0 { | 	if len(atomics) > 0 { | ||||||
|   | |||||||
| @@ -0,0 +1,375 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2024 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package patch | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"github.com/google/go-cmp/cmp" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	appsv1 "k8s.io/api/apps/v1" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/equality" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/meta" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/resource" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"k8s.io/apiserver/pkg/admission" | ||||||
|  | 	"k8s.io/apiserver/pkg/admission/plugin/cel" | ||||||
|  | 	celconfig "k8s.io/apiserver/pkg/apis/cel" | ||||||
|  | 	"k8s.io/apiserver/pkg/cel/environment" | ||||||
|  | 	"k8s.io/client-go/openapi/openapitest" | ||||||
|  | 	"k8s.io/utils/ptr" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestApplyConfiguration(t *testing.T) { | ||||||
|  | 	deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} | ||||||
|  | 	deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"} | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name              string | ||||||
|  | 		expression        string | ||||||
|  | 		gvr               schema.GroupVersionResource | ||||||
|  | 		object, oldObject runtime.Object | ||||||
|  | 		expectedResult    runtime.Object | ||||||
|  | 		expectedErr       string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "apply configuration add to listType=map", | ||||||
|  | 			expression: `Object{ | ||||||
|  | 					spec: Object.spec{ | ||||||
|  | 						template: Object.spec.template{ | ||||||
|  | 							spec: Object.spec.template.spec{ | ||||||
|  | 								volumes: [Object.spec.template.spec.volumes{ | ||||||
|  | 									name: "y" | ||||||
|  | 								}] | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				}`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "apply configuration add to listType=map", | ||||||
|  | 			expression: `Object{ | ||||||
|  | 					spec: Object.spec{ | ||||||
|  | 						template: Object.spec.template{ | ||||||
|  | 							spec: Object.spec.template.spec{ | ||||||
|  | 								volumes: [Object.spec.template.spec.volumes{ | ||||||
|  | 									name: "y" | ||||||
|  | 								}] | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				}`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Volumes: []corev1.Volume{{Name: "x"}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "apply configuration update listType=map entry", | ||||||
|  | 			expression: `Object{ | ||||||
|  | 					spec: Object.spec{ | ||||||
|  | 						template: Object.spec.template{ | ||||||
|  | 							spec: Object.spec.template.spec{ | ||||||
|  | 								volumes: [Object.spec.template.spec.volumes{ | ||||||
|  | 									name: "y", | ||||||
|  | 									hostPath: Object.spec.template.spec.volumes.hostPath{ | ||||||
|  | 										path: "a" | ||||||
|  | 									} | ||||||
|  | 								}] | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				}`, | ||||||
|  | 			gvr: deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Volumes: []corev1.Volume{{Name: "x"}, {Name: "y", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "a"}}}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "apply configuration with conditionals", | ||||||
|  | 			expression: `Object{ | ||||||
|  | 					spec: Object.spec{ | ||||||
|  | 						replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas | ||||||
|  | 					} | ||||||
|  | 				}`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "apply configuration with old object", | ||||||
|  | 			expression: `Object{ | ||||||
|  | 					spec: Object.spec{ | ||||||
|  | 						replicas: oldObject.spec.replicas % 2 == 0?oldObject.spec.replicas + 1:oldObject.spec.replicas | ||||||
|  | 					} | ||||||
|  | 				}`, | ||||||
|  | 			gvr:            deploymentGVR, | ||||||
|  | 			object:         &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			oldObject:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "complex apply configuration initialization", | ||||||
|  | 			expression: `Object{ | ||||||
|  | 					spec: Object.spec{ | ||||||
|  | 						replicas: 1, | ||||||
|  | 						template: Object.spec.template{ | ||||||
|  | 							metadata: Object.spec.template.metadata{ | ||||||
|  | 								labels: {"app": "nginx"} | ||||||
|  | 							}, | ||||||
|  | 							spec: Object.spec.template.spec{ | ||||||
|  | 								containers: [Object.spec.template.spec.containers{ | ||||||
|  | 									name: "nginx", | ||||||
|  | 									image: "nginx:1.14.2", | ||||||
|  | 									ports: [Object.spec.template.spec.containers.ports{ | ||||||
|  | 										containerPort: 80 | ||||||
|  | 									}], | ||||||
|  | 									resources: Object.spec.template.spec.containers.resources{ | ||||||
|  | 										limits: {"cpu": "128M"}, | ||||||
|  | 									} | ||||||
|  | 								}] | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				}`, | ||||||
|  |  | ||||||
|  | 			gvr:    deploymentGVR, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}}, | ||||||
|  | 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Replicas: ptr.To[int32](1), | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 						Labels: map[string]string{"app": "nginx"}, | ||||||
|  | 					}, | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Containers: []corev1.Container{{ | ||||||
|  | 							Name:  "nginx", | ||||||
|  | 							Image: "nginx:1.14.2", | ||||||
|  | 							Ports: []corev1.ContainerPort{ | ||||||
|  | 								{ContainerPort: 80}, | ||||||
|  | 							}, | ||||||
|  | 							Resources: corev1.ResourceRequirements{ | ||||||
|  | 								Limits: corev1.ResourceList{corev1.ResourceName("cpu"): resource.MustParse("128M")}, | ||||||
|  | 							}, | ||||||
|  | 						}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "apply configuration with change to atomic", | ||||||
|  | 			expression: `Object{ | ||||||
|  | 					spec: Object.spec{ | ||||||
|  | 						selector: Object.spec.selector{ | ||||||
|  | 							matchLabels: {"l": "v"} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				}`, | ||||||
|  | 			gvr:         deploymentGVR, | ||||||
|  | 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			expectedErr: "error applying patch: invalid ApplyConfiguration: may not mutate atomic arrays, maps or structs: .spec.selector", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "apply configuration with invalid type name", | ||||||
|  | 			expression: `Object{ | ||||||
|  | 					spec: Object.specx{ | ||||||
|  | 						replicas: 1 | ||||||
|  | 					} | ||||||
|  | 				}`, | ||||||
|  | 			gvr:         deploymentGVR, | ||||||
|  | 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			expectedErr: "type mismatch: unexpected type name \"Object.specx\", expected \"Object.spec\", which matches field name path from root Object type", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "apply configuration with invalid field name", | ||||||
|  | 			expression: `Object{ | ||||||
|  | 					spec: Object.spec{ | ||||||
|  | 						replicasx: 1 | ||||||
|  | 					} | ||||||
|  | 				}`, | ||||||
|  | 			gvr:         deploymentGVR, | ||||||
|  | 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			expectedErr: "error applying patch: failed to convert patch object to typed object: .spec.replicasx: field not declared in schema", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "apply configuration with invalid return type", | ||||||
|  | 			expression:  `"I'm a teapot!"`, | ||||||
|  | 			gvr:         deploymentGVR, | ||||||
|  | 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			expectedErr: "must evaluate to Object but got string", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "apply configuration with invalid initializer return type", | ||||||
|  | 			expression:  `Object.spec.metadata{}`, | ||||||
|  | 			gvr:         deploymentGVR, | ||||||
|  | 			object:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, | ||||||
|  | 			expectedErr: "must evaluate to Object but got Object.spec.metadata", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	defer cancel() | ||||||
|  | 	tcManager := NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient()) | ||||||
|  | 	go tcManager.Run(ctx) | ||||||
|  |  | ||||||
|  | 	err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) { | ||||||
|  | 		converter := tcManager.GetTypeConverter(deploymentGVK) | ||||||
|  | 		return converter != nil, nil | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range tests { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			accessor := &ApplyConfigurationCondition{Expression: tc.expression} | ||||||
|  | 			compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{StrictCost: true, HasPatchTypes: true}, environment.StoredExpressions) | ||||||
|  |  | ||||||
|  | 			patcher := applyConfigPatcher{expressionEvaluator: compileResult} | ||||||
|  |  | ||||||
|  | 			scheme := runtime.NewScheme() | ||||||
|  | 			err := appsv1.AddToScheme(scheme) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			var gvk schema.GroupVersionKind | ||||||
|  | 			gvks, _, err := scheme.ObjectKinds(tc.object) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			if len(gvks) == 1 { | ||||||
|  | 				gvk = gvks[0] | ||||||
|  | 			} else { | ||||||
|  | 				t.Fatalf("Failed to find gvk for type: %T", tc.object) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			metaAccessor, err := meta.Accessor(tc.object) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			typeAccessor, err := meta.TypeAccessor(tc.object) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			typeAccessor.SetKind(gvk.Kind) | ||||||
|  | 			typeAccessor.SetAPIVersion(gvk.GroupVersion().String()) | ||||||
|  |  | ||||||
|  | 			attrs := admission.NewAttributesRecord(tc.object, tc.oldObject, gvk, | ||||||
|  | 				metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr, | ||||||
|  | 				"", admission.Create, &metav1.CreateOptions{}, false, nil) | ||||||
|  | 			vAttrs := &admission.VersionedAttributes{ | ||||||
|  | 				Attributes:         attrs, | ||||||
|  | 				VersionedKind:      gvk, | ||||||
|  | 				VersionedObject:    tc.object, | ||||||
|  | 				VersionedOldObject: tc.oldObject, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			r := Request{ | ||||||
|  | 				MatchedResource:     tc.gvr, | ||||||
|  | 				VersionedAttributes: vAttrs, | ||||||
|  | 				ObjectInterfaces:    admission.NewObjectInterfacesFromScheme(scheme), | ||||||
|  | 				OptionalVariables:   cel.OptionalVariableBindings{}, | ||||||
|  | 				TypeConverter:       tcManager.GetTypeConverter(gvk), | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			patched, err := patcher.Patch(ctx, r, celconfig.RuntimeCELCostBudget) | ||||||
|  | 			if len(tc.expectedErr) > 0 { | ||||||
|  | 				if err == nil { | ||||||
|  | 					t.Fatalf("expected error: %s", tc.expectedErr) | ||||||
|  | 				} else { | ||||||
|  | 					if !strings.Contains(err.Error(), tc.expectedErr) { | ||||||
|  | 						t.Fatalf("expected error: %s, got: %s", tc.expectedErr, err.Error()) | ||||||
|  | 					} | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if err != nil && len(tc.expectedErr) == 0 { | ||||||
|  | 				t.Fatalf("unexpected error: %v", err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			got, err := runtime.DefaultUnstructuredConverter.ToUnstructured(patched) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			wantTypeAccessor, err := meta.TypeAccessor(tc.expectedResult) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			wantTypeAccessor.SetKind(gvk.Kind) | ||||||
|  | 			wantTypeAccessor.SetAPIVersion(gvk.GroupVersion().String()) | ||||||
|  |  | ||||||
|  | 			want, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.expectedResult) | ||||||
|  |  | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			if !equality.Semantic.DeepEqual(want, got) { | ||||||
|  | 				t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(want, got)) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -0,0 +1,100 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2024 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package patch | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"github.com/google/go-cmp/cmp" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	appsv1 "k8s.io/api/apps/v1" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/equality" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/meta" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"k8s.io/client-go/openapi/openapitest" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestTypeConverter(t *testing.T) { | ||||||
|  | 	deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"} | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name   string | ||||||
|  | 		gvk    schema.GroupVersionKind | ||||||
|  | 		object runtime.Object | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "simple round trip", | ||||||
|  | 			gvk:  deploymentGVK, | ||||||
|  | 			object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ | ||||||
|  | 				Containers: []corev1.Container{{Name: "a"}, {Name: "x", Ports: []corev1.ContainerPort{{ContainerPort: 8080}}}}, | ||||||
|  | 			}}}}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	t.Cleanup(cancel) | ||||||
|  | 	tcManager := NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient()) | ||||||
|  | 	go tcManager.Run(ctx) | ||||||
|  |  | ||||||
|  | 	err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) { | ||||||
|  | 		converter := tcManager.GetTypeConverter(deploymentGVK) | ||||||
|  | 		return converter != nil, nil | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range tests { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			typeAccessor, err := meta.TypeAccessor(tc.object) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			typeAccessor.SetKind(tc.gvk.Kind) | ||||||
|  | 			typeAccessor.SetAPIVersion(tc.gvk.GroupVersion().String()) | ||||||
|  |  | ||||||
|  | 			converter := tcManager.GetTypeConverter(tc.gvk) | ||||||
|  | 			if converter == nil { | ||||||
|  | 				t.Errorf("nil TypeConverter") | ||||||
|  | 			} | ||||||
|  | 			typedObject, err := converter.ObjectToTyped(tc.object) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			roundTripped, err := converter.TypedToObject(typedObject) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			got, err := runtime.DefaultUnstructuredConverter.ToUnstructured(roundTripped) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			want, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.object) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			if !equality.Semantic.DeepEqual(want, got) { | ||||||
|  | 				t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(want, got)) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -53,7 +53,6 @@ func Register(plugins *admission.Plugins) { | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Plugin is an implementation of admission.Interface. |  | ||||||
| type Policy = v1alpha1.MutatingAdmissionPolicy | type Policy = v1alpha1.MutatingAdmissionPolicy | ||||||
| type PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding | type PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding | ||||||
| type PolicyMutation = v1alpha1.Mutation | type PolicyMutation = v1alpha1.Mutation | ||||||
| @@ -80,6 +79,7 @@ type PolicyEvaluator struct { | |||||||
| 	Error          error | 	Error          error | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Plugin is an implementation of admission.Interface. | ||||||
| type Plugin struct { | type Plugin struct { | ||||||
| 	*generic.Plugin[PolicyHook] | 	*generic.Plugin[PolicyHook] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -119,6 +119,68 @@ func TestBasicPatch(t *testing.T) { | |||||||
| 	require.Equal(t, expectedAnnotations, testObject.Annotations) | 	require.Equal(t, expectedAnnotations, testObject.Annotations) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestJSONPatch(t *testing.T) { | ||||||
|  | 	patchObj := &unstructured.Unstructured{ | ||||||
|  | 		Object: map[string]interface{}{ | ||||||
|  | 			"apiVersion": "v1", | ||||||
|  | 			"kind":       "ConfigMap", | ||||||
|  | 			"metadata": map[string]interface{}{ | ||||||
|  | 				"annotations": map[string]interface{}{ | ||||||
|  | 					"foo": "bar", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			"data": map[string]interface{}{ | ||||||
|  | 				"myfield": "myvalue", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator { | ||||||
|  | 		return mutating.PolicyEvaluator{ | ||||||
|  | 			Mutators: []patch.Patcher{smdPatcher{patch: patchObj}}, | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	// Set up a policy and binding that match, no params | ||||||
|  | 	require.NoError(t, testContext.UpdateAndWait( | ||||||
|  | 		&mutating.Policy{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{Name: "policy"}, | ||||||
|  | 			Spec: v1alpha1.MutatingAdmissionPolicySpec{ | ||||||
|  | 				MatchConstraints: &v1alpha1.MatchResources{ | ||||||
|  | 					MatchPolicy:       ptr.To(v1alpha1.Equivalent), | ||||||
|  | 					NamespaceSelector: &metav1.LabelSelector{}, | ||||||
|  | 					ObjectSelector:    &metav1.LabelSelector{}, | ||||||
|  | 				}, | ||||||
|  | 				Mutations: []v1alpha1.Mutation{ | ||||||
|  | 					{ | ||||||
|  | 						JSONPatch: &v1alpha1.JSONPatch{ | ||||||
|  | 							Expression: "ignored, but required", | ||||||
|  | 						}, | ||||||
|  | 						PatchType: v1alpha1.PatchTypeApplyConfiguration, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		&mutating.PolicyBinding{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{Name: "binding"}, | ||||||
|  | 			Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ | ||||||
|  | 				PolicyName: "policy", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	)) | ||||||
|  |  | ||||||
|  | 	// Show that if we run an object through the policy, it gets the annotation | ||||||
|  | 	testObject := &corev1.ConfigMap{} | ||||||
|  | 	err := testContext.Dispatch(testObject, nil, admission.Create) | ||||||
|  | 	require.NoError(t, err) | ||||||
|  | 	require.Equal(t, &corev1.ConfigMap{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Annotations: map[string]string{"foo": "bar"}, | ||||||
|  | 		}, | ||||||
|  | 		Data: map[string]string{"myfield": "myvalue"}, | ||||||
|  | 	}, testObject) | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestSSAPatch(t *testing.T) { | func TestSSAPatch(t *testing.T) { | ||||||
| 	patchObj := &unstructured.Unstructured{ | 	patchObj := &unstructured.Unstructured{ | ||||||
| 		Object: map[string]interface{}{ | 		Object: map[string]interface{}{ | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| /* | /* | ||||||
| Copyright 2019 The Kubernetes Authors. | Copyright 2024 The Kubernetes Authors. | ||||||
|  |  | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|   | |||||||
| @@ -0,0 +1,147 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2024 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package mutating | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	v1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestFullReinvocation(t *testing.T) { | ||||||
|  | 	key1 := key{PolicyUID: types.NamespacedName{Name: "p1"}, BindingUID: types.NamespacedName{Name: "b1"}} | ||||||
|  | 	key2 := key{PolicyUID: types.NamespacedName{Name: "p2"}, BindingUID: types.NamespacedName{Name: "b2"}} | ||||||
|  | 	key3 := key{PolicyUID: types.NamespacedName{Name: "p3"}, BindingUID: types.NamespacedName{Name: "b3"}} | ||||||
|  |  | ||||||
|  | 	cm1v1 := &v1.ConfigMap{Data: map[string]string{"v": "1"}} | ||||||
|  | 	cm1v2 := &v1.ConfigMap{Data: map[string]string{"v": "2"}} | ||||||
|  |  | ||||||
|  | 	rc := policyReinvokeContext{} | ||||||
|  |  | ||||||
|  | 	// key1 is invoked and it updates the configmap | ||||||
|  | 	rc.SetLastPolicyInvocationOutput(cm1v1) | ||||||
|  | 	rc.RequireReinvokingPreviouslyInvokedPlugins() | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key1) | ||||||
|  |  | ||||||
|  | 	assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2)) | ||||||
|  |  | ||||||
|  | 	// key2 is invoked and it updates the configmap | ||||||
|  | 	rc.SetLastPolicyInvocationOutput(cm1v2) | ||||||
|  | 	rc.RequireReinvokingPreviouslyInvokedPlugins() | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key2) | ||||||
|  |  | ||||||
|  | 	assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1)) | ||||||
|  |  | ||||||
|  | 	// key3 is invoked but it does not change anything | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key3) | ||||||
|  |  | ||||||
|  | 	assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2)) | ||||||
|  |  | ||||||
|  | 	// key1 is reinvoked | ||||||
|  | 	assert.True(t, rc.ShouldReinvoke(key1)) | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key1) | ||||||
|  | 	rc.SetLastPolicyInvocationOutput(cm1v1) | ||||||
|  |  | ||||||
|  | 	assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2)) | ||||||
|  | 	rc.RequireReinvokingPreviouslyInvokedPlugins() | ||||||
|  |  | ||||||
|  | 	// key2 is reinvoked | ||||||
|  | 	assert.True(t, rc.ShouldReinvoke(key2)) | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key2) | ||||||
|  | 	rc.SetLastPolicyInvocationOutput(cm1v2) | ||||||
|  |  | ||||||
|  | 	assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1)) | ||||||
|  | 	rc.RequireReinvokingPreviouslyInvokedPlugins() | ||||||
|  |  | ||||||
|  | 	// key3 is reinvoked, because the reinvocations have changed the resource | ||||||
|  | 	assert.True(t, rc.ShouldReinvoke(key3)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPartialReinvocation(t *testing.T) { | ||||||
|  | 	key1 := key{PolicyUID: types.NamespacedName{Name: "p1"}, BindingUID: types.NamespacedName{Name: "b1"}} | ||||||
|  | 	key2 := key{PolicyUID: types.NamespacedName{Name: "p2"}, BindingUID: types.NamespacedName{Name: "b2"}} | ||||||
|  | 	key3 := key{PolicyUID: types.NamespacedName{Name: "p3"}, BindingUID: types.NamespacedName{Name: "b3"}} | ||||||
|  |  | ||||||
|  | 	cm1v1 := &v1.ConfigMap{Data: map[string]string{"v": "1"}} | ||||||
|  | 	cm1v2 := &v1.ConfigMap{Data: map[string]string{"v": "2"}} | ||||||
|  |  | ||||||
|  | 	rc := policyReinvokeContext{} | ||||||
|  |  | ||||||
|  | 	// key1 is invoked and it updates the configmap | ||||||
|  | 	rc.SetLastPolicyInvocationOutput(cm1v1) | ||||||
|  | 	rc.RequireReinvokingPreviouslyInvokedPlugins() | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key1) | ||||||
|  |  | ||||||
|  | 	assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2)) | ||||||
|  |  | ||||||
|  | 	// key2 is invoked and it updates the configmap | ||||||
|  | 	rc.SetLastPolicyInvocationOutput(cm1v2) | ||||||
|  | 	rc.RequireReinvokingPreviouslyInvokedPlugins() | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key2) | ||||||
|  |  | ||||||
|  | 	assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1)) | ||||||
|  |  | ||||||
|  | 	// key3 is invoked but it does not change anything | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key3) | ||||||
|  |  | ||||||
|  | 	assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2)) | ||||||
|  |  | ||||||
|  | 	// key1 is reinvoked but does not change anything | ||||||
|  | 	assert.True(t, rc.ShouldReinvoke(key1)) | ||||||
|  |  | ||||||
|  | 	// key2 is not reinvoked because nothing changed since last invocation | ||||||
|  | 	assert.False(t, rc.ShouldReinvoke(key2)) | ||||||
|  |  | ||||||
|  | 	// key3 is not reinvoked because nothing changed since last invocation | ||||||
|  | 	assert.False(t, rc.ShouldReinvoke(key3)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestNoReinvocation(t *testing.T) { | ||||||
|  | 	key1 := key{PolicyUID: types.NamespacedName{Name: "p1"}, BindingUID: types.NamespacedName{Name: "b1"}} | ||||||
|  | 	key2 := key{PolicyUID: types.NamespacedName{Name: "p2"}, BindingUID: types.NamespacedName{Name: "b2"}} | ||||||
|  | 	key3 := key{PolicyUID: types.NamespacedName{Name: "p3"}, BindingUID: types.NamespacedName{Name: "b3"}} | ||||||
|  |  | ||||||
|  | 	cm1v1 := &v1.ConfigMap{Data: map[string]string{"v": "1"}} | ||||||
|  |  | ||||||
|  | 	rc := policyReinvokeContext{} | ||||||
|  |  | ||||||
|  | 	// key1 is invoked and it updates the configmap | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key1) | ||||||
|  | 	rc.SetLastPolicyInvocationOutput(cm1v1) | ||||||
|  |  | ||||||
|  | 	assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1)) | ||||||
|  |  | ||||||
|  | 	// key2 is invoked but does not change anything | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key2) | ||||||
|  | 	rc.SetLastPolicyInvocationOutput(cm1v1) | ||||||
|  |  | ||||||
|  | 	assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1)) | ||||||
|  |  | ||||||
|  | 	// key3 is invoked but it does not change anything | ||||||
|  | 	rc.AddReinvocablePolicyToPreviouslyInvoked(key3) | ||||||
|  | 	rc.SetLastPolicyInvocationOutput(cm1v1) | ||||||
|  |  | ||||||
|  | 	assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1)) | ||||||
|  |  | ||||||
|  | 	// no keys are reinvoked | ||||||
|  | 	assert.False(t, rc.ShouldReinvoke(key1)) | ||||||
|  | 	assert.False(t, rc.ShouldReinvoke(key2)) | ||||||
|  | 	assert.False(t, rc.ShouldReinvoke(key3)) | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -436,7 +436,7 @@ func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*envi | |||||||
| 	) | 	) | ||||||
| } | } | ||||||
|  |  | ||||||
| // createVariableOpts creates a slice of ResolverEnvOption | // createVariableOpts creates a slice of EnvOption | ||||||
| // that can be used for creating a CEL env containing variables of declType. | // that can be used for creating a CEL env containing variables of declType. | ||||||
| // declType can be nil, in which case the variables will be of DynType. | // declType can be nil, in which case the variables will be of DynType. | ||||||
| func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption { | func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption { | ||||||
|   | |||||||
| @@ -235,11 +235,11 @@ func convertField(value ref.Val) (any, error) { | |||||||
| 	// unstructured maps, as seen in annotations | 	// unstructured maps, as seen in annotations | ||||||
| 	// map keys must be strings | 	// map keys must be strings | ||||||
| 	if mapOfVal, ok := value.Value().(map[ref.Val]ref.Val); ok { | 	if mapOfVal, ok := value.Value().(map[ref.Val]ref.Val); ok { | ||||||
| 		result := make(map[string]any) | 		result := make(map[string]any, len(mapOfVal)) | ||||||
| 		for k, v := range mapOfVal { | 		for k, v := range mapOfVal { | ||||||
| 			stringKey, ok := k.Value().(string) | 			stringKey, ok := k.Value().(string) | ||||||
| 			if !ok { | 			if !ok { | ||||||
| 				return nil, fmt.Errorf("map key %q is of type %t, not string", k, k) | 				return nil, fmt.Errorf("map key %q is of type %T, not string", k, k) | ||||||
| 			} | 			} | ||||||
| 			result[stringKey] = v.Value() | 			result[stringKey] = v.Value() | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -121,14 +121,21 @@ func (p *JSONPatchVal) ConvertToType(typeValue ref.Type) ref.Val { | |||||||
| 	} else if typeValue == types.TypeType { | 	} else if typeValue == types.TypeType { | ||||||
| 		return types.NewTypeTypeWithParam(jsonPatchType) | 		return types.NewTypeTypeWithParam(jsonPatchType) | ||||||
| 	} | 	} | ||||||
| 	return types.NewErr("Unsupported type: %s", typeValue.TypeName()) | 	return types.NewErr("unsupported type: %s", typeValue.TypeName()) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (p *JSONPatchVal) Equal(other ref.Val) ref.Val { | func (p *JSONPatchVal) Equal(other ref.Val) ref.Val { | ||||||
| 	if o, ok := other.(*JSONPatchVal); ok && p != nil && o != nil { | 	if o, ok := other.(*JSONPatchVal); ok && p != nil && o != nil { | ||||||
| 		if *p == *o { | 		if p.Op != o.Op || p.From != o.From || p.Path != o.Path { | ||||||
|  | 			return types.False | ||||||
|  | 		} | ||||||
|  | 		if (p.Val == nil) != (o.Val == nil) { | ||||||
|  | 			return types.False | ||||||
|  | 		} | ||||||
|  | 		if p.Val == nil { | ||||||
| 			return types.True | 			return types.True | ||||||
| 		} | 		} | ||||||
|  | 		return p.Val.Equal(o.Val) | ||||||
| 	} | 	} | ||||||
| 	return types.False | 	return types.False | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Joe Betz
					Joe Betz