Browse Source

feat: introduce secret rewrite merge operation (#4894)

* initial implementation of rewrite merge

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* introduce stable secrets merging order to ensure predictable key overwrites

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* dataFrom.rewrite.merge: implement key priority

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* introduce conflictPolicy & strategy, refactor

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* polish code, improve tests, add combined rewrite tests

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* add documentation

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* make Error default conflict policy for ExternalSecretRewriteMerge operations

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* update CRDs after setting Error as default conflictPolicy for rewrite merge

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* reduce cognitive complexity of RewriteMap method

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* improve error messages by adding failed rewrite operation type

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* use maps.Copy() in RewriteMerge to propagate input map

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

* introduce check if key in priority list exists in input

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>

---------

Signed-off-by: Riccardo M. Cefala <riccardo.c@miro.com>
Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Riccardo M. Cefala 9 months ago
parent
commit
d6d5ce9623

+ 42 - 0
apis/externalsecrets/v1/externalsecret_types.go

@@ -305,6 +305,12 @@ type ExternalSecretDataFromRemoteRef struct {
 }
 }
 
 
 type ExternalSecretRewrite struct {
 type ExternalSecretRewrite struct {
+
+	// Used to merge key/values in one single Secret
+	// The resulting key will contain all values from the specified secrets
+	// +optional
+	Merge *ExternalSecretRewriteMerge `json:"merge,omitempty"`
+
 	// Used to rewrite with regular expressions.
 	// Used to rewrite with regular expressions.
 	// The resulting key will be the output of a regexp.ReplaceAll operation.
 	// The resulting key will be the output of a regexp.ReplaceAll operation.
 	// +optional
 	// +optional
@@ -316,6 +322,42 @@ type ExternalSecretRewrite struct {
 	Transform *ExternalSecretRewriteTransform `json:"transform,omitempty"`
 	Transform *ExternalSecretRewriteTransform `json:"transform,omitempty"`
 }
 }
 
 
+type ExternalSecretRewriteMerge struct {
+	// Used to define the target key of the merge operation.
+	// Required if strategy is JSON. Ignored otherwise.
+	// +optional
+	// +kubebuilder:default=""
+	Into string `json:"into,omitempty"`
+
+	// Used to define key priority in conflict resolution.
+	// +optional
+	Priority []string `json:"priority,omitempty"`
+
+	// Used to define the policy to use in conflict resolution.
+	// +optional
+	// +kubebuilder:default="Error"
+	ConflictPolicy ExternalSecretRewriteMergeConflictPolicy `json:"conflictPolicy,omitempty"`
+
+	// Used to define the strategy to use in the merge operation.
+	// +optional
+	// +kubebuilder:default="Extract"
+	Strategy ExternalSecretRewriteMergeStrategy `json:"strategy,omitempty"`
+}
+
+type ExternalSecretRewriteMergeConflictPolicy string
+
+const (
+	ExternalSecretRewriteMergeConflictPolicyIgnore ExternalSecretRewriteMergeConflictPolicy = "Ignore"
+	ExternalSecretRewriteMergeConflictPolicyError  ExternalSecretRewriteMergeConflictPolicy = "Error"
+)
+
+type ExternalSecretRewriteMergeStrategy string
+
+const (
+	ExternalSecretRewriteMergeStrategyExtract ExternalSecretRewriteMergeStrategy = "Extract"
+	ExternalSecretRewriteMergeStrategyJSON    ExternalSecretRewriteMergeStrategy = "JSON"
+)
+
 type ExternalSecretRewriteRegexp struct {
 type ExternalSecretRewriteRegexp struct {
 	// Used to define the regular expression of a re.Compiler.
 	// Used to define the regular expression of a re.Compiler.
 	Source string `json:"source"`
 	Source string `json:"source"`

+ 25 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -1388,6 +1388,11 @@ func (in *ExternalSecretMetadata) DeepCopy() *ExternalSecretMetadata {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalSecretRewrite) DeepCopyInto(out *ExternalSecretRewrite) {
 func (in *ExternalSecretRewrite) DeepCopyInto(out *ExternalSecretRewrite) {
 	*out = *in
 	*out = *in
+	if in.Merge != nil {
+		in, out := &in.Merge, &out.Merge
+		*out = new(ExternalSecretRewriteMerge)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Regexp != nil {
 	if in.Regexp != nil {
 		in, out := &in.Regexp, &out.Regexp
 		in, out := &in.Regexp, &out.Regexp
 		*out = new(ExternalSecretRewriteRegexp)
 		*out = new(ExternalSecretRewriteRegexp)
@@ -1411,6 +1416,26 @@ func (in *ExternalSecretRewrite) DeepCopy() *ExternalSecretRewrite {
 }
 }
 
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalSecretRewriteMerge) DeepCopyInto(out *ExternalSecretRewriteMerge) {
+	*out = *in
+	if in.Priority != nil {
+		in, out := &in.Priority, &out.Priority
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretRewriteMerge.
+func (in *ExternalSecretRewriteMerge) DeepCopy() *ExternalSecretRewriteMerge {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalSecretRewriteMerge)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalSecretRewriteRegexp) DeepCopyInto(out *ExternalSecretRewriteRegexp) {
 func (in *ExternalSecretRewriteRegexp) DeepCopyInto(out *ExternalSecretRewriteRegexp) {
 	*out = *in
 	*out = *in
 }
 }

+ 28 - 0
config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml

@@ -302,6 +302,34 @@ spec:
                             Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
                             Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
                           items:
                           items:
                             properties:
                             properties:
+                              merge:
+                                description: |-
+                                  Used to merge key/values in one single Secret
+                                  The resulting key will contain all values from the specified secrets
+                                properties:
+                                  conflictPolicy:
+                                    default: Error
+                                    description: Used to define the policy to use
+                                      in conflict resolution.
+                                    type: string
+                                  into:
+                                    default: ""
+                                    description: |-
+                                      Used to define the target key of the merge operation.
+                                      Required if strategy is JSON. Ignored otherwise.
+                                    type: string
+                                  priority:
+                                    description: Used to define key priority in conflict
+                                      resolution.
+                                    items:
+                                      type: string
+                                    type: array
+                                  strategy:
+                                    default: Extract
+                                    description: Used to define the strategy to use
+                                      in the merge operation.
+                                    type: string
+                                type: object
                               regexp:
                               regexp:
                                 description: |-
                                 description: |-
                                   Used to rewrite with regular expressions.
                                   Used to rewrite with regular expressions.

+ 28 - 0
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -282,6 +282,34 @@ spec:
                         Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
                         Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
                       items:
                       items:
                         properties:
                         properties:
+                          merge:
+                            description: |-
+                              Used to merge key/values in one single Secret
+                              The resulting key will contain all values from the specified secrets
+                            properties:
+                              conflictPolicy:
+                                default: Error
+                                description: Used to define the policy to use in conflict
+                                  resolution.
+                                type: string
+                              into:
+                                default: ""
+                                description: |-
+                                  Used to define the target key of the merge operation.
+                                  Required if strategy is JSON. Ignored otherwise.
+                                type: string
+                              priority:
+                                description: Used to define key priority in conflict
+                                  resolution.
+                                items:
+                                  type: string
+                                type: array
+                              strategy:
+                                default: Extract
+                                description: Used to define the strategy to use in
+                                  the merge operation.
+                                type: string
+                            type: object
                           regexp:
                           regexp:
                             description: |-
                             description: |-
                               Used to rewrite with regular expressions.
                               Used to rewrite with regular expressions.

+ 50 - 0
deploy/crds/bundle.yaml

@@ -287,6 +287,31 @@ spec:
                               Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
                               Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
                             items:
                             items:
                               properties:
                               properties:
+                                merge:
+                                  description: |-
+                                    Used to merge key/values in one single Secret
+                                    The resulting key will contain all values from the specified secrets
+                                  properties:
+                                    conflictPolicy:
+                                      default: Error
+                                      description: Used to define the policy to use in conflict resolution.
+                                      type: string
+                                    into:
+                                      default: ""
+                                      description: |-
+                                        Used to define the target key of the merge operation.
+                                        Required if strategy is JSON. Ignored otherwise.
+                                      type: string
+                                    priority:
+                                      description: Used to define key priority in conflict resolution.
+                                      items:
+                                        type: string
+                                      type: array
+                                    strategy:
+                                      default: Extract
+                                      description: Used to define the strategy to use in the merge operation.
+                                      type: string
+                                  type: object
                                 regexp:
                                 regexp:
                                   description: |-
                                   description: |-
                                     Used to rewrite with regular expressions.
                                     Used to rewrite with regular expressions.
@@ -10605,6 +10630,31 @@ spec:
                           Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
                           Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
                         items:
                         items:
                           properties:
                           properties:
+                            merge:
+                              description: |-
+                                Used to merge key/values in one single Secret
+                                The resulting key will contain all values from the specified secrets
+                              properties:
+                                conflictPolicy:
+                                  default: Error
+                                  description: Used to define the policy to use in conflict resolution.
+                                  type: string
+                                into:
+                                  default: ""
+                                  description: |-
+                                    Used to define the target key of the merge operation.
+                                    Required if strategy is JSON. Ignored otherwise.
+                                  type: string
+                                priority:
+                                  description: Used to define key priority in conflict resolution.
+                                  items:
+                                    type: string
+                                  type: array
+                                strategy:
+                                  default: Extract
+                                  description: Used to define the strategy to use in the merge operation.
+                                  type: string
+                              type: object
                             regexp:
                             regexp:
                               description: |-
                               description: |-
                                 Used to rewrite with regular expressions.
                                 Used to rewrite with regular expressions.

+ 128 - 0
docs/api/spec.md

@@ -3813,6 +3813,21 @@ map[string]string
 <tbody>
 <tbody>
 <tr>
 <tr>
 <td>
 <td>
+<code>merge</code></br>
+<em>
+<a href="#external-secrets.io/v1.ExternalSecretRewriteMerge">
+ExternalSecretRewriteMerge
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to merge key/values in one single Secret
+The resulting key will contain all values from the specified secrets</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>regexp</code></br>
 <code>regexp</code></br>
 <em>
 <em>
 <a href="#external-secrets.io/v1.ExternalSecretRewriteRegexp">
 <a href="#external-secrets.io/v1.ExternalSecretRewriteRegexp">
@@ -3843,6 +3858,119 @@ The resulting key will be the output of the template applied by the operation.</
 </tr>
 </tr>
 </tbody>
 </tbody>
 </table>
 </table>
+<h3 id="external-secrets.io/v1.ExternalSecretRewriteMerge">ExternalSecretRewriteMerge
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.ExternalSecretRewrite">ExternalSecretRewrite</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>into</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to define the target key of the merge operation.
+Required if strategy is JSON. Ignored otherwise.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>priority</code></br>
+<em>
+[]string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to define key priority in conflict resolution.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>conflictPolicy</code></br>
+<em>
+<a href="#external-secrets.io/v1.ExternalSecretRewriteMergeConflictPolicy">
+ExternalSecretRewriteMergeConflictPolicy
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to define the policy to use in conflict resolution.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>strategy</code></br>
+<em>
+<a href="#external-secrets.io/v1.ExternalSecretRewriteMergeStrategy">
+ExternalSecretRewriteMergeStrategy
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to define the strategy to use in the merge operation.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.ExternalSecretRewriteMergeConflictPolicy">ExternalSecretRewriteMergeConflictPolicy
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.ExternalSecretRewriteMerge">ExternalSecretRewriteMerge</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;Error&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;Ignore&#34;</p></td>
+<td></td>
+</tr></tbody>
+</table>
+<h3 id="external-secrets.io/v1.ExternalSecretRewriteMergeStrategy">ExternalSecretRewriteMergeStrategy
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.ExternalSecretRewriteMerge">ExternalSecretRewriteMerge</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;Extract&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;JSON&#34;</p></td>
+<td></td>
+</tr></tbody>
+</table>
 <h3 id="external-secrets.io/v1.ExternalSecretRewriteRegexp">ExternalSecretRewriteRegexp
 <h3 id="external-secrets.io/v1.ExternalSecretRewriteRegexp">ExternalSecretRewriteRegexp
 </h3>
 </h3>
 <p>
 <p>

+ 47 - 2
docs/guides/datafrom-rewrite.md

@@ -11,11 +11,23 @@ Rewrite operations are all applied before `ConversionStrategy` is applied.
 ### Regexp
 ### Regexp
 This method implements rewriting through the use of regular expressions. It needs a `source` and a `target` field. The source field is where the definition of the matching regular expression goes, where the `target` field is where the replacing expression goes.
 This method implements rewriting through the use of regular expressions. It needs a `source` and a `target` field. The source field is where the definition of the matching regular expression goes, where the `target` field is where the replacing expression goes.
 
 
-Some considerations about the implementation of Regexp Rewrite:
+### Merge
+This method implements rewriting keys by merging operation and solving key collisions. It supports two merging strategies: `Extract` and `JSON`.
+
+The `Extract` strategy interprets all secret values in the secret map as JSON and merges all contained key/value pairs hoisting them to the top level, substituting the original secret map.
+
+The `JSON` strategy interprets all secret values in the secret map as JSON and merges all contained key/value pairs in the key specified by the _required_ parameter `into`. If the key specified by `into` already exists in the original secrets map it will be overwritten.
+
+Key collisions can be ignored or cause an error according to `conflictPolicy` which can be either `Ignore` or `Error`.  
+
+To guarantee deterministic results of the merge operation, secret keys are processed in alphabetical order. Key priority can also be made explicit by providing a list of secret keys in the `priority` parameter. These keys will be processed last in the order they appear while all other keys will still be processed in alphabetical order.
+
+## Considerations about Rewrite implementation
 
 
 1. The input of a subsequent rewrite operation are the outputs of the previous rewrite.
 1. The input of a subsequent rewrite operation are the outputs of the previous rewrite.
 2. If a given set of keys do not match any Rewrite operation, there will be no error. Rather, the original keys will be used.
 2. If a given set of keys do not match any Rewrite operation, there will be no error. Rather, the original keys will be used.
-3. If a `source` is not a compilable `regexp` expression, an error will be produced and the external secret goes into a error state.
+3. In Regexp operations, if a `source` is not a compilable `regexp` expression, an error will be produced and the external secret will go into a error state.
+4. In Merge operations, if secrets are not valid JSON, an error will be produced and the external secret will go into an error state.
 
 
 ## Examples
 ## Examples
 ### Removing a common path from find operations
 ### Removing a common path from find operations
@@ -93,6 +105,39 @@ data:
     foo_baz: MjIyMg== #2222
     foo_baz: MjIyMg== #2222
 ```
 ```
 
 
+### Merging all secrets 
+
+The following ExternalSecret:
+```yaml
+{% include 'datafrom-rewrite-merge-empty.yaml' %}
+
+```
+Will merge all keys found in all secrets at top level.
+In this example, if we had the following secrets available in the provider:
+```json
+{
+    "path/to/secrets/object-storage-credentials": {
+        "ACCESS_KEY": "XXXX",
+        "SECRET_KEY": "YYYY"
+    },
+    "path/to/secrets/mongo-credentials": {
+        "USERNAME": "XXXX",
+        "PASSWORD": "YYYY"
+    }
+}
+```
+the output kubernetes secret would be:
+```yaml
+apiVersion: v1
+kind: Secret
+type: Opaque
+data:
+    ACCESS_KEY: WFhYWA== #XXXX
+    SECRET_KEY: WVlZWQ== #YYYY
+    USERNAME: WFhYWA== #XXXX
+    PASSWORD:  WVlZWQ== #YYYY
+```
+
 ## Limitations
 ## Limitations
 
 
 Regexp Rewrite is based on golang `regexp`, which in turns implements `RE2` regexp language. There a a series of known limitations to this implementation, such as:
 Regexp Rewrite is based on golang `regexp`, which in turns implements `RE2` regexp language. There a a series of known limitations to this implementation, such as:

+ 15 - 0
docs/snippets/datafrom-rewrite-merge-empty.yaml

@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: merge-basic-example
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: vault-backend
+    kind: SecretStore
+  dataFrom:
+    - find:
+        path: path/to/secrets
+        regexp: ".*-credentials"
+      rewrite:
+        - merge: {}

+ 102 - 10
pkg/utils/utils.go

@@ -24,10 +24,13 @@ import (
 	"encoding/pem"
 	"encoding/pem"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"maps"
 	"net"
 	"net"
 	"net/url"
 	"net/url"
 	"reflect"
 	"reflect"
 	"regexp"
 	"regexp"
+	"slices"
+	"sort"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	tpl "text/template"
 	tpl "text/template"
@@ -80,28 +83,117 @@ func RewriteMap(operations []esv1.ExternalSecretRewrite, in map[string][]byte) (
 	out := in
 	out := in
 	var err error
 	var err error
 	for i, op := range operations {
 	for i, op := range operations {
-		if op.Regexp != nil {
-			out, err = RewriteRegexp(*op.Regexp, out)
-			if err != nil {
-				return nil, fmt.Errorf("failed rewriting regexp operation[%v]: %w", i, err)
-			}
+		out, err = handleRewriteOperation(op, out)
+		if err != nil {
+			return nil, fmt.Errorf("failed rewrite operation[%v]: %w", i, err)
 		}
 		}
-		if op.Transform != nil {
-			out, err = RewriteTransform(*op.Transform, out)
+	}
+	return out, nil
+}
+
+func handleRewriteOperation(op esv1.ExternalSecretRewrite, in map[string][]byte) (map[string][]byte, error) {
+	switch {
+	case op.Merge != nil:
+		return RewriteMerge(*op.Merge, in)
+	case op.Regexp != nil:
+		return RewriteRegexp(*op.Regexp, in)
+	case op.Transform != nil:
+		return RewriteTransform(*op.Transform, in)
+	default:
+		return in, nil
+	}
+}
+
+// RewriteMerge merges input values according to the operation's strategy and conflict policy.
+func RewriteMerge(operation esv1.ExternalSecretRewriteMerge, in map[string][]byte) (map[string][]byte, error) {
+	var out map[string][]byte
+
+	mergedMap, conflicts, err := merge(operation, in)
+	if err != nil {
+		return nil, err
+	}
+
+	if operation.ConflictPolicy != esv1.ExternalSecretRewriteMergeConflictPolicyIgnore {
+		if len(conflicts) > 0 {
+			return nil, fmt.Errorf("merge failed with conflicts: %v", strings.Join(conflicts, ", "))
+		}
+	}
+
+	switch operation.Strategy {
+	case esv1.ExternalSecretRewriteMergeStrategyExtract, "":
+		out = make(map[string][]byte)
+		for k, v := range mergedMap {
+			byteValue, err := GetByteValue(v)
 			if err != nil {
 			if err != nil {
-				return nil, fmt.Errorf("failed rewriting transform operation[%v]: %w", i, err)
+				return nil, fmt.Errorf("merge failed with failed to convert value to []byte: %w", err)
 			}
 			}
+			out[k] = byteValue
+		}
+	case esv1.ExternalSecretRewriteMergeStrategyJSON:
+		out = make(map[string][]byte)
+		if operation.Into == "" {
+			return nil, fmt.Errorf("merge failed with missing 'into' field")
 		}
 		}
+		mergedBytes, err := JSONMarshal(mergedMap)
+		if err != nil {
+			return nil, fmt.Errorf("merge failed with failed to marshal merged map: %w", err)
+		}
+		maps.Copy(out, in)
+		out[operation.Into] = mergedBytes
 	}
 	}
+
 	return out, nil
 	return out, nil
 }
 }
 
 
+// merge merges the input maps and returns the merged map and a list of conflicting keys.
+func merge(operation esv1.ExternalSecretRewriteMerge, in map[string][]byte) (map[string]any, []string, error) {
+	mergedMap := make(map[string]any)
+	conflicts := make([]string, 0)
+
+	// sort keys with priority keys at the end in their specified order
+	keys := sortKeysWithPriority(operation, in)
+
+	for _, key := range keys {
+		value, exists := in[key]
+		if !exists {
+			return nil, nil, fmt.Errorf("merge failed with key %q not found in input map", key)
+		}
+		var jsonMap map[string]any
+		if err := json.Unmarshal(value, &jsonMap); err != nil {
+			return nil, nil, fmt.Errorf("merge failed with failed to unmarshal JSON: %w", err)
+		}
+
+		for k, v := range jsonMap {
+			if _, conflict := mergedMap[k]; conflict {
+				conflicts = append(conflicts, k)
+			}
+			mergedMap[k] = v
+		}
+	}
+
+	return mergedMap, conflicts, nil
+}
+
+// sortKeysWithPriority sorts keys with priority keys at the end in their specified order.
+// Non-priority keys are sorted alphabetically and placed before priority keys.
+func sortKeysWithPriority(operation esv1.ExternalSecretRewriteMerge, in map[string][]byte) []string {
+	keys := make([]string, 0, len(in))
+	for k := range in {
+		if !slices.Contains(operation.Priority, k) {
+			keys = append(keys, k)
+		}
+	}
+	sort.Strings(keys)
+	keys = append(keys, operation.Priority...)
+	return keys
+}
+
 // RewriteRegexp rewrites a single Regexp Rewrite Operation.
 // RewriteRegexp rewrites a single Regexp Rewrite Operation.
 func RewriteRegexp(operation esv1.ExternalSecretRewriteRegexp, in map[string][]byte) (map[string][]byte, error) {
 func RewriteRegexp(operation esv1.ExternalSecretRewriteRegexp, in map[string][]byte) (map[string][]byte, error) {
 	out := make(map[string][]byte)
 	out := make(map[string][]byte)
 	re, err := regexp.Compile(operation.Source)
 	re, err := regexp.Compile(operation.Source)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("regexp failed with failed to compile: %w", err)
 	}
 	}
 	for key, value := range in {
 	for key, value := range in {
 		newKey := re.ReplaceAllString(key, operation.Target)
 		newKey := re.ReplaceAllString(key, operation.Target)
@@ -120,7 +212,7 @@ func RewriteTransform(operation esv1.ExternalSecretRewriteTransform, in map[stri
 
 
 		result, err := transform(operation.Template, data)
 		result, err := transform(operation.Template, data)
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("transform failed with failed to transform key: %w", err)
 		}
 		}
 
 
 		newKey := string(result)
 		newKey := string(result)

+ 204 - 19
pkg/utils/utils_test.go

@@ -430,6 +430,67 @@ func TestRewrite(t *testing.T) {
 		wantErr bool
 		wantErr bool
 	}{
 	}{
 		{
 		{
+			name: "using double merge",
+			args: args{
+				operations: []esv1.ExternalSecretRewrite{
+					{
+						Merge: &esv1.ExternalSecretRewriteMerge{
+							Strategy:       esv1.ExternalSecretRewriteMergeStrategyJSON,
+							ConflictPolicy: esv1.ExternalSecretRewriteMergeConflictPolicyIgnore,
+							Into:           "merged",
+							Priority:       []string{"a"},
+						},
+					},
+					{
+						Merge: &esv1.ExternalSecretRewriteMerge{
+							Strategy:       esv1.ExternalSecretRewriteMergeStrategyExtract,
+							ConflictPolicy: esv1.ExternalSecretRewriteMergeConflictPolicyIgnore,
+							Priority:       []string{"b"},
+						},
+					},
+				},
+				in: map[string][]byte{
+					"a": []byte(`{"host": "dba", "pass": "yola", "port": 123}`),
+					"b": []byte(`{"host": "dbb", "pass": "yolb"}`),
+				},
+			},
+			want: map[string][]byte{
+				"host": []byte("dbb"),
+				"pass": []byte("yolb"),
+				"port": []byte("123"),
+			},
+		},
+		{
+			name: "using regexp and merge",
+			args: args{
+				operations: []esv1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1.ExternalSecretRewriteRegexp{
+							Source: "db/(.*)",
+							Target: "$1",
+						},
+					},
+					{
+						Merge: &esv1.ExternalSecretRewriteMerge{
+							Strategy:       esv1.ExternalSecretRewriteMergeStrategyJSON,
+							ConflictPolicy: esv1.ExternalSecretRewriteMergeConflictPolicyIgnore,
+							Into:           "merged",
+							Priority:       []string{"a"},
+						},
+					},
+				},
+				in: map[string][]byte{
+					"db/a": []byte(`{"host": "dba.example.com"}`),
+					"db/b": []byte(`{"host": "dbb.example.com", "pass": "yolo"}`),
+				},
+			},
+			want: map[string][]byte{
+				"a":      []byte(`{"host": "dba.example.com"}`),
+				"b":      []byte(`{"host": "dbb.example.com", "pass": "yolo"}`),
+				"merged": []byte(`{"host":"dba.example.com","pass":"yolo"}`),
+			},
+		},
+		{
 			name: "replace of a single key",
 			name: "replace of a single key",
 			args: args{
 			args: args{
 				operations: []esv1.ExternalSecretRewrite{
 				operations: []esv1.ExternalSecretRewrite{
@@ -578,50 +639,174 @@ func TestRewrite(t *testing.T) {
 					},
 					},
 				},
 				},
 				in: map[string][]byte{
 				in: map[string][]byte{
-					"my/app/bar/api-key":      []byte("bar"),
-					"my/app/bar/api-password": []byte("barr"),
+					"my/app/bar/key": []byte("bar"),
 				},
 				},
 			},
 			},
 			want: map[string][]byte{
 			want: map[string][]byte{
-				"APP_API_KEY":      []byte("bar"),
-				"APP_API_PASSWORD": []byte("barr"),
+				"APP_KEY": []byte("bar"),
 			},
 			},
 		},
 		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := RewriteMap(tt.args.operations, tt.args.in)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("RewriteMap() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("RewriteMap() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestRewriteMerge(t *testing.T) {
+	type args struct {
+		operation esv1.ExternalSecretRewriteMerge
+		in        map[string][]byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    map[string][]byte
+		wantErr bool
+	}{
 		{
 		{
-			name: "using transform rewrite operation to lower case",
+			name: "using empty merge",
 			args: args{
 			args: args{
-				operations: []esv1.ExternalSecretRewrite{
-					{
-						Transform: &esv1.ExternalSecretRewriteTransform{
-							Template: `{{ .value | lower }}`,
-						},
-					},
+				operation: esv1.ExternalSecretRewriteMerge{},
+				in: map[string][]byte{
+					"mongo-credentials": []byte(`{"username": "foz", "password": "baz"}`),
+					"redis-credentials": []byte(`{"host": "redis.example.com", "port": "6379"}`),
+				},
+			},
+			want: map[string][]byte{
+				"username": []byte("foz"),
+				"password": []byte("baz"),
+				"host":     []byte("redis.example.com"),
+				"port":     []byte("6379"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "using priority",
+			args: args{
+				operation: esv1.ExternalSecretRewriteMerge{
+					ConflictPolicy: esv1.ExternalSecretRewriteMergeConflictPolicyIgnore,
+					Priority:       []string{"mongo-credentials", "redis-credentials"},
+				},
+				in: map[string][]byte{
+					"redis-credentials": []byte(`{"host": "redis.example.com", "port": "6379"}`),
+					"mongo-credentials": []byte(`{"username": "foz", "password": "baz"}`),
+					"other-credentials": []byte(`{"key": "value", "host": "other.example.com"}`),
+				},
+			},
+			want: map[string][]byte{
+				"username": []byte("foz"),
+				"password": []byte("baz"),
+				"host":     []byte("redis.example.com"),
+				"port":     []byte("6379"),
+				"key":      []byte("value"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "using priority with keys not in input",
+			args: args{
+				operation: esv1.ExternalSecretRewriteMerge{
+					ConflictPolicy: esv1.ExternalSecretRewriteMergeConflictPolicyIgnore,
+					Priority:       []string{"non-existent-key", "another-missing-key", "mongo-credentials"},
+				},
+				in: map[string][]byte{
+					"mongo-credentials": []byte(`{"username": "foz", "password": "baz"}`),
+					"redis-credentials": []byte(`{"host": "redis.example.com", "port": "6379"}`),
+				},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name: "using conflict policy error",
+			args: args{
+				operation: esv1.ExternalSecretRewriteMerge{
+					ConflictPolicy: esv1.ExternalSecretRewriteMergeConflictPolicyError,
 				},
 				},
 				in: map[string][]byte{
 				in: map[string][]byte{
-					"API_FOO": []byte("bar"),
-					"KEY_FOO": []byte("barr"),
+					"mongo-credentials": []byte(`{"username": "foz", "password": "baz"}`),
+					"redis-credentials": []byte(`{"username": "redis", "port": "6379"}`),
+				},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name: "using JSON strategy",
+			args: args{
+				operation: esv1.ExternalSecretRewriteMerge{
+					Strategy: esv1.ExternalSecretRewriteMergeStrategyJSON,
+					Into:     "credentials",
+				},
+				in: map[string][]byte{
+					"mongo-credentials": []byte(`{"username": "foz", "password": "baz"}`),
+					"redis-credentials": []byte(`{"host": "redis.example.com", "port": "6379"}`),
 				},
 				},
 			},
 			},
 			want: map[string][]byte{
 			want: map[string][]byte{
-				"api_foo": []byte("bar"),
-				"key_foo": []byte("barr"),
+				"mongo-credentials": []byte(`{"username": "foz", "password": "baz"}`),
+				"redis-credentials": []byte(`{"host": "redis.example.com", "port": "6379"}`),
+				"credentials": func() []byte {
+					expected := map[string]interface{}{
+						"username": "foz",
+						"password": "baz",
+						"host":     "redis.example.com",
+						"port":     "6379",
+					}
+					b, _ := json.Marshal(expected)
+					return b
+				}(),
 			},
 			},
+			wantErr: false,
+		},
+		{
+			name: "using JSON strategy without into",
+			args: args{
+				operation: esv1.ExternalSecretRewriteMerge{
+					Strategy: esv1.ExternalSecretRewriteMergeStrategyJSON,
+				},
+				in: map[string][]byte{
+					"mongo-credentials": []byte(`{"username": "foz", "password": "baz"}`),
+					"redis-credentials": []byte(`{"host": "redis.example.com", "port": "6379"}`),
+				},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name: "with invalid JSON",
+			args: args{
+				operation: esv1.ExternalSecretRewriteMerge{},
+				in: map[string][]byte{
+					"invalid-json": []byte(`{"username": "foz", "password": "baz"`),
+				},
+			},
+			want:    nil,
+			wantErr: true,
 		},
 		},
 	}
 	}
 	for _, tt := range tests {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := RewriteMap(tt.args.operations, tt.args.in)
+			got, err := RewriteMerge(tt.args.operation, tt.args.in)
 			if (err != nil) != tt.wantErr {
 			if (err != nil) != tt.wantErr {
-				t.Errorf("RewriteMap() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("RewriteMerge() error = %v, wantErr %v", err, tt.wantErr)
 				return
 				return
 			}
 			}
 			if !reflect.DeepEqual(got, tt.want) {
 			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("RewriteMap() = %v, want %v", got, tt.want)
+				t.Errorf("RewriteMerge() = %v, want %v", got, tt.want)
 			}
 			}
 		})
 		})
 	}
 	}
 }
 }
-
 func TestReverse(t *testing.T) {
 func TestReverse(t *testing.T) {
 	type args struct {
 	type args struct {
 		strategy esv1alpha1.PushSecretConversionStrategy
 		strategy esv1alpha1.PushSecretConversionStrategy