diff --git a/fn.go b/fn.go index e0662f6..c0cdcdd 100644 --- a/fn.go +++ b/fn.go @@ -2,6 +2,7 @@ package main import ( "context" + "maps" "reflect" "sort" @@ -85,6 +86,15 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) response.Fatal(rsp, errors.Wrapf(err, "cannot create new Struct from extra resources output")) return rsp, nil } + + if in.Spec.Context.GetPolicy() == v1beta1.ContextPolicyMerge { + v, _ := request.GetContextKey(req, in.Spec.Context.GetKey()) + if fields := v.GetStructValue().GetFields(); fields != nil { + maps.Copy(fields, s.GetFields()) + s.Fields = fields + } + } + response.SetContextKey(rsp, in.Spec.Context.GetKey(), structpb.NewStructValue(s)) return rsp, nil diff --git a/fn_test.go b/fn_test.go index 2dde8be..d2755d6 100644 --- a/fn_test.go +++ b/fn_test.go @@ -230,6 +230,21 @@ func TestRunFunction(t *testing.T) { args: args{ req: &fnv1.RunFunctionRequest{ Meta: &fnv1.RequestMeta{Tag: "hello"}, + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + v1beta1.FunctionContextKeyExtraResources: structpb.NewStructValue(resource.MustStructJSON(`{ + "previous": [ + { + "apiVersion": "apiextensions.crossplane.io/v1beta1", + "kind": "EnvironmentConfig", + "metadata": { + "name": "previous" + } + } + ] + }`)), + }, + }, Observed: &fnv1.State{ Composite: &fnv1.Resource{ Resource: resource.MustStructJSON(`{ @@ -646,7 +661,7 @@ func TestRunFunction(t *testing.T) { "apiVersion": "apiextensions.crossplane.io/v1beta1", "type": "Reference", "into": "obj-0", - "ref": { + "ref": { "name": "my-env-config" } } @@ -688,6 +703,116 @@ func TestRunFunction(t *testing.T) { }, }, }, + "MergeContext": { + reason: "The Function should put resolved extra resources into existing context value when policy is Merge.", + args: args{ + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "hello"}, + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + v1beta1.FunctionContextKeyExtraResources: structpb.NewStructValue(resource.MustStructJSON(`{ + "previous": [ + { + "apiVersion": "apiextensions.crossplane.io/v1beta1", + "kind": "EnvironmentConfig", + "metadata": { + "name": "previous" + } + } + ] + }`)), + }, + }, + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(`{ + "apiVersion": "test.crossplane.io/v1alpha1", + "kind": "XR", + "metadata": { + "name": "my-xr" + } + }`), + }, + }, + RequiredResources: map[string]*fnv1.Resources{ + "obj-0": { + Items: []*fnv1.Resource{ + { + Resource: resource.MustStructJSON(`{ + "apiVersion": "apiextensions.crossplane.io/v1beta1", + "kind": "EnvironmentConfig", + "metadata": { + "name": "my-env-config" + } + }`), + }, + }, + }, + }, + Input: resource.MustStructJSON(`{ + "apiVersion": "extra-resources.fn.crossplane.io/v1beta1", + "kind": "Input", + "spec": { + "context": { + "policy": "Merge" + }, + "extraResources": [ + { + "kind": "EnvironmentConfig", + "apiVersion": "apiextensions.crossplane.io/v1beta1", + "type": "Reference", + "into": "obj-0", + "ref": { + "name": "my-env-config" + } + } + ] + } + }`), + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1.Result{}, + Requirements: &fnv1.Requirements{ + Resources: map[string]*fnv1.ResourceSelector{ + "obj-0": { + ApiVersion: "apiextensions.crossplane.io/v1beta1", + Kind: "EnvironmentConfig", + Match: &fnv1.ResourceSelector_MatchName{ + MatchName: "my-env-config", + }, + }, + }, + }, + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + v1beta1.FunctionContextKeyExtraResources: structpb.NewStructValue(resource.MustStructJSON(`{ + "previous": [ + { + "apiVersion": "apiextensions.crossplane.io/v1beta1", + "kind": "EnvironmentConfig", + "metadata": { + "name": "previous" + } + } + ], + "obj-0": [ + { + "apiVersion": "apiextensions.crossplane.io/v1beta1", + "kind": "EnvironmentConfig", + "metadata": { + "name": "my-env-config" + } + } + ] + }`)), + }, + }, + }, + }, + }, } for name, tc := range cases { diff --git a/input/v1beta1/resource_select.go b/input/v1beta1/resource_select.go index 49dc69e..d5e44bb 100644 --- a/input/v1beta1/resource_select.go +++ b/input/v1beta1/resource_select.go @@ -48,6 +48,13 @@ type Context struct { // standard functions such as Function Patch and Transform. // +kubebuilder:default=apiextensions.crossplane.io/extra-resources Key *string `json:"key,omitempty"` + + // Policy specifies how to handle the context's potentially existing value. + // Replace replaces the existing context key with the new extra resources. + // Merge merges the extra resources into the context key's existing value. + // +kubebuilder:default=Replace + // +kubebuilder:validation:Enum=Replace;Merge + Policy *ContextPolicy `json:"policy,omitempty"` } // GetKey returns the key of the context, defaulting to @@ -59,6 +66,25 @@ func (i *Context) GetKey() string { return *i.Key } +// ContextPolicy specifies how to handle the context's potentially existing value. +type ContextPolicy string + +const ( + // ContextPolicyReplace replaces the context key. + ContextPolicyReplace ContextPolicy = "Replace" + // ContextPolicyMerge merges into the context key. + ContextPolicyMerge ContextPolicy = "Merge" +) + +// GetPolicy returns the policy of the context, defaulting to Replace if not +// specified. +func (i *Context) GetPolicy() ContextPolicy { + if i == nil || i.Policy == nil { + return ContextPolicyReplace + } + return *i.Policy +} + // Policy represents the Resolution policy of Reference instance. type Policy struct { // Resolution specifies whether resolution of this reference is required. diff --git a/input/v1beta1/zz_generated.deepcopy.go b/input/v1beta1/zz_generated.deepcopy.go index d75d0a0..104714d 100644 --- a/input/v1beta1/zz_generated.deepcopy.go +++ b/input/v1beta1/zz_generated.deepcopy.go @@ -17,6 +17,11 @@ func (in *Context) DeepCopyInto(out *Context) { *out = new(string) **out = **in } + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(ContextPolicy) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Context. diff --git a/package/input/extra-resources.fn.crossplane.io_inputs.yaml b/package/input/extra-resources.fn.crossplane.io_inputs.yaml index c5fa61f..168e9d8 100644 --- a/package/input/extra-resources.fn.crossplane.io_inputs.yaml +++ b/package/input/extra-resources.fn.crossplane.io_inputs.yaml @@ -52,6 +52,16 @@ spec: E.g. 'apiextensions.crossplane.io/environment', the environment used in standard functions such as Function Patch and Transform. type: string + policy: + default: Replace + description: |- + Policy specifies how to handle the context's potentially existing value. + Replace replaces the existing context key with the new extra resources. + Merge merges the extra resources into the context key's existing value. + enum: + - Replace + - Merge + type: string type: object extraResources: description: |-