Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ spec:
spec:
extraResources:
- kind: XCluster
into: XCluster
toFieldPath: XCluster
apiVersion: example.crossplane.io/v1
type: Selector
selector:
Expand Down
4 changes: 2 additions & 2 deletions example/composition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ spec:
spec:
extraResources:
- kind: EnvironmentConfig
into: envConfs
toFieldPath: envConfs
apiVersion: apiextensions.crossplane.io/v1alpha1
type: Selector
selector:
Expand All @@ -28,7 +28,7 @@ spec:
type: Value
value: cluster
- kind: XCluster
into: XCluster
toFieldPath: XCluster
apiVersion: example.crossplane.io/v1
type: Selector
selector:
Expand Down
66 changes: 39 additions & 27 deletions fn.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package main

import (
"context"
"encoding/json"
"reflect"
"sort"

"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

Expand All @@ -33,6 +31,11 @@ type Function struct {
log logging.Logger
}

type FetchedResult struct {
source v1beta1.ResourceSource
resources []interface{}
}

// RunFunction runs the Function.
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
f.log.Info("Running function", "tag", req.GetMeta().GetTag())
Expand Down Expand Up @@ -88,27 +91,20 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest)
return rsp, nil
}

// For now cheaply convert to JSON for serializing.
//
// TODO(reedjosh): look into resources.AsStruct or simlar since unsturctured k8s objects are already almost json.
// structpb.NewList(v []interface{}) should create an array like.
// Combining this and similar structures from the structpb lib should should be done to create
// a map[string][object] container into which the found extra resources can be dumped.
//
// The found extra resources should then be directly marhsal-able via:
// obj := &unstructured.Unstructured{}
// obj.MarshalJSON()
b, err := json.Marshal(verifiedExtras)
if err != nil {
response.Fatal(rsp, errors.Errorf("cannot marshal %T: %w", verifiedExtras, err))
return rsp, nil
out := &unstructured.Unstructured{Object: map[string]interface{}{}}
for _, extras := range verifiedExtras {
if err := fieldpath.Pave(out.Object).SetValue(extras.source.ToFieldPath, extras.resources); err != nil {
response.Fatal(rsp, errors.Wrapf(err, "cannot set nested field path %q", extras.source.ToFieldPath))
return rsp, nil
}
}
s := &structpb.Struct{}
err = protojson.Unmarshal(b, s)

s, err := resource.AsStruct(out)
if err != nil {
response.Fatal(rsp, errors.Errorf("cannot unmarshal %T into %T: %w", extraResources, s, err))
response.Fatal(rsp, errors.Wrap(err, "cannot convert unstructured to protobuf Struct well-known type"))
return rsp, nil
}

response.SetContextKey(rsp, FunctionContextKeyExtraResources, structpb.NewStructValue(s))

return rsp, nil
Expand All @@ -119,7 +115,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest)
func buildRequirements(in *v1beta1.Input, xr *resource.Composite) (*fnv1.Requirements, error) { //nolint:gocyclo // Adding non-nil validations increases function complexity.
extraResources := make(map[string]*fnv1.ResourceSelector, len(in.Spec.ExtraResources))
for _, extraResource := range in.Spec.ExtraResources {
extraResName := extraResource.Into
extraResName := extraResource.ToFieldPath
switch extraResource.Type {
case v1beta1.ResourceSourceTypeReference, "":
extraResources[extraResName] = &fnv1.ResourceSelector{
Expand Down Expand Up @@ -171,14 +167,15 @@ func buildRequirements(in *v1beta1.Input, xr *resource.Composite) (*fnv1.Require

// Verify Min/Max and sort extra resources by field path within a single kind.
func verifyAndSortExtras(in *v1beta1.Input, extraResources map[string][]resource.Required, //nolint:gocyclo // TODO(reedjosh): refactor
) (cleanedExtras map[string][]unstructured.Unstructured, err error) {
cleanedExtras = make(map[string][]unstructured.Unstructured)
) ([]FetchedResult, error) {
results := []FetchedResult{}
for _, extraResource := range in.Spec.ExtraResources {
extraResName := extraResource.Into
extraResName := extraResource.ToFieldPath
resources, ok := extraResources[extraResName]
if !ok {
return nil, errors.Errorf("cannot find expected extra resource %q", extraResName)
}

switch extraResource.GetType() {
case v1beta1.ResourceSourceTypeReference:
if len(resources) == 0 {
Expand All @@ -190,7 +187,6 @@ func verifyAndSortExtras(in *v1beta1.Input, extraResources map[string][]resource
if len(resources) > 1 {
return nil, errors.Errorf("expected exactly one extra resource %q, got %d", extraResName, len(resources))
}
cleanedExtras[extraResName] = append(cleanedExtras[extraResName], *resources[0].Resource)

case v1beta1.ResourceSourceTypeSelector:
selector := extraResource.Selector
Expand All @@ -203,12 +199,28 @@ func verifyAndSortExtras(in *v1beta1.Input, extraResources map[string][]resource
if selector.MaxMatch != nil && uint64(len(resources)) > *selector.MaxMatch {
resources = resources[:*selector.MaxMatch]
}
for _, r := range resources {
cleanedExtras[extraResName] = append(cleanedExtras[extraResName], *r.Resource)
}

result := FetchedResult{source: extraResource}
for _, r := range resources {
if path := extraResource.FromFieldPath; path != nil {
if *path == "" {
return nil, errors.New("fromFieldPath cannot be empty, omit the field to get the whole object")
}

// Extract part of the object, from `FromFieldPath`.
object, err := fieldpath.Pave(r.Resource.Object).GetValue(*path)
if err != nil {
return nil, err
}
result.resources = append(result.resources, object)
} else {
result.resources = append(result.resources, r.Resource.Object)
}
}
results = append(results, result)
}
return cleanedExtras, nil
return results, nil
}

// Sort extra resources by field path within a single kind.
Expand Down
132 changes: 119 additions & 13 deletions fn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestRunFunction(t *testing.T) {
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"type": "Reference",
"into": "obj-0",
"toFieldPath": "obj-0",
"ref": {
"name": "my-env-config"
}
Expand All @@ -72,7 +72,7 @@ func TestRunFunction(t *testing.T) {
"type": "Reference",
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"into": "obj-1",
"toFieldPath": "obj-1",
"ref": {
"name": "my-second-env-config"
}
Expand All @@ -81,7 +81,7 @@ func TestRunFunction(t *testing.T) {
"type": "Selector",
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"into": "obj-2",
"toFieldPath": "obj-2",
"selector": {
"matchLabels": [
{
Expand All @@ -96,7 +96,7 @@ func TestRunFunction(t *testing.T) {
"type": "Selector",
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"into": "obj-3",
"toFieldPath": "obj-3",
"selector": {
"matchLabels": [
{
Expand All @@ -111,7 +111,7 @@ func TestRunFunction(t *testing.T) {
"type": "Selector",
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"into": "obj-4",
"toFieldPath": "obj-4",
"selector": {
"matchLabels": [
{
Expand All @@ -127,7 +127,7 @@ func TestRunFunction(t *testing.T) {
"kind": "Foo",
"apiVersion": "test.crossplane.io/v1alpha1",
"namespace": "my-namespace",
"into": "obj-5",
"toFieldPath": "obj-5",
"ref": {
"name": "my-foo"
}
Expand All @@ -137,7 +137,7 @@ func TestRunFunction(t *testing.T) {
"kind": "Bar",
"apiVersion": "test.crossplane.io/v1alpha1",
"namespace": "my-namespace",
"into": "obj-6",
"toFieldPath": "obj-6",
"selector": {
"matchLabels": [
{
Expand Down Expand Up @@ -345,7 +345,7 @@ func TestRunFunction(t *testing.T) {
"extraResources": [
{
"type": "Reference",
"into": "obj-0",
"toFieldPath": "obj-0",
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"ref": {
Expand All @@ -354,7 +354,7 @@ func TestRunFunction(t *testing.T) {
},
{
"type": "Reference",
"into": "obj-1",
"toFieldPath": "obj-1",
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"ref": {
Expand All @@ -363,7 +363,7 @@ func TestRunFunction(t *testing.T) {
},
{
"type": "Selector",
"into": "obj-2",
"toFieldPath": "obj-2",
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"selector": {
Expand All @@ -378,7 +378,7 @@ func TestRunFunction(t *testing.T) {
},
{
"type": "Selector",
"into": "obj-3",
"toFieldPath": "obj-3",
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"selector": {
Expand All @@ -393,7 +393,7 @@ func TestRunFunction(t *testing.T) {
},
{
"type": "Selector",
"into": "obj-4",
"toFieldPath": "obj-4",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"kind": "EnvironmentConfig",
"selector": {
Expand Down Expand Up @@ -565,7 +565,7 @@ func TestRunFunction(t *testing.T) {
"extraResources": [
{
"type": "Reference",
"into": "obj-0",
"toFieldPath": "obj-0",
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"ref": {
Expand Down Expand Up @@ -600,6 +600,112 @@ func TestRunFunction(t *testing.T) {
},
},
},
"FromFieldPathAndToFieldPath": {
reason: "The Function should extract from FromFieldPath and put into ToFieldPath.",
args: args{
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "hello"},
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{
"envconfs.names": {
Items: []*fnv1.Resource{
{
Resource: resource.MustStructJSON(`{
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"kind": "EnvironmentConfig",
"metadata": {
"name": "first",
"labels": {
"foo": "bar"
}
}
}`),
},
{
Resource: resource.MustStructJSON(`{
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"kind": "EnvironmentConfig",
"metadata": {
"name": "second",
"labels": {
"foo": "bar"
}
}
}`),
},
},
},
},
Input: resource.MustStructJSON(`{
"apiVersion": "extra-resources.fn.crossplane.io/v1beta1",
"kind": "Input",
"spec": {
"extraResources": [
{
"kind": "EnvironmentConfig",
"apiVersion": "apiextensions.crossplane.io/v1beta1",
"fromFieldPath": "metadata.name",
"toFieldPath": "envconfs.names",
"type": "Selector",
"selector": {
"matchLabels": [
{
"type": "Value",
"key": "foo",
"value": "bar"
}
]
}
}
]
}
}`),
},
},
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{
"envconfs.names": {
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{
"apiextensions.crossplane.io/extra-resources": structpb.NewStructValue(resource.MustStructJSON(`{
"envconfs": {
"names": [
"first",
"second"
]
}
}`)),
},
},
},
},
},
}

for name, tc := range cases {
Expand Down
Loading