Repository: clusternet/clusternet (go)

Issue:
Globalization enhancement, support cluster affinity and json/yaml text field override
#### What would you like to be added:

feature 1:
globalization add cluster affinity to select target cluster

```yaml
apiVersion: apps.clusternet.io/v1alpha1
kind: Globalization
metadata:
  name: nginx-global-overrides-demo
spec:
  priority: 600
  feed:
    apiVersion: apps/v1
    kind: Deployment
    name: my-nginx
    namespace: foo
  clusterAffinity:
    region: shanghai
  overrides:
    - name: add-update-labels
      type: MergePatch
      # Value is a YAML/JSON format patch that provides MergePatch to current resource defined by feed.
      # This override adds or updates some labels.
      value: |-
        metadata:
          labels:
            some-label: some-value
            another-label: another-value
```

feature: 2:
overrides support field path to choose configmap or secret data field

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: db-conf
  namespace: foo
data:
  db.conf: |
    mysql:
      .....
      address: "default.mysql.db"
    otherconfigs:
      ....
---

apiVersion: apps.clusternet.io/v1alpha1
kind: Globalization
metadata:
  name: db-conf-overrides
spec:
  # Priority is an integer defining the relative importance of this Globalization compared to others.
  # Lower numbers are considered lower priority.
  # Override values in lower Globalization will be overridden by those in higher Globalization.
  # (Optional) Default priority is 500.
  priority: 600
  feed:
    apiVersion: v1
    kind: ConfigMap
    name: db-conf
    namespace: foo
  clusterAffinity:
    region: shanghai
  overrides: # defines all the overrides to be processed with
    - name: modify-db-addreess
      fieldPath: /data/db.conf
      type: MergePatch
      value: |-
        mysql:
          address: "shanghai.mysql.db:3306"
```

#### Why is this needed:

If I have a configmap that stores the DB address for each region, and this DB address is different in each cluster. Assuming I have 2 regions, each with 3 clusters, I would need to define 6 localizations through Clusternet, and each localization would need to specify the namespace of the member cluster, which feels quite inconvenient.






Pull request:
feature: add field patch type
#### What type of PR is this?

Fixes #882

#### What this PR does / why we need it:

add field patch type

#### Special notes for your reviewer:


Patch:
diff --git a/examples/fieldpatch/server-configmap-globalization.yaml b/examples/fieldpatch/server-configmap-globalization.yaml
new file mode 100644
index 0000000..6d2fd18
--- /dev/null
+++ b/examples/fieldpatch/server-configmap-globalization.yaml
@@ -0,0 +1,65 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: server-conf
+  namespace: qux
+data:
+  server.conf: |
+    {
+        "db_address": "127.0.0.1:3306",
+        "region_id": "default",
+        "zone_id": "default",
+        "server":{
+            "tls_ca_file": "/data/ca.crt",
+            "tls_cert_file": "/data/server.crt",
+            "tls_key_file": "/data/server.key",
+            "tls_passwd_file": "/data/encrypt.key",
+            "bind_ip": "::",
+            "bind_port": 8080,
+            "bind_port_v1": 8081
+        },
+        "metric":{
+            "exporter_bind_ip": "::",
+            "exporter_bind_port": 29402,
+            "exporter_thread_num": 8
+        }
+    }
+---
+
+apiVersion: apps.clusternet.io/v1alpha1
+kind: Globalization
+metadata:
+  name: server-conf-overrides-demo
+  namespace: qux
+spec:
+  # Priority is an integer defining the relative importance of this Globalization compared to others.
+  # Lower numbers are considered lower priority.
+  # Override values in lower Globalization will be overridden by those in higher Globalization.
+  # (Optional) Default priority is 500.
+  priority: 600
+  feed:
+    apiVersion: v1
+    kind: ConfigMap
+    name: server-conf
+    namespace: qux
+  clusterAffinity: # defines the clusters to which this Globalization applies
+    matchExpressions:
+      - key: clusters.clusternet.io/cluster-name
+        operator: In
+        values:
+        - us-west
+  overrides: # defines all the overrides to be processed with
+    - name: update-zoneid-regionid
+      type: FieldMergePatch
+      # Path is the path of the field to be patched.
+      fieldPath: /data/server.conf
+      # FieldFormat is the format of the field to be patched.
+      fieldFormat: JSON
+      # Value is a YAML/JSON format patch that provides FieldMergePatch to current resource defined by feed.
+      # This override adds or updates some labels.
+      value: |-
+        {
+            "db_address": "local:3306",
+            "region_id": "us-west-1",
+            "zone_id": "zone-1"
+        }
\ No newline at end of file
diff --git a/examples/fieldpatch/server-deployment.yaml b/examples/fieldpatch/server-deployment.yaml
new file mode 100644
index 0000000..7ccfda7
--- /dev/null
+++ b/examples/fieldpatch/server-deployment.yaml
@@ -0,0 +1,33 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: qux
+---
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: server-deployment
+  namespace: qux
+spec:
+  selector:
+    matchLabels:
+      app: server
+  replicas: 3
+  template:
+    metadata:
+      labels:
+        app: server
+    spec:
+      containers:
+        - name: nginx
+          image: nginx:1.14.2
+          ports:
+            - containerPort: 80
+          volumeMounts:
+            - name: config-volume
+              mountPath: "/etc/config"
+      volumes:
+      - name: config-volume
+        configMap:
+          name: server-conf
\ No newline at end of file
diff --git a/examples/fieldpatch/subscription.yaml b/examples/fieldpatch/subscription.yaml
new file mode 100644
index 0000000..159e867
--- /dev/null
+++ b/examples/fieldpatch/subscription.yaml
@@ -0,0 +1,25 @@
+apiVersion: apps.clusternet.io/v1alpha1
+kind: Subscription
+metadata:
+  name: dynamic-dividing-scheduling-demo
+spec:
+  subscribers: # filter out a set of desired clusters
+    - clusterAffinity:
+        matchExpressions:
+          - key: clusters.clusternet.io/cluster-name
+            operator: Exists
+  schedulingStrategy: Dividing
+  priority: 100
+  dividingScheduling:
+    type: Dynamic
+    dynamicDividing:
+      strategy: Spread # currently we only support Spread dividing strategy
+  feeds: # defines all the resources to be deployed with
+    - apiVersion: apps/v1 # with a total of 6 replicas
+      kind: Deployment
+      name: my-server
+      namespace: qux
+    - apiVersion: v1
+      kind: ConfigMap
+      name: server-conf
+      namespace: qux
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 3cbdb39..2a4f3db 100644
--- a/go.mod
+++ b/go.mod
@@ -108,6 +108,7 @@ require (
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/hashicorp/golang-lru v1.0.2 // indirect
 	github.com/huandu/xstrings v1.4.0 // indirect
 	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -119,7 +120,7 @@ require (
 	github.com/lib/pq v1.10.9 // indirect
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
-	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
diff --git a/go.sum b/go.sum
index 954884b..649073b 100644
--- a/go.sum
+++ b/go.sum
@@ -225,8 +225,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
-github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
+github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
 github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
@@ -281,9 +281,8 @@ github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
 github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
 github.com/mattbaird/jsonpatch v0.0.0-20200820163806-098863c1fc24 h1:uYuGXJBAi1umT+ZS4oQJUgKtfXCAYTR+n9zw1ViT0vA=
 github.com/mattbaird/jsonpatch v0.0.0-20200820163806-098863c1fc24/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
@@ -528,7 +527,6 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
diff --git a/manifests/crds/apps.clusternet.io_globalizations.yaml b/manifests/crds/apps.clusternet.io_globalizations.yaml
index 20c2e0d..b08cf78 100644
--- a/manifests/crds/apps.clusternet.io_globalizations.yaml
+++ b/manifests/crds/apps.clusternet.io_globalizations.yaml
@@ -135,6 +135,17 @@ spec:
                   description: OverrideConfig holds information that describes a override
                     config.
                   properties:
+                    fieldFormat:
+                      description: FieldFormat indicates the field format for the
+                        override value for FieldPatch.
+                      enum:
+                      - YAML
+                      - JSON
+                      type: string
+                    fieldPath:
+                      description: FieldPath indicates the field path for the override
+                        value for FieldPatch.
+                      type: string
                     name:
                       description: Name indicate the OverrideConfig name.
                       type: string
@@ -148,6 +159,8 @@ spec:
                       - Helm
                       - JSONPatch
                       - MergePatch
+                      - FieldJSONPatch
+                      - FieldMergePatch
                       type: string
                     value:
                       description: Value represents override value.
diff --git a/manifests/crds/apps.clusternet.io_localizations.yaml b/manifests/crds/apps.clusternet.io_localizations.yaml
index 9de37a5..883d5df 100644
--- a/manifests/crds/apps.clusternet.io_localizations.yaml
+++ b/manifests/crds/apps.clusternet.io_localizations.yaml
@@ -86,6 +86,17 @@ spec:
                   description: OverrideConfig holds information that describes a override
                     config.
                   properties:
+                    fieldFormat:
+                      description: FieldFormat indicates the field format for the
+                        override value for FieldPatch.
+                      enum:
+                      - YAML
+                      - JSON
+                      type: string
+                    fieldPath:
+                      description: FieldPath indicates the field path for the override
+                        value for FieldPatch.
+                      type: string
                     name:
                       description: Name indicate the OverrideConfig name.
                       type: string
@@ -99,6 +110,8 @@ spec:
                       - Helm
                       - JSONPatch
                       - MergePatch
+                      - FieldJSONPatch
+                      - FieldMergePatch
                       type: string
                     value:
                       description: Value represents override value.
diff --git a/pkg/apis/apps/v1alpha1/localization.go b/pkg/apis/apps/v1alpha1/localization.go
index f329691..a58a42c 100644
--- a/pkg/apis/apps/v1alpha1/localization.go
+++ b/pkg/apis/apps/v1alpha1/localization.go
@@ -95,6 +95,24 @@ const (
 
 	// StrategicMergePatchType won't be supported, since `patchStrategy`
 	// and `patchMergeKey` can not be retrieved.
+
+	// FieldJSONPatchType applies a field patch for all matched objects.
+	// Note: FieldJSONPatchType does not work with HelmChart(s).
+	FieldJSONPatchType OverrideType = "FieldJSONPatch"
+
+	// FieldMergePatchType applies a field merge patch for all matched objects.
+	// Note: FieldMergePatchType does not work with HelmChart(s).
+	FieldMergePatchType OverrideType = "FieldMergePatch"
+)
+
+type FieldFormatType string
+
+const (
+	// YAMLFormat is the default format for FieldPatchType.
+	YAMLFormat FieldFormatType = "YAML"
+
+	// JSONFormat is the format for FieldPatchType.
+	JSONFormat FieldFormatType = "JSON"
 )
 
 // OverrideConfig holds information that describes a override config.
@@ -116,13 +134,25 @@ type OverrideConfig struct {
 	// +required
 	// +kubebuilder:validation:Required
 	// +kubebuilder:validation:Type=string
-	// +kubebuilder:validation:Enum=Helm;JSONPatch;MergePatch
+	// +kubebuilder:validation:Enum=Helm;JSONPatch;MergePatch;FieldJSONPatch;FieldMergePatch
 	Type OverrideType `json:"type"`
 
 	// OverrideChart indicates whether the override value for the HelmChart CR.
 	//
 	// +optional
 	OverrideChart bool `json:"overrideChart,omitempty"`
+
+	// FieldPath indicates the field path for the override value for FieldPatch.
+	//
+	// +optional
+	FieldPath string `json:"fieldPath,omitempty"`
+
+	// FieldFormat indicates the field format for the override value for FieldPatch.
+	//
+	// +optional
+	// +kubebuilder:validation:Type=string
+	// +kubebuilder:validation:Enum=YAML;JSON
+	FieldFormat FieldFormatType `json:"fieldFormat,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/pkg/controllermanager/deployer/localizer/patch.go b/pkg/controllermanager/deployer/localizer/patch.go
index 7c55e45..a24a373 100644
--- a/pkg/controllermanager/deployer/localizer/patch.go
+++ b/pkg/controllermanager/deployer/localizer/patch.go
@@ -17,11 +17,14 @@ limitations under the License.
 package localizer
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
+	"strconv"
 	"strings"
 
 	jsonpatch "github.com/evanphx/json-patch"
+	jsoniter "github.com/json-iterator/go"
 	"helm.sh/helm/v3/pkg/chartutil"
 	"sigs.k8s.io/yaml"
 
@@ -32,6 +35,10 @@ import (
 const (
 	// maximum number of operations a single json patch may contain.
 	maxJSONPatchOperations = 10000
+	// nonIndent is the non-indented format for json patch.
+	nonIndent = ""
+	// jsonIndent is the indented format for json patch.
+	jsonIndent = "    "
 )
 
 // applyOverrides applies the overrides to the current resource.
@@ -77,7 +84,7 @@ func applyOverrides(genericOriginal []byte, chartOriginal []byte, overrides []ap
 				return nil, nil, fmt.Errorf("failed to apply OverrideConfig %s: %v", overrideConfig.Name, err)
 			}
 		case appsapi.JSONPatchType:
-			genericResult, err = applyJSONPatch(genericResult, overrideBytes)
+			genericResult, err = applyJSONPatch(genericResult, overrideBytes, nonIndent)
 			if err != nil {
 				return nil, nil, fmt.Errorf("failed to apply OverrideConfig %s: %v", overrideConfig.Name, err)
 			}
@@ -86,6 +93,18 @@ func applyOverrides(genericOriginal []byte, chartOriginal []byte, overrides []ap
 			if err != nil {
 				return nil, nil, fmt.Errorf("failed to apply OverrideConfig %s: %v", overrideConfig.Name, err)
 			}
+		case appsapi.FieldJSONPatchType:
+			genericResult, err = applyFieldJSONPatch(
+				genericResult, overrideConfig.FieldPath, overrideConfig.FieldFormat, overrideBytes)
+			if err != nil {
+				return nil, nil, fmt.Errorf("failed to apply OverrideConfig %s: %v", overrideConfig.Name, err)
+			}
+		case appsapi.FieldMergePatchType:
+			genericResult, err = applyFieldMergePatch(
+				genericResult, overrideConfig.FieldPath, overrideConfig.FieldFormat, overrideBytes)
+			if err != nil {
+				return nil, nil, fmt.Errorf("failed to apply OverrideConfig %s: %v", overrideConfig.Name, err)
+			}
 		default:
 			return nil, nil, fmt.Errorf("unsupported OverrideType %s", overrideConfig.Type)
 		}
@@ -94,7 +113,10 @@ func applyOverrides(genericOriginal []byte, chartOriginal []byte, overrides []ap
 	return genericResult, chartResult, nil
 }
 
-func applyJSONPatch(cur, overrideBytes []byte) ([]byte, error) {
+// indent is used to format the JSON patch result
+// for JSONPatchType, the indent is "" by default
+// for FieldPatchType, the indent is "    " for pretty printing
+func applyJSONPatch(cur, overrideBytes []byte, indent string) ([]byte, error) {
 	if strings.TrimSpace(string(cur)) == "" {
 		return cur, nil
 	}
@@ -106,7 +128,7 @@ func applyJSONPatch(cur, overrideBytes []byte) ([]byte, error) {
 		return nil, fmt.Errorf("the allowed maximum operations in a JSON patch is %d, got %d",
 			maxJSONPatchOperations, len(patchObj))
 	}
-	patchedJS, err := patchObj.Apply(cur)
+	patchedJS, err := patchObj.ApplyIndent(cur, indent)
 	if err == nil {
 		return patchedJS, nil
 	}
@@ -128,3 +150,145 @@ func applyHelmValuesOverride(currentByte, overrideByte []byte) ([]byte, error) {
 	}
 	return utils.Marshal(chartutil.CoalesceTables(overrideValues, currentObj))
 }
+
+func applyFieldJSONPatch(cur []byte, fieldPath string, fieldFormat appsapi.FieldFormatType, overrideBytes []byte) (
+	[]byte, error) {
+
+	fieldValue, err := findField(cur, fieldPath)
+	if err != nil {
+		return nil, err
+	}
+	if len(fieldValue) == 0 {
+		return cur, nil
+	}
+
+	var result []byte
+	switch fieldFormat {
+	case appsapi.JSONFormat:
+		result, err = applyJSONPatch([]byte(fieldValue), overrideBytes, jsonIndent)
+		if err != nil {
+			return nil, err
+		}
+	case appsapi.YAMLFormat:
+		var fieldValueJSON []byte
+		var resultJSON []byte
+		fieldValueJSON, err = yaml.YAMLToJSON([]byte(fieldValue))
+		if err != nil {
+			return nil, fmt.Errorf("failed to convert patch %s to JSON: %v", cur, err)
+		}
+		resultJSON, err = applyJSONPatch(fieldValueJSON, overrideBytes, jsonIndent)
+		if err != nil {
+			return nil, err
+		}
+		result, err = yaml.JSONToYAML(resultJSON)
+		if err != nil {
+			return nil, fmt.Errorf("failed to convert json %s to yaml, error: %v", string(resultJSON), err)
+		}
+	default:
+		return nil, fmt.Errorf("unsupported FieldFormatType %s", fieldFormat)
+	}
+	patches := []utils.JsonPatchOption{
+		{
+			Op:    "replace",
+			Path:  fieldPath,
+			Value: string(result),
+		},
+	}
+	patchesBytes, _ := jsoniter.MarshalToString(patches)
+	return applyJSONPatch(
+		cur, []byte(patchesBytes), "")
+}
+
+func applyFieldMergePatch(cur []byte, fieldPath string, fieldFormat appsapi.FieldFormatType, overrideBytes []byte) (
+	[]byte, error) {
+
+	fieldValue, err := findField(cur, fieldPath)
+	if err != nil {
+		return nil, err
+	}
+	if len(fieldValue) == 0 {
+		return cur, nil
+	}
+
+	var result []byte
+	switch fieldFormat {
+	case appsapi.JSONFormat:
+		result, err = jsonpatch.MergePatch([]byte(fieldValue), overrideBytes)
+		if err != nil {
+			return nil, err
+		}
+		var tmpObj map[string]interface{}
+		if err = json.Unmarshal(result, &tmpObj); err != nil {
+			return nil, fmt.Errorf("failed to unmarshal %s to json, error: %v", string(result), err)
+		}
+		result, err = json.MarshalIndent(tmpObj, nonIndent, jsonIndent)
+		if err != nil {
+			return nil, fmt.Errorf("failed to marshal %v to json, error: %v", tmpObj, err)
+		}
+	case appsapi.YAMLFormat:
+		var fieldValueJSON []byte
+		var resultJSON []byte
+		fieldValueJSON, err = yaml.YAMLToJSON([]byte(fieldValue))
+		if err != nil {
+			return nil, fmt.Errorf("failed to convert patch %s to JSON: %v", cur, err)
+		}
+		resultJSON, err = jsonpatch.MergePatch(fieldValueJSON, overrideBytes)
+		if err != nil {
+			return nil, err
+		}
+		result, err = yaml.JSONToYAML(resultJSON)
+		if err != nil {
+			return nil, fmt.Errorf("failed to convert json %s to yaml, error: %v", string(resultJSON), err)
+		}
+	default:
+		return nil, fmt.Errorf("unsupported FieldFormatType %s", fieldFormat)
+	}
+	patches := []utils.JsonPatchOption{
+		{
+			Op:    "replace",
+			Path:  fieldPath,
+			Value: string(result),
+		},
+	}
+	patchesBytes, _ := jsoniter.MarshalToString(patches)
+	return applyJSONPatch(
+		cur, []byte(patchesBytes), "")
+}
+
+// findField finds a field in a json
+// fieldPath format: /path/to/parts/0/field
+func findField(cur []byte, fieldPath string) (string, error) {
+	obj := map[string]interface{}{}
+	if err := jsoniter.Unmarshal(cur, &obj); err != nil {
+		return "", fmt.Errorf("failed to unmarshal %s to json, error: %v", cur, err)
+	}
+	any := jsoniter.Wrap(obj)
+
+	split := strings.Split(fieldPath, "/")
+	if len(split) < 2 {
+		return "", fmt.Errorf("invalid fieldPath %s, should start with /", fieldPath)
+	}
+	parts := split[1:]
+	for _, part := range parts {
+		var curPath interface{}
+		switch any.ValueType() {
+		case jsoniter.InvalidValue, jsoniter.NilValue, jsoniter.NumberValue, jsoniter.BoolValue:
+			return "", fmt.Errorf("object is not a string or array, cannot index by field %s", part)
+		case jsoniter.ArrayValue:
+			num, err := strconv.Atoi(part)
+			if err != nil {
+				return "", fmt.Errorf("object is a array, but convert field %s to number, error: %v", part, err)
+			}
+			curPath = num
+		case jsoniter.ObjectValue:
+			curPath = part
+		default:
+			return "", fmt.Errorf("invalid valueType %v, cannot find field %s", any.ValueType(), part)
+		}
+		any = any.Get(curPath)
+	}
+	if any.ValueType() != jsoniter.StringValue {
+		return "", fmt.Errorf("field value index by path %s is not a string, but %v", fieldPath, any.ValueType())
+	}
+	return any.ToString(), nil
+}
diff --git a/pkg/generated/applyconfiguration/apps/v1alpha1/overrideconfig.go b/pkg/generated/applyconfiguration/apps/v1alpha1/overrideconfig.go
index 45ad18e..f854136 100644
--- a/pkg/generated/applyconfiguration/apps/v1alpha1/overrideconfig.go
+++ b/pkg/generated/applyconfiguration/apps/v1alpha1/overrideconfig.go
@@ -24,10 +24,12 @@ import (
 // OverrideConfigApplyConfiguration represents a declarative configuration of the OverrideConfig type for use
 // with apply.
 type OverrideConfigApplyConfiguration struct {
-	Name          *string                    `json:"name,omitempty"`
-	Value         *string                    `json:"value,omitempty"`
-	Type          *appsv1alpha1.OverrideType `json:"type,omitempty"`
-	OverrideChart *bool                      `json:"overrideChart,omitempty"`
+	Name          *string                       `json:"name,omitempty"`
+	Value         *string                       `json:"value,omitempty"`
+	Type          *appsv1alpha1.OverrideType    `json:"type,omitempty"`
+	OverrideChart *bool                         `json:"overrideChart,omitempty"`
+	FieldPath     *string                       `json:"fieldPath,omitempty"`
+	FieldFormat   *appsv1alpha1.FieldFormatType `json:"fieldFormat,omitempty"`
 }
 
 // OverrideConfigApplyConfiguration constructs a declarative configuration of the OverrideConfig type for use with
@@ -67,3 +69,19 @@ func (b *OverrideConfigApplyConfiguration) WithOverrideChart(value bool) *Overri
 	b.OverrideChart = &value
 	return b
 }
+
+// WithFieldPath sets the FieldPath field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the FieldPath field is set to the value of the last call.
+func (b *OverrideConfigApplyConfiguration) WithFieldPath(value string) *OverrideConfigApplyConfiguration {
+	b.FieldPath = &value
+	return b
+}
+
+// WithFieldFormat sets the FieldFormat field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the FieldFormat field is set to the value of the last call.
+func (b *OverrideConfigApplyConfiguration) WithFieldFormat(value appsv1alpha1.FieldFormatType) *OverrideConfigApplyConfiguration {
+	b.FieldFormat = &value
+	return b
+}


Test patch:
diff --git a/pkg/controllermanager/deployer/localizer/patch_test.go b/pkg/controllermanager/deployer/localizer/patch_test.go
index a3f212c..276b13a 100644
--- a/pkg/controllermanager/deployer/localizer/patch_test.go
+++ b/pkg/controllermanager/deployer/localizer/patch_test.go
@@ -424,6 +424,84 @@ hole: black
 				}
 			}`),
 		},
+		{
+			name: "FieldPatch",
+			original: []byte(`{
+				"apiVersion": "v1",
+				"kind": "ConfigMap",
+				"metadata": {
+					"name": "example-configmap",
+					"namespace": "default"
+				},
+				"data": {
+					"server.conf": "{\n    \"db_address\": \"127.0.0.1:3306\",\n    \"region_id\": \"us-west-1\",\n    \"zone_id\": \"zone-1\"\n}"
+				}
+			}`),
+			overrides: []appsapi.OverrideConfig{
+				{
+					Name:  "empty override with whitespaces",
+					Type:  appsapi.MergePatchType,
+					Value: `   `,
+				},
+				{
+					Name:        "overridge db address - json format",
+					Type:        appsapi.FieldJSONPatchType,
+					FieldPath:   "/data/server.conf",
+					FieldFormat: appsapi.JSONFormat,
+					Value:       `[{"op":"replace","path": "/db_address","value":"localhost:3306"}]`,
+				},
+			},
+			want: []byte(`{
+				"apiVersion": "v1",
+				"kind": "ConfigMap",
+				"metadata": {
+					"name": "example-configmap",
+					"namespace": "default"
+				},
+				"data": {
+					"server.conf": "{\n    \"db_address\": \"localhost:3306\",\n    \"region_id\": \"us-west-1\",\n    \"zone_id\": \"zone-1\"\n}"
+				}
+			}`),
+		},
+		{
+			name: "FieldPatch",
+			original: []byte(`{
+				"apiVersion": "v1",
+				"kind": "ConfigMap",
+				"metadata": {
+					"name": "example-configmap",
+					"namespace": "default"
+				},
+				"data": {
+					"server.conf": "db_address: 127.0.0.1:5432\nregion_id: us-west-1\nzone_id: zone-1\n"
+				}
+			}`),
+			overrides: []appsapi.OverrideConfig{
+				{
+					Name:  "empty override with whitespaces",
+					Type:  appsapi.MergePatchType,
+					Value: `   `,
+				},
+				{
+					Name:        "overridge db address - json format",
+					Type:        appsapi.FieldMergePatchType,
+					FieldPath:   "/data/server.conf",
+					FieldFormat: appsapi.YAMLFormat,
+					Value:       "region_id: us-west-2\nzone_id: zone-2\n",
+				},
+			},
+			want: []byte(`{
+				"apiVersion": "v1",
+				"kind": "ConfigMap",
+				"metadata": {
+					"name": "example-configmap",
+					"namespace": "default"
+				},
+				"data": {
+					"server.conf": "db_address: 127.0.0.1:5432\nregion_id: us-west-2\nzone_id: zone-2\n"
+				}
+			}`),
+		},
 	}
 
 	for _, tt := range tests {
@@ -494,7 +572,7 @@ func TestApplyJSONPatch(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := applyJSONPatch(tt.cur, tt.overrideBytes)
+			got, err := applyJSONPatch(tt.cur, tt.overrideBytes, "")
 			if (err != nil) != tt.wantErr {
 				t.Errorf("applyJSONPatch() error = %v, wantErr %v", err, tt.wantErr)
 				return
@@ -505,3 +583,162 @@ func TestApplyJSONPatch(t *testing.T) {
 		})
 	}
 }
+
+func TestApplyFieldPatch(t *testing.T) {
+	tests := []struct {
+		name           string
+		cur            []byte
+		overrideBytes  []byte
+		fieldPatchType appsapi.OverrideType
+		fieldPath      string
+		fieldFormat    appsapi.FieldFormatType
+		want           []byte
+		wantErr        bool
+	}{
+		{
+			name:           "json patch add mysql timeout to configmap in json",
+			cur:            []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"{\"mysql\":{\"address\":\"127.0.0.1:3306\"}}"}`),
+			overrideBytes:  []byte(`[{"path":"/mysql/timeout","op":"add","value": 5}]`),
+			fieldPatchType: appsapi.FieldJSONPatchType,
+			fieldPath:      "/mydata",
+			fieldFormat:    appsapi.JSONFormat,
+			want:           []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"{\n    \"mysql\": {\n        \"address\": \"127.0.0.1:3306\",\n        \"timeout\": 5\n    }\n}"}`),
+			wantErr:        false,
+		},
+		{
+			name:           "merge patch mysql timeout to configmap in json",
+			cur:            []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"{\"mysql\":{\"address\":\"127.0.0.1:3306\"}}"}`),
+			overrideBytes:  []byte(`{"mysql":{"timeout":5}}`),
+			fieldPatchType: appsapi.FieldMergePatchType,
+			fieldPath:      "/mydata",
+			fieldFormat:    appsapi.JSONFormat,
+			want:           []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"{\n    \"mysql\": {\n        \"address\": \"127.0.0.1:3306\",\n        \"timeout\": 5\n    }\n}"}`),
+			wantErr:        false,
+		},
+		{
+			name:           "merge patch add mysql timeout to configmap in yaml",
+			cur:            []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"mysql:\n  address: 127.0.0.1:3306\n"}`),
+			overrideBytes:  []byte(`{"mysql":{"timeout":5}}`),
+			fieldPatchType: appsapi.FieldMergePatchType,
+			fieldPath:      "/mydata",
+			fieldFormat:    appsapi.YAMLFormat,
+			want:           []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"mysql:\n  address: 127.0.0.1:3306\n  timeout: 5\n"}`),
+			wantErr:        false,
+		},
+		{
+			name:           "json patch add mysql timeout to configmap in yaml",
+			cur:            []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"mysql:\n  address: 127.0.0.1:3306\n"}`),
+			overrideBytes:  []byte(`[{"path":"/mysql/timeout","op":"add","value": 5}]`),
+			fieldPatchType: appsapi.FieldJSONPatchType,
+			fieldPath:      "/mydata",
+			fieldFormat:    appsapi.YAMLFormat,
+			want:           []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"mysql:\n  address: 127.0.0.1:3306\n  timeout: 5\n"}`),
+			wantErr:        false,
+		},
+		{
+			name:           "add mysql timeout to configmap in yaml when field not exist",
+			cur:            []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"mysql:\n  address: 127.0.0.1:3306\n"}`),
+			overrideBytes:  []byte(`{"mysql":{"timeout":5}}`),
+			fieldPatchType: appsapi.FieldMergePatchType,
+			fieldPath:      "/field-not-exist",
+			fieldFormat:    appsapi.YAMLFormat,
+			want:           nil,
+			wantErr:        true,
+		},
+		{
+			name:           "invalid yaml field value",
+			cur:            []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"mysql:  address: 127.0.0.1:3306\n"}`),
+			overrideBytes:  []byte(`{"mysql":{"timeout":5}}`),
+			fieldPatchType: appsapi.FieldMergePatchType,
+			fieldPath:      "/mydata",
+			fieldFormat:    appsapi.YAMLFormat,
+			want:           nil,
+			wantErr:        true,
+		},
+		{
+			name:           "invalid json field value",
+			cur:            []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"mydata":"{\"mysql\"\"\"\":{\"address\":\"127.0.0.1:3306\"}}"}`),
+			overrideBytes:  []byte(`{"mysql":{"timeout":5}}`),
+			fieldPatchType: appsapi.FieldMergePatchType,
+			fieldPath:      "/mydata",
+			fieldFormat:    appsapi.JSONFormat,
+			want:           nil,
+			wantErr:        true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var got []byte
+			var err error
+			if tt.fieldPatchType == appsapi.FieldJSONPatchType {
+				got, err = applyFieldJSONPatch(tt.cur, tt.fieldPath, tt.fieldFormat, tt.overrideBytes)
+			}
+			if tt.fieldPatchType == appsapi.FieldMergePatchType {
+				got, err = applyFieldMergePatch(tt.cur, tt.fieldPath, tt.fieldFormat, tt.overrideBytes)
+			}
+
+			if (err != nil) != tt.wantErr {
+				t.Errorf("applyFieldPatch() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("applyFieldPatch() got = %s, want %s", string(got), string(tt.want))
+			}
+		})
+	}
+}
+
+func TestFindField(t *testing.T) {
+	testCases := []struct {
+		name      string
+		cur       []byte
+		fieldPath string
+		result    string
+		wantErr   bool
+	}{
+		{
+			name:      "find metadata.labels",
+			cur:       []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"spec":{"version":"1.8.0"}}`),
+			fieldPath: "/metadata/labels",
+			result:    "",
+			wantErr:   true,
+		},
+		{
+			name:      "find metadata.labels.another-label",
+			cur:       []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"spec":{"version":"1.8.0"}}`),
+			fieldPath: "/metadata/labels/another-label",
+			result:    "another-value",
+		},
+		{
+			name:      "find some field in string array",
+			cur:       []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"spec":{"commands":["sleep", "999999"],"version":"1.8.0"}}`),
+			fieldPath: "/spec/commands/1",
+			result:    "999999",
+		},
+		{
+			name:      "find some field in object array",
+			cur:       []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"spec":{"containers":[{"image":"nginx:latest"}, {"image":"redis:latest"}],"version":"1.8.0"}}`),
+			fieldPath: "/spec/containers/1/image",
+			result:    "redis:latest",
+		},
+		{
+			name:      "error field path",
+			cur:       []byte(`{"metadata":{"labels":{"another-label":"another-value","some-label":"some-value"}},"spec":{"containers":[{"image":"nginx:latest"}, {"image":"redis:latest"}],"version":"1.8.0"}}`),
+			fieldPath: "/spec/containers/error/image",
+			result:    "",
+			wantErr:   true,
+		},
+	}
+	for _, tt := range testCases {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := findField(tt.cur, tt.fieldPath)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("findField() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.result {
+				t.Errorf("findField() got = %v, want %v", got, tt.result)
+			}
+		})
+	}
+}