From 937ee9f2f6904ff826b9db17b13ee763b5c93c04 Mon Sep 17 00:00:00 2001 From: Amund Tenstad Date: Sun, 1 Feb 2026 11:00:50 +0100 Subject: [PATCH 1/3] feat: configurable environment path Signed-off-by: Amund Tenstad --- fn.go | 29 ++++- fn_test.go | 130 +++++++++++++++++++++++ input/v1beta1/composition_environment.go | 4 + 3 files changed, 159 insertions(+), 4 deletions(-) diff --git a/fn.go b/fn.go index deb75c2..0e7aae0 100644 --- a/fn.go +++ b/fn.go @@ -143,6 +143,8 @@ func getSelectedEnvConfigs(in *v1beta1.Input, requiredResources map[string][]res // Skip if the required resource was not requested (e.g., optional selector with no matchLabels) continue } + + var processed []unstructured.Unstructured switch config.GetType() { case v1beta1.EnvironmentSourceTypeReference: out, err := processSourceByReference(in, config, resources) @@ -152,21 +154,40 @@ func getSelectedEnvConfigs(in *v1beta1.Input, requiredResources map[string][]res if out == nil { continue } - envConfigs = append(envConfigs, *out) + processed = []unstructured.Unstructured{*out} case v1beta1.EnvironmentSourceTypeSelector: out, err := processEnvironmentSource(config, resources) if err != nil { return nil, errors.Wrapf(err, "cannot process environment config %q by selector", extraResName) } - if len(out) > 0 { - envConfigs = append(envConfigs, out...) - } + processed = out } + + if err := moveDataToFieldPath(config.ToFieldPath, processed); err != nil { + return nil, err + } + envConfigs = append(envConfigs, processed...) } return envConfigs, nil } +func moveDataToFieldPath(fieldPath *string, envConfigs []unstructured.Unstructured) error { + if fieldPath == nil { + return nil + } + + for _, e := range envConfigs { + data := e.Object["data"] + delete(e.Object, "data") + + if err := fieldpath.Pave(e.Object).SetValue("data."+*fieldPath, data); err != nil { + return errors.Errorf("Unable to move environment to target path '%s'", *fieldPath) + } + } + return nil +} + func processEnvironmentSource(config v1beta1.EnvironmentSource, resources []resource.Required) ([]unstructured.Unstructured, error) { out := make([]unstructured.Unstructured, 0) selector := config.Selector diff --git a/fn_test.go b/fn_test.go index 92892c4..955441e 100644 --- a/fn_test.go +++ b/fn_test.go @@ -773,6 +773,136 @@ func TestRunFunction(t *testing.T) { }, }, }, + "ToFieldPath": { + reason: "The Function should load into the specified toFieldPath", + args: args{ + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "hello"}, + Input: resource.MustStructJSON(`{ + "apiVersion": "template.fn.crossplane.io/v1beta1", + "kind": "Input", + "spec": { + "defaultData": { + "a": "from-default" + }, + "environmentConfigs": [ + { + "type": "Reference", + "ref": { + "name": "foo" + }, + "toFieldPath": "foo" + }, + { + "type": "Selector", + "selector": { + "mode": "Multiple", + "matchLabels": [ + { + "type": "Value", + "key": "foo", + "value": "bar" + } + ] + }, + "toFieldPath": "foo.bar" + } + ] + } + }`), + RequiredResources: map[string]*fnv1.Resources{ + "environment-config-0": { + Items: []*fnv1.Resource{ + { + Resource: resource.MustStructJSON(`{ + "apiVersion": "apiextensions.crossplane.io/v1beta1", + "kind": "EnvironmentConfig", + "metadata": { + "name": "foo" + }, + "data": { + "a": "from-foo" + } + }`), + }, + }, + }, + "environment-config-1": { + Items: []*fnv1.Resource{ + { + Resource: resource.MustStructJSON(`{ + "apiVersion": "apiextensions.crossplane.io/v1beta1", + "kind": "EnvironmentConfig", + "metadata": { + "name": "first" + }, + "data": { + "a": "from-label-select-first" + } + }`), + }, + { + Resource: resource.MustStructJSON(`{ + "apiVersion": "apiextensions.crossplane.io/v1beta1", + "kind": "EnvironmentConfig", + "metadata": { + "name": "second" + }, + "data": { + "b": "from-label-select-second" + } + }`), + }, + }, + }, + }, + }, + }, + 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{ + "environment-config-0": { + ApiVersion: "apiextensions.crossplane.io/v1beta1", + Kind: "EnvironmentConfig", + Match: &fnv1.ResourceSelector_MatchName{ + MatchName: "foo", + }, + }, + "environment-config-1": { + ApiVersion: "apiextensions.crossplane.io/v1beta1", + Kind: "EnvironmentConfig", + Match: &fnv1.ResourceSelector_MatchLabels{ + MatchLabels: &fnv1.MatchLabels{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + }, + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + FunctionContextKeyEnvironment: structpb.NewStructValue(resource.MustStructJSON(`{ + "apiVersion": "internal.crossplane.io/v1alpha1", + "kind": "Environment", + "a": "from-default", + "foo": { + "a": "from-foo", + "bar": { + "a": "from-label-select-first", + "b": "from-label-select-second" + } + } + }`)), + }, + }, + }, + }, + }, } for name, tc := range cases { diff --git a/input/v1beta1/composition_environment.go b/input/v1beta1/composition_environment.go index 529fec7..81b9d79 100644 --- a/input/v1beta1/composition_environment.go +++ b/input/v1beta1/composition_environment.go @@ -111,6 +111,10 @@ type EnvironmentSource struct { // Selector selects EnvironmentConfig(s) via labels. // +optional Selector *EnvironmentSourceSelector `json:"selector,omitempty"` + + // ToFieldPath specifies where in the environment to load the EnvironmentConfig(s). + // +optional + ToFieldPath *string `json:"toFieldPath,omitempty"` } // GetType returns the type of the environment source, returning the default if not set. From 72b4fcaa358710b443fd0960bf361acc587fcf7f Mon Sep 17 00:00:00 2001 From: Amund Tenstad Date: Sun, 1 Feb 2026 16:45:07 +0100 Subject: [PATCH 2/3] refactor: dont modify EnvironmentConfig before data extraction Signed-off-by: Amund Tenstad --- fn.go | 59 +++++++++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/fn.go b/fn.go index 0e7aae0..255d461 100644 --- a/fn.go +++ b/fn.go @@ -135,7 +135,9 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) return rsp, nil } -func getSelectedEnvConfigs(in *v1beta1.Input, requiredResources map[string][]resource.Required) (envConfigs []unstructured.Unstructured, err error) { +func getSelectedEnvConfigs(in *v1beta1.Input, requiredResources map[string][]resource.Required) (map[string][]unstructured.Unstructured, error) { + envConfigs := make(map[string][]unstructured.Unstructured) + for i, config := range in.Spec.EnvironmentConfigs { extraResName := fmt.Sprintf("environment-config-%d", i) resources, ok := requiredResources[extraResName] @@ -144,7 +146,11 @@ func getSelectedEnvConfigs(in *v1beta1.Input, requiredResources map[string][]res continue } - var processed []unstructured.Unstructured + toFieldPath := "" + if config.ToFieldPath != nil { + toFieldPath = *config.ToFieldPath + } + switch config.GetType() { case v1beta1.EnvironmentSourceTypeReference: out, err := processSourceByReference(in, config, resources) @@ -154,40 +160,21 @@ func getSelectedEnvConfigs(in *v1beta1.Input, requiredResources map[string][]res if out == nil { continue } - processed = []unstructured.Unstructured{*out} + envConfigs[toFieldPath] = append(envConfigs[toFieldPath], *out) case v1beta1.EnvironmentSourceTypeSelector: out, err := processEnvironmentSource(config, resources) if err != nil { return nil, errors.Wrapf(err, "cannot process environment config %q by selector", extraResName) } - processed = out - } - - if err := moveDataToFieldPath(config.ToFieldPath, processed); err != nil { - return nil, err + if len(out) > 0 { + envConfigs[toFieldPath] = append(envConfigs[toFieldPath], out...) + } } - envConfigs = append(envConfigs, processed...) } return envConfigs, nil } -func moveDataToFieldPath(fieldPath *string, envConfigs []unstructured.Unstructured) error { - if fieldPath == nil { - return nil - } - - for _, e := range envConfigs { - data := e.Object["data"] - delete(e.Object, "data") - - if err := fieldpath.Pave(e.Object).SetValue("data."+*fieldPath, data); err != nil { - return errors.Errorf("Unable to move environment to target path '%s'", *fieldPath) - } - } - return nil -} - func processEnvironmentSource(config v1beta1.EnvironmentSource, resources []resource.Required) ([]unstructured.Unstructured, error) { out := make([]unstructured.Unstructured, 0) selector := config.Selector @@ -373,15 +360,23 @@ func buildRequirements(in *v1beta1.Input, xr *resource.Composite) (*fnv1.Require return &fnv1.Requirements{Resources: resources}, nil } -func mergeEnvConfigsData(configs []unstructured.Unstructured) (map[string]any, error) { +func mergeEnvConfigsData(configsByField map[string][]unstructured.Unstructured) (map[string]interface{}, error) { merged := map[string]any{} - for _, c := range configs { - data := map[string]any{} - if err := fieldpath.Pave(c.Object).GetValueInto("data", &data); err != nil { - return nil, errors.Wrapf(err, "cannot get data from environment config %q", c.GetName()) - } + for fieldPath, configs := range configsByField { + for _, c := range configs { + data := map[string]any{} + if fieldPath != "" { + if err := fieldpath.Pave(data).SetValue(fieldPath, c.Object["data"]); err != nil { + return nil, errors.Errorf("cannot get data from environment config %s into path %q", c.GetName(), fieldPath) + } + } else { + if err := fieldpath.Pave(c.Object).GetValueInto("data", &data); err != nil { + return nil, errors.Wrapf(err, "cannot get data from environment config %q", c.GetName()) + } + } - merged = mergeMaps(merged, data) + merged = mergeMaps(merged, data) + } } return merged, nil } From 192feed6178cf9dd2823a1dbe1906a9f95bdd563 Mon Sep 17 00:00:00 2001 From: Amund Tenstad Date: Thu, 9 Apr 2026 17:08:12 +0200 Subject: [PATCH 3/3] fix: interface -> any Signed-off-by: Amund Tenstad --- fn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fn.go b/fn.go index 255d461..d8572af 100644 --- a/fn.go +++ b/fn.go @@ -360,7 +360,7 @@ func buildRequirements(in *v1beta1.Input, xr *resource.Composite) (*fnv1.Require return &fnv1.Requirements{Resources: resources}, nil } -func mergeEnvConfigsData(configsByField map[string][]unstructured.Unstructured) (map[string]interface{}, error) { +func mergeEnvConfigsData(configsByField map[string][]unstructured.Unstructured) (map[string]any, error) { merged := map[string]any{} for fieldPath, configs := range configsByField { for _, c := range configs {