Skip to content

Commit 0193c1d

Browse files
authored
Merge pull request #91 from phisco/respect-merge-options
feat: toFieldPath policies, replacing upstream MergeOptions
2 parents c732bf9 + 14ea3fd commit 0193c1d

File tree

9 files changed

+174
-514
lines changed

9 files changed

+174
-514
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,12 @@ These fields are now required. This makes P&T configuration less ambiguous:
128128
* `resources[i].patches[i].transforms[i].math.type`
129129

130130
Also, the `resources[i].patches[i].policy.mergeOptions` field is no longer
131-
supported.
132-
133-
Composition functions use Kubernetes server-side apply to intelligently merge
134-
arrays and objects. This requires merge configuration to be specified at the
135-
composed resource schema level (i.e. in CRDs) per [#4617].
131+
supported. The same capability can be achieved by setting
132+
`resources[i].patches[i].policy.toFieldPath` to:
133+
- `MergeObject` - equivalent to
134+
`resources[i].patches[i].policy.mergeOptions.keepMapValues: true`
135+
- `AppendArray` - equivalent to
136+
`resources[i].patches[i].policy.mergeOptions.appendSlice: false`
136137

137138
## Developing this function
138139

fn_test.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ func TestRunFunction(t *testing.T) {
465465
},
466466
{
467467
Name: "existing-resource",
468-
Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD","spec":{}}`)},
468+
Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"targetObject": {"keep": "me"}}}`)},
469469
Patches: []v1beta1.ComposedPatch{
470470
{
471471
// This patch should work.
@@ -475,6 +475,17 @@ func TestRunFunction(t *testing.T) {
475475
ToFieldPath: ptr.To[string]("spec.watchers"),
476476
},
477477
},
478+
{
479+
// This patch should work too and properly handle mergeOptions.
480+
Type: v1beta1.PatchTypeFromCompositeFieldPath,
481+
Patch: v1beta1.Patch{
482+
FromFieldPath: ptr.To[string]("spec.sourceObject"),
483+
ToFieldPath: ptr.To[string]("spec.targetObject"),
484+
Policy: &v1beta1.PatchPolicy{
485+
ToFieldPath: ptr.To(v1beta1.ToFieldPathPolicyMergeObject),
486+
},
487+
},
488+
},
478489
{
479490
// This patch will fail because the path
480491
// is not found.
@@ -492,7 +503,7 @@ func TestRunFunction(t *testing.T) {
492503
}),
493504
Observed: &fnv1beta1.State{
494505
Composite: &fnv1beta1.Resource{
495-
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10"}}`),
506+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","spec":{"widgets":"10", "sourceObject": {"me": "too"}}}`),
496507
},
497508
Resources: map[string]*fnv1beta1.Resource{
498509
// "existing-resource" exists.
@@ -520,7 +531,7 @@ func TestRunFunction(t *testing.T) {
520531
// Note that the first patch did work. We only
521532
// skipped the patch from the required field path.
522533
"existing-resource": {
523-
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":"10"}}`),
534+
Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","spec":{"watchers":"10", "targetObject": {"me": "too", "keep": "me"}}}`),
524535
},
525536

526537
// Note "new-resource" doesn't appear here.
@@ -534,7 +545,7 @@ func TestRunFunction(t *testing.T) {
534545
},
535546
{
536547
Severity: fnv1beta1.Severity_SEVERITY_WARNING,
537-
Message: `cannot render composed resource "existing-resource" "FromCompositeFieldPath" patch at index 1: ignoring 'policy.fromFieldPath: Required' because 'to' resource already exists: spec.doesNotExist: no such field`,
548+
Message: `cannot render composed resource "existing-resource" "FromCompositeFieldPath" patch at index 2: ignoring 'policy.fromFieldPath: Required' because 'to' resource already exists: spec.doesNotExist: no such field`,
538549
},
539550
},
540551
},

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ require (
4747
github.com/mailru/easyjson v0.7.7 // indirect
4848
github.com/mattn/go-colorable v0.1.13 // indirect
4949
github.com/mattn/go-isatty v0.0.20 // indirect
50-
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
5150
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
5251
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
5352
github.com/modern-go/reflect2 v1.0.2 // indirect

go.sum

Lines changed: 31 additions & 498 deletions
Large diffs are not rendered by default.

input/v1beta1/resources_patches.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ const (
3232
FromFieldPathPolicyRequired FromFieldPathPolicy = "Required"
3333
)
3434

35+
// A ToFieldPathPolicy determines how to patch to a field path.
36+
type ToFieldPathPolicy string
37+
38+
// ToFieldPath patch policies.
39+
const (
40+
ToFieldPathPolicyReplace ToFieldPathPolicy = "Replace"
41+
ToFieldPathPolicyMergeObject ToFieldPathPolicy = "MergeObject"
42+
ToFieldPathPolicyAppendArray ToFieldPathPolicy = "AppendArray"
43+
)
44+
3545
// A PatchPolicy configures the specifics of patching behaviour.
3646
type PatchPolicy struct {
3747
// FromFieldPath specifies how to patch from a field path. The default is
@@ -41,6 +51,15 @@ type PatchPolicy struct {
4151
// +kubebuilder:validation:Enum=Optional;Required
4252
// +optional
4353
FromFieldPath *FromFieldPathPolicy `json:"fromFieldPath,omitempty"`
54+
55+
// ToFieldPath specifies how to patch to a field path. The default is
56+
// 'Replace', which means the patch will completely replace the target field,
57+
// or create it if it does not exist. Use 'MergeObject' to merge the patch
58+
// object with the target object, or 'AppendArray' to append the patch array
59+
// to the target array.
60+
// +kubebuilder:validation:Enum=Replace;MergeObject;AppendArray
61+
// +optional
62+
ToFieldPath *ToFieldPathPolicy `json:"toFieldPath,omitempty"`
4463
}
4564

4665
// GetFromFieldPathPolicy returns the FromFieldPathPolicy for this PatchPolicy, defaulting to FromFieldPathPolicyOptional if not specified.
@@ -51,6 +70,14 @@ func (pp *PatchPolicy) GetFromFieldPathPolicy() FromFieldPathPolicy {
5170
return *pp.FromFieldPath
5271
}
5372

73+
// GetToFieldPathPolicy returns the ToFieldPathPolicy for this PatchPolicy, defaulting to ToFieldPathPolicyReplace if not specified.
74+
func (pp *PatchPolicy) GetToFieldPathPolicy() ToFieldPathPolicy {
75+
if pp == nil || pp.ToFieldPath == nil {
76+
return ToFieldPathPolicyReplace
77+
}
78+
return *pp.ToFieldPath
79+
}
80+
5481
// Environment represents the Composition environment.
5582
type Environment struct {
5683
// Patches is a list of environment patches that are executed before a

input/v1beta1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package/input/pt.fn.crossplane.io_resources.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ spec:
117117
- Optional
118118
- Required
119119
type: string
120+
toFieldPath:
121+
description: |-
122+
ToFieldPath specifies how to patch to a field path. The default is
123+
'Replace', which means the patch will completely replace the target field,
124+
or create it if it does not exist. Use 'MergeObject' to merge the patch
125+
object with the target object, or 'AppendArray' to append the patch array
126+
to the target array.
127+
enum:
128+
- Replace
129+
- MergeObject
130+
- AppendArray
131+
type: string
120132
type: object
121133
toFieldPath:
122134
description: |-
@@ -448,6 +460,18 @@ spec:
448460
- Optional
449461
- Required
450462
type: string
463+
toFieldPath:
464+
description: |-
465+
ToFieldPath specifies how to patch to a field path. The default is
466+
'Replace', which means the patch will completely replace the target field,
467+
or create it if it does not exist. Use 'MergeObject' to merge the patch
468+
object with the target object, or 'AppendArray' to append the patch array
469+
to the target array.
470+
enum:
471+
- Replace
472+
- MergeObject
473+
- AppendArray
474+
type: string
451475
type: object
452476
toFieldPath:
453477
description: |-
@@ -843,6 +867,18 @@ spec:
843867
- Optional
844868
- Required
845869
type: string
870+
toFieldPath:
871+
description: |-
872+
ToFieldPath specifies how to patch to a field path. The default is
873+
'Replace', which means the patch will completely replace the target field,
874+
or create it if it does not exist. Use 'MergeObject' to merge the patch
875+
object with the target object, or 'AppendArray' to append the patch array
876+
to the target array.
877+
enum:
878+
- Replace
879+
- MergeObject
880+
- AppendArray
881+
type: string
846882
type: object
847883
toFieldPath:
848884
description: |-

patches.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"github.com/pkg/errors"
88
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
99
"k8s.io/apimachinery/pkg/runtime"
10+
"k8s.io/utils/ptr"
1011

12+
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
1113
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
1214

1315
"github.com/crossplane/function-sdk-go/resource/composed"
@@ -20,11 +22,11 @@ const (
2022
errPatchSetType = "a patch in a PatchSet cannot be of type PatchSet"
2123

2224
errFmtUndefinedPatchSet = "cannot find PatchSet by name %s"
23-
errFmtInvalidPatchType = "patch type %s is unsupported"
2425
errFmtCombineStrategyNotSupported = "combine strategy %s is not supported"
2526
errFmtCombineConfigMissing = "given combine strategy %s requires configuration"
2627
errFmtCombineStrategyFailed = "%s strategy could not combine"
2728
errFmtExpandingArrayFieldPaths = "cannot expand ToFieldPath %s"
29+
errFmtInvalidPatchPolicy = "invalid patch policy %s"
2830
)
2931

3032
// A PatchInterface is a patch that can be applied between resources.
@@ -80,7 +82,35 @@ func ApplyFromFieldPathPatch(p PatchInterface, from, to runtime.Object) error {
8082
return patchFieldValueToMultiple(p.GetToFieldPath(), out, to)
8183
}
8284

83-
return errors.Wrap(patchFieldValueToObject(p.GetToFieldPath(), out, to), "cannot patch to object")
85+
mo, err := toMergeOption(p)
86+
if err != nil {
87+
return err
88+
}
89+
90+
return errors.Wrap(patchFieldValueToObject(p.GetToFieldPath(), out, to, mo), "cannot patch to object")
91+
}
92+
93+
// toMergeOption returns the MergeOptions from the PatchPolicy's ToFieldPathPolicy, if defined.
94+
func toMergeOption(p PatchInterface) (mo *xpv1.MergeOptions, err error) {
95+
if p == nil {
96+
return nil, nil
97+
}
98+
pp := p.GetPolicy()
99+
if pp == nil {
100+
return nil, nil
101+
}
102+
switch pp.GetToFieldPathPolicy() {
103+
case v1beta1.ToFieldPathPolicyReplace:
104+
// nothing to do, this is the default
105+
case v1beta1.ToFieldPathPolicyAppendArray:
106+
mo = &xpv1.MergeOptions{AppendSlice: ptr.To(true)}
107+
case v1beta1.ToFieldPathPolicyMergeObject:
108+
mo = &xpv1.MergeOptions{KeepMapValues: ptr.To(true)}
109+
default:
110+
// should never happen
111+
return nil, errors.Errorf(errFmtInvalidPatchPolicy, pp.GetToFieldPathPolicy())
112+
}
113+
return mo, nil
84114
}
85115

86116
// ApplyCombineFromVariablesPatch patches the "to" resource, taking a list of
@@ -127,7 +157,7 @@ func ApplyCombineFromVariablesPatch(p PatchInterface, from, to runtime.Object) e
127157
return err
128158
}
129159

130-
return errors.Wrap(patchFieldValueToObject(p.GetToFieldPath(), out, to), "cannot patch to object")
160+
return errors.Wrap(patchFieldValueToObject(p.GetToFieldPath(), out, to, nil), "cannot patch to object")
131161
}
132162

133163
// ApplyEnvironmentPatch applies a patch to or from the environment. Patches to
@@ -298,13 +328,15 @@ func ComposedTemplates(pss []v1beta1.PatchSet, cts []v1beta1.ComposedTemplate) (
298328

299329
// patchFieldValueToObject applies the value to the "to" object at the given
300330
// path, returning any errors as they occur.
301-
func patchFieldValueToObject(fieldPath string, value any, to runtime.Object) error {
331+
// If no merge options is supplied, then destination field is replaced
332+
// with the given value.
333+
func patchFieldValueToObject(fieldPath string, value any, to runtime.Object, mo *xpv1.MergeOptions) error {
302334
paved, err := fieldpath.PaveObject(to)
303335
if err != nil {
304336
return err
305337
}
306338

307-
if err := paved.SetValue(fieldPath, value); err != nil {
339+
if err := paved.MergeValue(fieldPath, value, mo); err != nil {
308340
return err
309341
}
310342

validate.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,23 @@ func ValidatePatch(p PatchInterface) *field.Error { //nolint: gocyclo // This is
199199
return WrapFieldError(err, field.NewPath("transforms").Index(i))
200200
}
201201
}
202-
202+
if pp := p.GetPolicy(); pp != nil {
203+
switch pp.GetToFieldPathPolicy() {
204+
case v1beta1.ToFieldPathPolicyReplace,
205+
v1beta1.ToFieldPathPolicyAppendArray,
206+
v1beta1.ToFieldPathPolicyMergeObject:
207+
// ok
208+
default:
209+
return field.Invalid(field.NewPath("policy", "toFieldPathPolicy"), pp.GetToFieldPathPolicy(), "unknown toFieldPathPolicy")
210+
}
211+
switch pp.GetFromFieldPathPolicy() {
212+
case v1beta1.FromFieldPathPolicyRequired,
213+
v1beta1.FromFieldPathPolicyOptional:
214+
// ok
215+
default:
216+
return field.Invalid(field.NewPath("policy", "fromFieldPathPolicy"), pp.GetFromFieldPathPolicy(), "unknown fromFieldPathPolicy")
217+
}
218+
}
203219
return nil
204220
}
205221

0 commit comments

Comments
 (0)