diff --git a/fn.go b/fn.go index deb75c2..d8572af 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] @@ -143,6 +145,12 @@ 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 } + + toFieldPath := "" + if config.ToFieldPath != nil { + toFieldPath = *config.ToFieldPath + } + switch config.GetType() { case v1beta1.EnvironmentSourceTypeReference: out, err := processSourceByReference(in, config, resources) @@ -152,7 +160,7 @@ func getSelectedEnvConfigs(in *v1beta1.Input, requiredResources map[string][]res if out == nil { continue } - envConfigs = append(envConfigs, *out) + envConfigs[toFieldPath] = append(envConfigs[toFieldPath], *out) case v1beta1.EnvironmentSourceTypeSelector: out, err := processEnvironmentSource(config, resources) @@ -160,7 +168,7 @@ func getSelectedEnvConfigs(in *v1beta1.Input, requiredResources map[string][]res return nil, errors.Wrapf(err, "cannot process environment config %q by selector", extraResName) } if len(out) > 0 { - envConfigs = append(envConfigs, out...) + envConfigs[toFieldPath] = append(envConfigs[toFieldPath], out...) } } } @@ -352,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]any, 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 } 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.