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" | ||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||
| 	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" | ||||
| 	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" | ||||
| 	"k8s.io/apiserver/pkg/cel" | ||||
| @@ -1493,7 +1493,7 @@ func validateApplyConfiguration(compiler plugincel.Compiler, applyConfig *admiss | ||||
| 		if opts.preexistingExpressions.applyConfigurationExpressions.Has(applyConfig.Expression) { | ||||
| 			envType = environment.StoredExpressions | ||||
| 		} | ||||
| 		accessor := &mutating.ApplyConfigurationCondition{ | ||||
| 		accessor := &patch.ApplyConfigurationCondition{ | ||||
| 			Expression: trimmedExpression, | ||||
| 		} | ||||
| 		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) { | ||||
| 			envType = environment.StoredExpressions | ||||
| 		} | ||||
| 		accessor := &mutating.JSONPatchCondition{ | ||||
| 		accessor := &patch.JSONPatchCondition{ | ||||
| 			Expression: trimmedExpression, | ||||
| 		} | ||||
| 		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) { | ||||
| 	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) { | ||||
| @@ -227,7 +227,7 @@ func TestCategories(t *testing.T) { | ||||
| 	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{ | ||||
| 		Group:    "rules.example.com", | ||||
| 		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 | ||||
| // request. It then resolves all params and creates an Invocation for each | ||||
| // 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' 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 { | ||||
| 		case v1alpha1.PatchTypeJSONPatch: | ||||
| 			if m.JSONPatch != nil { | ||||
| 				accessor := &JSONPatchCondition{Expression: m.JSONPatch.Expression} | ||||
| 				accessor := &patch.JSONPatchCondition{Expression: m.JSONPatch.Expression} | ||||
| 				compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions) | ||||
| 				patchers = append(patchers, patch.NewJSONPatcher(compileResult)) | ||||
| 			} | ||||
| 		case v1alpha1.PatchTypeApplyConfiguration: | ||||
| 			if m.ApplyConfiguration != nil { | ||||
| 				accessor := &ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression} | ||||
| 				accessor := &patch.ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression} | ||||
| 				compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions) | ||||
| 				patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult)) | ||||
| 			} | ||||
|   | ||||
| @@ -21,16 +21,17 @@ import ( | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"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" | ||||
| 	"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" | ||||
| 	"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" | ||||
| @@ -45,6 +46,7 @@ import ( | ||||
| // on the results. | ||||
| func TestCompilation(t *testing.T) { | ||||
| 	deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} | ||||
| 	deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"} | ||||
| 	testCases := []struct { | ||||
| 		name           string | ||||
| 		policy         *Policy | ||||
| @@ -56,429 +58,6 @@ func TestCompilation(t *testing.T) { | ||||
| 		expectedErr    string | ||||
| 		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", | ||||
| 			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)}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "apply configuration add to listType=map", | ||||
| 			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" | ||||
| 								}] | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				}`), | ||||
| 			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 | ||||
| 					} | ||||
| 				}`), | ||||
| 			name: "jsonPatch 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)}}, | ||||
| 			oldObject:      &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}}, | ||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}}, | ||||
| 			expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](11)}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			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)}}, | ||||
| 			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", | ||||
| 			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)}}, | ||||
| 			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", | ||||
| 			policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ | ||||
| @@ -901,10 +300,18 @@ func TestCompilation(t *testing.T) { | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	defer cancel() | ||||
| 	t.Cleanup(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) | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			var gvk schema.GroupVersionKind | ||||
| @@ -939,7 +346,7 @@ func TestCompilation(t *testing.T) { | ||||
|  | ||||
| 			for _, patcher := range policyEvaluator.Mutators { | ||||
| 				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) | ||||
| 				vAttrs := &admission.VersionedAttributes{ | ||||
| 					Attributes:         attrs, | ||||
| @@ -1038,6 +445,11 @@ func mutations(policy *v1alpha1.MutatingAdmissionPolicy, mutations ...v1alpha1.M | ||||
| 	return policy | ||||
| } | ||||
|  | ||||
| func matchConstraints(policy *v1alpha1.MutatingAdmissionPolicy, matchConstraints *v1alpha1.MatchResources) *v1alpha1.MutatingAdmissionPolicy { | ||||
| 	policy.Spec.MatchConstraints = matchConstraints | ||||
| 	return policy | ||||
| } | ||||
|  | ||||
| type fakeAuthorizer struct{} | ||||
|  | ||||
| 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] { | ||||
| 	res := &dispatcher{ | ||||
| 		matcher: m, | ||||
| 		authz:   a, | ||||
| 		//!TODO: pass in static type converter to reduce network calls | ||||
| 		matcher:              m, | ||||
| 		authz:                a, | ||||
| 		typeConverterManager: tcm, | ||||
| 	} | ||||
| 	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 | ||||
| 			// many evaluators as there are mutations | ||||
| 			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) | ||||
| @@ -152,6 +151,7 @@ func (d *dispatcher) dispatchInvocations( | ||||
| 			matchResults := invocation.Evaluator.Matcher.Match(ctx, versionedAttr, invocation.Param, authz) | ||||
| 			if matchResults.Error != nil { | ||||
| 				addConfigError(matchResults.Error, invocation, metav1.StatusReasonInvalid) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// 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. | ||||
| 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) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import ( | ||||
| 	gojson "encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	celgo "github.com/google/cel-go/cel" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
|  | ||||
| @@ -41,6 +42,24 @@ import ( | ||||
| 	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. | ||||
| func NewJSONPatcher(patchEvaluator plugincel.MutatingEvaluator) Patcher { | ||||
| 	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 | ||||
|  | ||||
| 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" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	celgo "github.com/google/cel-go/cel" | ||||
| 	celtypes "github.com/google/cel-go/common/types" | ||||
| 	"strings" | ||||
|  | ||||
| 	"sigs.k8s.io/structured-merge-diff/v4/fieldpath" | ||||
| @@ -35,6 +37,24 @@ import ( | ||||
| 	"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. | ||||
| func NewApplyConfigurationPatcher(expressionEvaluator plugincel.MutatingEvaluator) Patcher { | ||||
| 	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 | ||||
| // 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 { | ||||
| 	atomics := findAtomics(nil, v.Schema(), v.TypeRef(), v.AsValue()) | ||||
| 	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 PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding | ||||
| type PolicyMutation = v1alpha1.Mutation | ||||
| @@ -80,6 +79,7 @@ type PolicyEvaluator struct { | ||||
| 	Error          error | ||||
| } | ||||
|  | ||||
| // Plugin is an implementation of admission.Interface. | ||||
| type Plugin struct { | ||||
| 	*generic.Plugin[PolicyHook] | ||||
| } | ||||
|   | ||||
| @@ -119,6 +119,68 @@ func TestBasicPatch(t *testing.T) { | ||||
| 	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) { | ||||
| 	patchObj := &unstructured.Unstructured{ | ||||
| 		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"); | ||||
| 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. | ||||
| // declType can be nil, in which case the variables will be of DynType. | ||||
| 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 | ||||
| 	// map keys must be strings | ||||
| 	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 { | ||||
| 			stringKey, ok := k.Value().(string) | ||||
| 			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() | ||||
| 		} | ||||
|   | ||||
| @@ -121,14 +121,21 @@ func (p *JSONPatchVal) ConvertToType(typeValue ref.Type) ref.Val { | ||||
| 	} else if typeValue == types.TypeType { | ||||
| 		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 { | ||||
| 	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 p.Val.Equal(o.Val) | ||||
| 	} | ||||
| 	return types.False | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Joe Betz
					Joe Betz