From 6bf6f305b847d8c089cdae67f87835a552869483 Mon Sep 17 00:00:00 2001 From: sophie Date: Mon, 23 Sep 2024 02:27:38 -0400 Subject: [PATCH] feat: add fieldoverrider Signed-off-by: sophie --- go.mod | 2 +- pkg/util/overridemanager/overridemanager.go | 121 +++++++++ .../overridemanager/overridemanager_test.go | 241 +++++++++++++++++- pkg/util/validation/validation.go | 46 +++- pkg/webhook/overridepolicy/validating_test.go | 142 +++++++++++ test/e2e/coverage_docs/overridepolicy_test.md | 2 + test/e2e/overridepolicy_test.go | 219 ++++++++++++++++ 7 files changed, 768 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index ac69a20ec..550fe9d0a 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/emirpasic/gods v1.18.1 github.com/evanphx/json-patch/v5 v5.9.0 github.com/go-co-op/gocron v1.30.1 + github.com/go-openapi/jsonpointer v0.20.2 github.com/gogo/protobuf v1.3.2 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.5.0 @@ -90,7 +91,6 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/swag v0.22.7 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect diff --git a/pkg/util/overridemanager/overridemanager.go b/pkg/util/overridemanager/overridemanager.go index 4663e7e36..f6c7abbf4 100644 --- a/pkg/util/overridemanager/overridemanager.go +++ b/pkg/util/overridemanager/overridemanager.go @@ -19,14 +19,18 @@ package overridemanager import ( "context" "encoding/json" + "fmt" + "reflect" "sort" jsonpatch "github.com/evanphx/json-patch/v5" + "github.com/go-openapi/jsonpointer" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" @@ -280,6 +284,52 @@ func applyJSONPatch(obj *unstructured.Unstructured, overrides []overrideOption) return err } +// applyRawJSONPatch applies the override on to the given raw json object. +func applyRawJSONPatch(raw []byte, overrides []overrideOption) ([]byte, error) { + jsonPatchBytes, err := json.Marshal(overrides) + if err != nil { + return nil, err + } + + patch, err := jsonpatch.DecodePatch(jsonPatchBytes) + if err != nil { + return nil, err + } + + return patch.Apply(raw) +} + +func applyRawYAMLPatch(raw []byte, overrides []overrideOption) ([]byte, error) { + rawJSON, err := yaml.YAMLToJSON(raw) + if err != nil { + klog.ErrorS(err, "Failed to convert yaml to json") + return nil, err + } + + jsonPatchBytes, err := json.Marshal(overrides) + if err != nil { + return nil, err + } + + patch, err := jsonpatch.DecodePatch(jsonPatchBytes) + if err != nil { + return nil, err + } + + rawJSON, err = patch.Apply(rawJSON) + if err != nil { + return nil, err + } + + rawYAML, err := yaml.JSONToYAML(rawJSON) + if err != nil { + klog.Errorf("Failed to convert json to yaml, error: %v", err) + return nil, err + } + + return rawYAML, nil +} + // applyPolicyOverriders applies OverridePolicy/ClusterOverridePolicy overriders to target object func applyPolicyOverriders(rawObj *unstructured.Unstructured, overriders policyv1alpha1.Overriders) error { err := applyImageOverriders(rawObj, overriders.ImageOverrider) @@ -300,6 +350,9 @@ func applyPolicyOverriders(rawObj *unstructured.Unstructured, overriders policyv if err := applyAnnotationsOverriders(rawObj, overriders.AnnotationsOverrider); err != nil { return err } + if err := applyFieldOverriders(rawObj, overriders.FieldOverrider); err != nil { + return err + } return applyJSONPatch(rawObj, parseJSONPatchesByPlaintext(overriders.Plaintext)) } @@ -352,6 +405,50 @@ func applyArgsOverriders(rawObj *unstructured.Unstructured, argsOverriders []pol return nil } +func applyFieldOverriders(rawObj *unstructured.Unstructured, FieldOverriders []policyv1alpha1.FieldOverrider) error { + if len(FieldOverriders) == 0 { + return nil + } + for index := range FieldOverriders { + pointer, err := jsonpointer.New(FieldOverriders[index].FieldPath) + if err != nil { + klog.Errorf("Build jsonpointer with overrider's path err: %v", err) + return err + } + res, kind, err := pointer.Get(rawObj.Object) + if err != nil { + klog.Errorf("Get value by overrider's path err: %v", err) + return err + } + if kind != reflect.String { + errMsg := fmt.Sprintf("Get object's value by overrider's path(%s) is not string", FieldOverriders[index].FieldPath) + klog.Errorf(errMsg) + return fmt.Errorf(errMsg) + } + dataBytes := []byte(res.(string)) + klog.V(4).Infof("Parsed JSON patches by FieldOverriders[%d](%+v)", index, FieldOverriders[index]) + var appliedRawData []byte + if len(FieldOverriders[index].YAML) > 0 { + appliedRawData, err = applyRawYAMLPatch(dataBytes, parseYAMLPatchesByField(FieldOverriders[index].YAML)) + if err != nil { + klog.Errorf("Error applying raw JSON patch: %v", err) + return err + } + } else if len(FieldOverriders[index].JSON) > 0 { + appliedRawData, err = applyRawJSONPatch(dataBytes, parseJSONPatchesByField(FieldOverriders[index].JSON)) + if err != nil { + klog.Errorf("Error applying raw YAML patch: %v", err) + return err + } + } + _, err = pointer.Set(rawObj.Object, string(appliedRawData)) + if err != nil { + return err + } + } + return nil +} + func parseJSONPatchesByPlaintext(overriders []policyv1alpha1.PlaintextOverrider) []overrideOption { patches := make([]overrideOption, 0, len(overriders)) for i := range overriders { @@ -363,3 +460,27 @@ func parseJSONPatchesByPlaintext(overriders []policyv1alpha1.PlaintextOverrider) } return patches } + +func parseYAMLPatchesByField(overriders []policyv1alpha1.YAMLPatchOperation) []overrideOption { + patches := make([]overrideOption, 0, len(overriders)) + for i := range overriders { + patches = append(patches, overrideOption{ + Op: string(overriders[i].Operator), + Path: overriders[i].SubPath, + Value: overriders[i].Value, + }) + } + return patches +} + +func parseJSONPatchesByField(overriders []policyv1alpha1.JSONPatchOperation) []overrideOption { + patches := make([]overrideOption, 0, len(overriders)) + for i := range overriders { + patches = append(patches, overrideOption{ + Op: string(overriders[i].Operator), + Path: overriders[i].SubPath, + Value: overriders[i].Value, + }) + } + return patches +} diff --git a/pkg/util/overridemanager/overridemanager_test.go b/pkg/util/overridemanager/overridemanager_test.go index e703d5c6a..ae66e6cee 100644 --- a/pkg/util/overridemanager/overridemanager_test.go +++ b/pkg/util/overridemanager/overridemanager_test.go @@ -34,7 +34,7 @@ import ( "github.com/karmada-io/karmada/test/helper" ) -func Test_overrideManagerImpl_ApplyOverridePolicies(t *testing.T) { +func Test_overrideManagerImpl_ApplyLabelAnnotationOverriderPolicies(t *testing.T) { deployment := helper.NewDeployment(metav1.NamespaceDefault, "test1") deployment.Labels = map[string]string{ "testLabel": "testLabel", @@ -442,3 +442,242 @@ func TestGetMatchingOverridePolicies(t *testing.T) { }) } } + +func Test_overrideManagerImpl_ApplyFieldOverriderPolicies_YAML(t *testing.T) { + configmap := helper.NewConfigMap(metav1.NamespaceDefault, "test1", map[string]string{ + "test.yaml": ` +key: + key1: value +`, + }) + configmapObj, _ := utilhelper.ToUnstructured(configmap) + + type fields struct { + Client client.Client + EventRecorder record.EventRecorder + } + type args struct { + rawObj *unstructured.Unstructured + clusterName string + } + tests := []struct { + name string + fields fields + args args + wantCOP *AppliedOverrides + wantOP *AppliedOverrides + wantErr bool + }{ + { + name: "test yaml overridePolicies", + fields: fields{ + Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithObjects(helper.NewCluster("test1"), + &policyv1alpha1.OverridePolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test1", Namespace: metav1.NamespaceDefault}, + Spec: policyv1alpha1.OverrideSpec{ + ResourceSelectors: []policyv1alpha1.ResourceSelector{ + { + APIVersion: "v1", + Kind: "ConfigMap", + Namespace: "default", + Name: "test1", + }, + }, + OverrideRules: []policyv1alpha1.RuleWithCluster{ + { + TargetCluster: &policyv1alpha1.ClusterAffinity{ClusterNames: []string{"test1"}}, + Overriders: policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "/data/test.yaml", + YAML: []policyv1alpha1.YAMLPatchOperation{ + { + SubPath: "/key/key1", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte(`"updated_value"`)}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ).Build(), + EventRecorder: &record.FakeRecorder{}, + }, + args: args{ + rawObj: configmapObj, + clusterName: "test1", + }, + wantCOP: nil, + wantOP: &AppliedOverrides{ + AppliedItems: []OverridePolicyShadow{ + { + PolicyName: "test1", + Overriders: policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "/data/test.yaml", + YAML: []policyv1alpha1.YAMLPatchOperation{ + { + SubPath: "/key/key1", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte(`"updated_value"`)}, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &overrideManagerImpl{ + Client: tt.fields.Client, + EventRecorder: tt.fields.EventRecorder, + } + gotCOP, gotOP, err := o.ApplyOverridePolicies(tt.args.rawObj, tt.args.clusterName) + if (err != nil) != tt.wantErr { + t.Errorf("ApplyOverridePolicies() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotCOP, tt.wantCOP) { + t.Errorf("ApplyOverridePolicies() gotCOP = %v, wantCOP %v", gotCOP, tt.wantCOP) + } + if !reflect.DeepEqual(gotOP, tt.wantOP) { + t.Errorf("ApplyOverridePolicies() gotOP = %v, wantOP %v", gotOP, tt.wantOP) + } + wantData := map[string]interface{}{ + "test.yaml": `key: + key1: updated_value +`, + } + if !reflect.DeepEqual(tt.args.rawObj.Object["data"], wantData) { + t.Errorf("ApplyOverridePolicies() gotData = %v, wantData %v", tt.args.rawObj.Object["data"], wantData) + } + }) + } +} + +func Test_overrideManagerImpl_ApplyJSONOverridePolicies_JSON(t *testing.T) { + configmap := helper.NewConfigMap(metav1.NamespaceDefault, "test1", map[string]string{ + "test.json": `{"key":{"key1":"value"}}`, + }) + configmapObj, _ := utilhelper.ToUnstructured(configmap) + + type fields struct { + Client client.Client + EventRecorder record.EventRecorder + } + type args struct { + rawObj *unstructured.Unstructured + clusterName string + } + tests := []struct { + name string + fields fields + args args + wantCOP *AppliedOverrides + wantOP *AppliedOverrides + wantErr bool + }{ + { + name: "test yaml overridePolicies", + fields: fields{ + Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithObjects(helper.NewCluster("test1"), + &policyv1alpha1.OverridePolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test1", Namespace: metav1.NamespaceDefault}, + Spec: policyv1alpha1.OverrideSpec{ + ResourceSelectors: []policyv1alpha1.ResourceSelector{ + { + APIVersion: "v1", + Kind: "ConfigMap", + Namespace: "default", + Name: "test1", + }, + }, + OverrideRules: []policyv1alpha1.RuleWithCluster{ + { + TargetCluster: &policyv1alpha1.ClusterAffinity{ClusterNames: []string{"test1"}}, + Overriders: policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "/data/test.json", + JSON: []policyv1alpha1.JSONPatchOperation{ + { + SubPath: "/key/key1", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte(`"updated_value"`)}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ).Build(), + EventRecorder: &record.FakeRecorder{}, + }, + args: args{ + rawObj: configmapObj, + clusterName: "test1", + }, + wantCOP: nil, + wantOP: &AppliedOverrides{ + AppliedItems: []OverridePolicyShadow{ + { + PolicyName: "test1", + Overriders: policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "/data/test.json", + JSON: []policyv1alpha1.JSONPatchOperation{ + { + SubPath: "/key/key1", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte(`"updated_value"`)}, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &overrideManagerImpl{ + Client: tt.fields.Client, + EventRecorder: tt.fields.EventRecorder, + } + gotCOP, gotOP, err := o.ApplyOverridePolicies(tt.args.rawObj, tt.args.clusterName) + if (err != nil) != tt.wantErr { + t.Errorf("ApplyOverridePolicies() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotCOP, tt.wantCOP) { + t.Errorf("ApplyOverridePolicies() gotCOP = %v, wantCOP %v", gotCOP, tt.wantCOP) + } + if !reflect.DeepEqual(gotOP, tt.wantOP) { + t.Errorf("ApplyOverridePolicies() gotOP = %v, wantOP %v", gotOP, tt.wantOP) + } + wantData := map[string]interface{}{ + "test.json": `{"key":{"key1":"updated_value"}}`, + } + if !reflect.DeepEqual(tt.args.rawObj.Object["data"], wantData) { + t.Errorf("ApplyOverridePolicies() gotData = %v, wantData %v", tt.args.rawObj.Object["data"], wantData) + } + }) + } +} diff --git a/pkg/util/validation/validation.go b/pkg/util/validation/validation.go index 3c3cfc8ae..59d1b4289 100644 --- a/pkg/util/validation/validation.go +++ b/pkg/util/validation/validation.go @@ -18,8 +18,8 @@ package validation import ( "fmt" - "strings" + "github.com/go-openapi/jsonpointer" corev1 "k8s.io/api/core/v1" apivalidation "k8s.io/apimachinery/pkg/api/validation" metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" @@ -304,13 +304,53 @@ func ValidateOverrideRules(overrideRules []policyv1alpha1.RuleWithCluster, fldPa // validates predicate path. for imageIndex, image := range rule.Overriders.ImageOverrider { imagePath := rulePath.Child("overriders").Child("imageOverrider").Index(imageIndex) - if image.Predicate != nil && !strings.HasPrefix(image.Predicate.Path, "/") { - allErrs = append(allErrs, field.Invalid(imagePath.Child("predicate").Child("path"), image.Predicate.Path, "path should be start with / character")) + if image.Predicate != nil { + if _, err := jsonpointer.New(image.Predicate.Path); err != nil { + allErrs = append(allErrs, field.Invalid(imagePath.Child("predicate").Child("path"), image.Predicate.Path, err.Error())) + } } } + for fieldIndex, fieldOverrider := range rule.Overriders.FieldOverrider { + fieldPath := rulePath.Child("overriders").Child("fieldOverrider").Index(fieldIndex) + // validates that either YAML or JSON is selected for each field overrider. + if len(fieldOverrider.YAML) > 0 && len(fieldOverrider.JSON) > 0 { + allErrs = append(allErrs, field.Invalid(fieldPath, fieldOverrider, "FieldOverrider has both YAML and JSON set. Only one is allowed")) + } + // validates the field path. + if _, err := jsonpointer.New(fieldOverrider.FieldPath); err != nil { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("fieldPath"), fieldOverrider.FieldPath, err.Error())) + } + // validates the JSON patch operations sub path. + allErrs = append(allErrs, validateJSONPatchSubPaths(fieldOverrider.JSON, fieldPath.Child("json"))...) + // validates the YAML patch operations sub path. + allErrs = append(allErrs, validateYAMLPatchSubPaths(fieldOverrider.YAML, fieldPath.Child("yaml"))...) + } + // validates the targetCluster. allErrs = append(allErrs, ValidateClusterAffinity(rule.TargetCluster, rulePath.Child("targetCluster"))...) } return allErrs } + +func validateJSONPatchSubPaths(patches []policyv1alpha1.JSONPatchOperation, fieldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + for index, patch := range patches { + patchPath := fieldPath.Index(index) + if _, err := jsonpointer.New(patch.SubPath); err != nil { + allErrs = append(allErrs, field.Invalid(patchPath.Child("subPath"), patch.SubPath, err.Error())) + } + } + return allErrs +} + +func validateYAMLPatchSubPaths(patches []policyv1alpha1.YAMLPatchOperation, fieldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + for index, patch := range patches { + patchPath := fieldPath.Index(index) + if _, err := jsonpointer.New(patch.SubPath); err != nil { + allErrs = append(allErrs, field.Invalid(patchPath.Child("subPath"), patch.SubPath, err.Error())) + } + } + return allErrs +} diff --git a/pkg/webhook/overridepolicy/validating_test.go b/pkg/webhook/overridepolicy/validating_test.go index cf299f83d..0c9786793 100644 --- a/pkg/webhook/overridepolicy/validating_test.go +++ b/pkg/webhook/overridepolicy/validating_test.go @@ -24,6 +24,7 @@ import ( "strings" "testing" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -154,6 +155,147 @@ func TestValidatingAdmission_Handle(t *testing.T) { Message: "", }, }, + { + name: "Handle_FieldOverrider_ContainsBothYAMLAndJSON_DeniesAdmission", + decoder: &fakeValidationDecoder{ + obj: &policyv1alpha1.OverridePolicy{ + Spec: policyv1alpha1.OverrideSpec{ + ResourceSelectors: []policyv1alpha1.ResourceSelector{ + {APIVersion: "test-apiversion", Kind: "test"}, + }, + OverrideRules: []policyv1alpha1.RuleWithCluster{ + { + TargetCluster: &policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"member1"}, + }, + Overriders: policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "/data/config", + JSON: []policyv1alpha1.JSONPatchOperation{ + { + SubPath: "/db-config", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte(`{"db": "new"}`)}, + }, + }, + YAML: []policyv1alpha1.YAMLPatchOperation{ + { + SubPath: "/db-config", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte("db: new")}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + req: admission.Request{}, + want: TestResponse{ + Type: Denied, + Message: "FieldOverrider has both YAML and JSON set. Only one is allowed", + }, + }, + { + name: "Handle_InvalidFieldPathInYAML_DeniesAdmission", + decoder: &fakeValidationDecoder{ + obj: &policyv1alpha1.OverridePolicy{ + Spec: policyv1alpha1.OverrideSpec{ + OverrideRules: []policyv1alpha1.RuleWithCluster{ + { + Overriders: policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "invalidPath", + YAML: []policyv1alpha1.YAMLPatchOperation{ + { + SubPath: "/db-config", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte("db: new")}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + req: admission.Request{}, + want: TestResponse{ + Type: Denied, + Message: "spec.overrideRules[0].overriders.fieldOverrider[0].fieldPath: Invalid value: \"invalidPath\": JSON pointer must be empty or start with a \"/", + }, + }, + { + name: "Handle_InvalidJSONSubPath_DeniesAdmission", + decoder: &fakeValidationDecoder{ + obj: &policyv1alpha1.OverridePolicy{ + Spec: policyv1alpha1.OverrideSpec{ + OverrideRules: []policyv1alpha1.RuleWithCluster{ + { + Overriders: policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "/data/config", + JSON: []policyv1alpha1.JSONPatchOperation{ + { + SubPath: "invalidSubPath", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte(`{"db": "new"}`)}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + req: admission.Request{}, + want: TestResponse{ + Type: Denied, + Message: "spec.overrideRules[0].overriders.fieldOverrider[0].json[0].subPath: Invalid value: \"invalidSubPath\": JSON pointer must be empty or start with a \"/", + }, + }, + { + name: "Handle_InvalidYAMLSubPath_DeniesAdmission", + decoder: &fakeValidationDecoder{ + obj: &policyv1alpha1.OverridePolicy{ + Spec: policyv1alpha1.OverrideSpec{ + OverrideRules: []policyv1alpha1.RuleWithCluster{ + { + Overriders: policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "/data/config", + YAML: []policyv1alpha1.YAMLPatchOperation{ + { + SubPath: "invalidSubPath", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte("db: new")}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + req: admission.Request{}, + want: TestResponse{ + Type: Denied, + Message: "spec.overrideRules[0].overriders.fieldOverrider[0].yaml[0].subPath: Invalid value: \"invalidSubPath\": JSON pointer must be empty or start with a \"/", + }, + }, } for _, tt := range tests { diff --git a/test/e2e/coverage_docs/overridepolicy_test.md b/test/e2e/coverage_docs/overridepolicy_test.md index 042b93564..9de4380e3 100644 --- a/test/e2e/coverage_docs/overridepolicy_test.md +++ b/test/e2e/coverage_docs/overridepolicy_test.md @@ -8,6 +8,8 @@ | Check if the OverridePolicy will update the deployment's image value | deployment imageOverride testing | | | Check if the OverridePolicy will update the pod's image value | pod imageOverride testing | | | Check if the OverridePolicy will update the specific image value | deployment imageOverride testing | | +| Check if the OverridePolicy will update the value inside JSON | deployment fieldOverride testing | | +| Check if the OverridePolicy will update the value inside YAML | deployment fieldOverride testing | | #### OverridePolicy with nil resourceSelector testing | Test Case | E2E Describe Text | Comments | diff --git a/test/e2e/overridepolicy_test.go b/test/e2e/overridepolicy_test.go index a7f8be920..663e4951d 100644 --- a/test/e2e/overridepolicy_test.go +++ b/test/e2e/overridepolicy_test.go @@ -17,9 +17,13 @@ limitations under the License. package e2e import ( + "fmt" + "strings" + "github.com/onsi/ginkgo/v2" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/util/rand" "k8s.io/klog/v2" @@ -425,6 +429,221 @@ var _ = ginkgo.Describe("[OverridePolicy] apply overriders testing", func() { }) }) }) + + ginkgo.Context("[FieldOverrider] apply field overrider testing to update JSON values in ConfigMap", func() { + var configMapNamespace, configMapName string + var configMap *corev1.ConfigMap + + ginkgo.BeforeEach(func() { + configMapNamespace = testNamespace + configMapName = configMapNamePrefix + rand.String(RandomStrLength) + propagationPolicyNamespace = testNamespace + propagationPolicyName = configMapName + overridePolicyNamespace = testNamespace + overridePolicyName = configMapName + + configMapData := map[string]string{ + "deploy.json": fmt.Sprintf(`{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "nginx-deploy", + "namespace": "%s" + }, + "spec": { + "replicas": 3, + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [ + { + "name": "nginx", + "image": "nginx:1.19.0" + } + ] + } + } + } + }`, configMapNamespace), + } + + configMap = helper.NewConfigMap(configMapNamespace, configMapName, configMapData) + propagationPolicy = helper.NewPropagationPolicy(propagationPolicyNamespace, propagationPolicyName, []policyv1alpha1.ResourceSelector{ + { + APIVersion: configMap.APIVersion, + Kind: configMap.Kind, + Name: configMap.Name, + }, + }, policyv1alpha1.Placement{ + ClusterAffinity: &policyv1alpha1.ClusterAffinity{ + ClusterNames: framework.ClusterNames(), + }, + }) + + overridePolicy = helper.NewOverridePolicy(overridePolicyNamespace, overridePolicyName, []policyv1alpha1.ResourceSelector{ + { + APIVersion: configMap.APIVersion, + Kind: configMap.Kind, + Name: configMap.Name, + }, + }, policyv1alpha1.ClusterAffinity{ + ClusterNames: framework.ClusterNames(), + }, policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "/data/deploy.json", + JSON: []policyv1alpha1.JSONPatchOperation{ + { + SubPath: "/spec/replicas", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte(`5`)}, + }, + { + SubPath: "/spec/template/spec/containers/-", + Operator: policyv1alpha1.OverriderOpAdd, + Value: apiextensionsv1.JSON{Raw: []byte(`{"name": "nginx-helper", "image": "nginx:1.19.1"}`)}, + }, + { + SubPath: "/spec/template/spec/containers/0/image", + Operator: policyv1alpha1.OverriderOpRemove, + }, + }, + }, + }, + }) + }) + + ginkgo.BeforeEach(func() { + framework.CreatePropagationPolicy(karmadaClient, propagationPolicy) + framework.CreateOverridePolicy(karmadaClient, overridePolicy) + framework.CreateConfigMap(kubeClient, configMap) + ginkgo.DeferCleanup(func() { + framework.RemovePropagationPolicy(karmadaClient, propagationPolicy.Namespace, propagationPolicy.Name) + framework.RemoveOverridePolicy(karmadaClient, overridePolicy.Namespace, overridePolicy.Name) + framework.RemoveConfigMap(kubeClient, configMap.Namespace, configMap.Name) + }) + }) + + ginkgo.It("should override JSON field in ConfigMap", func() { + klog.Infof("check if configMap present on member clusters has the correct JSON field value.") + framework.WaitConfigMapPresentOnClustersFitWith(framework.ClusterNames(), configMap.Namespace, configMap.Name, + func(cm *corev1.ConfigMap) bool { + return strings.Contains(cm.Data["deploy.json"], `"replicas":5`) && + strings.Contains(cm.Data["deploy.json"], `"name":"nginx-helper"`) && + !strings.Contains(cm.Data["deploy.json"], `"image":"nginx:1.19.0"`) + }) + }) + }) + + ginkgo.Context("[FieldOverrider] apply field overrider testing to update YAML values in ConfigMap", func() { + var configMapNamespace, configMapName string + var configMap *corev1.ConfigMap + + ginkgo.BeforeEach(func() { + configMapNamespace = testNamespace + configMapName = configMapNamePrefix + rand.String(RandomStrLength) + propagationPolicyNamespace = testNamespace + propagationPolicyName = configMapName + overridePolicyNamespace = testNamespace + overridePolicyName = configMapName + + // Define the ConfigMap data + configMapData := map[string]string{ + "nginx.yaml": ` +server: + listen: 80 + server_name: localhost + location /: + root: /usr/share/nginx/html + index: + - index.html + - index.htm + error_page: + - code: 500 + - code: 502 + - code: 503 + - code: 504 + location /50x.html: + root: /usr/share/nginx/html +`, + } + configMap = helper.NewConfigMap(configMapNamespace, configMapName, configMapData) + propagationPolicy = helper.NewPropagationPolicy(propagationPolicyNamespace, propagationPolicyName, []policyv1alpha1.ResourceSelector{ + { + APIVersion: configMap.APIVersion, + Kind: configMap.Kind, + Name: configMap.Name, + }, + }, policyv1alpha1.Placement{ + ClusterAffinity: &policyv1alpha1.ClusterAffinity{ + ClusterNames: framework.ClusterNames(), + }, + }) + + overridePolicy = helper.NewOverridePolicy(overridePolicyNamespace, overridePolicyName, []policyv1alpha1.ResourceSelector{ + { + APIVersion: configMap.APIVersion, + Kind: configMap.Kind, + Name: configMap.Name, + }, + }, policyv1alpha1.ClusterAffinity{ + ClusterNames: framework.ClusterNames(), + }, policyv1alpha1.Overriders{ + FieldOverrider: []policyv1alpha1.FieldOverrider{ + { + FieldPath: "/data/nginx.yaml", + YAML: []policyv1alpha1.YAMLPatchOperation{ + { + SubPath: "/server/location ~1/root", + Operator: policyv1alpha1.OverriderOpReplace, + Value: apiextensionsv1.JSON{Raw: []byte(`"/var/www/html"`)}, + }, + { + SubPath: "/server/error_page/-", + Operator: policyv1alpha1.OverriderOpAdd, + Value: apiextensionsv1.JSON{Raw: []byte(`{"code": 400}`)}, + }, + { + SubPath: "/server/location ~1/index", + Operator: policyv1alpha1.OverriderOpRemove, + }, + }, + }, + }, + }) + }) + + ginkgo.BeforeEach(func() { + framework.CreatePropagationPolicy(karmadaClient, propagationPolicy) + framework.CreateOverridePolicy(karmadaClient, overridePolicy) + framework.CreateConfigMap(kubeClient, configMap) + ginkgo.DeferCleanup(func() { + framework.RemovePropagationPolicy(karmadaClient, propagationPolicy.Namespace, propagationPolicy.Name) + framework.RemoveOverridePolicy(karmadaClient, overridePolicy.Namespace, overridePolicy.Name) + framework.RemoveConfigMap(kubeClient, configMap.Namespace, configMap.Name) + }) + }) + + ginkgo.It("should override YAML field in ConfigMap", func() { + klog.Infof("check if configMap present on member clusters has the correct YAML field value.") + framework.WaitConfigMapPresentOnClustersFitWith(framework.ClusterNames(), configMap.Namespace, configMap.Name, + func(cm *corev1.ConfigMap) bool { + return strings.Contains(cm.Data["nginx.yaml"], "root: /var/www/html") && + strings.Contains(cm.Data["nginx.yaml"], "code: 400") && + !strings.Contains(cm.Data["nginx.yaml"], "- index.html") + }) + }) + }) + }) var _ = framework.SerialDescribe("OverridePolicy with nil resourceSelector testing", func() {