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