Skip to content

Add fieldRef #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ spec:
```

- `shellEnvVars` - an array of environment variables with a
`key` and `value` each.
`key` and `value` each. Also supports reading from Composite fields with `fieldRef`.
- `shellCommand` - a shell command line that can contain pipes
and redirects and calling multiple programs.
- `shellCommandField` - a reference to a field that contains
Expand All @@ -72,9 +72,11 @@ standard error output should be written.

## Examples

This repository includes the following examples
This repository includes the following examples:

- echo
- datadog-dashboard-ids
- fieldRef
- ip-addr-validation

## Example: Obtain Dashboard Ids from Datadog
Expand Down
38 changes: 32 additions & 6 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,28 @@ func addShellEnvVarsFromRef(envVarsRef v1alpha1.ShellEnvVarsRef, shellEnvVars ma
return shellEnvVars, nil
}

func fromValueRef(req *fnv1.RunFunctionRequest, path string) (string, error) {
func fromFieldRef(req *fnv1.RunFunctionRequest, fieldRef v1alpha1.FieldRef) (string, error) {
if fieldRef.Path == "" {
return "", errors.New("path must be set")
}
// Check for context key presence and capture context key and path
contextRegex := regexp.MustCompile(`^context\[(.+?)].(.+)$`)
if match := contextRegex.FindStringSubmatch(path); match != nil {
if match := contextRegex.FindStringSubmatch(fieldRef.Path); match != nil {
if v, ok := request.GetContextKey(req, match[1]); ok {
context := &unstructured.Unstructured{}
if err := resource.AsObject(v.GetStructValue(), context); err != nil {
return "", errors.Wrapf(err, "cannot convert context to %s", v)
}
value, err := fieldpath.Pave(context.Object).GetValue(match[2])
if err != nil {
return "", errors.Wrapf(err, "cannot get context value at %s", match[2])
switch fieldRef.Policy {
case v1alpha1.FieldRefPolicyOptional:
return fieldRef.DefaultValue, nil
case v1alpha1.FieldRefPolicyRequired:
fallthrough
default:
return "", errors.Wrapf(err, "cannot get context value at %s", match[2])
}
}
return fmt.Sprintf("%v", value), nil
}
Expand All @@ -49,12 +59,28 @@ func fromValueRef(req *fnv1.RunFunctionRequest, path string) (string, error) {
if err != nil {
return "", errors.Wrapf(err, "cannot get observed composite resource from %T", req)
}
value, err := oxr.Resource.GetValue(path)
value, err := oxr.Resource.GetValue(fieldRef.Path)
if err != nil {
return "", errors.Wrapf(err, "cannot get observed composite value at %s", path)
switch fieldRef.Policy {
case v1alpha1.FieldRefPolicyOptional:
return fieldRef.DefaultValue, nil
case v1alpha1.FieldRefPolicyRequired:
fallthrough
default:
return "", errors.Wrapf(err, "cannot get observed composite value at %s", fieldRef.Path)
}
}
return fmt.Sprintf("%v", value), nil

}
return "", nil
return fieldRef.DefaultValue, nil
}

// a valueRef behaves like a fieldRef with a Required Policy
func fromValueRef(req *fnv1.RunFunctionRequest, path string) (string, error) {
return fromFieldRef(
req, v1alpha1.FieldRef{
Path: path,
Policy: v1alpha1.FieldRefPolicyRequired,
})
}
270 changes: 270 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"testing"

"github.com/crossplane-contrib/function-shell/input/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
"github.com/crossplane/function-sdk-go/resource"
"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -84,3 +86,271 @@ func TestFromValueRef(t *testing.T) {
}

}

func TestFromFieldRef(t *testing.T) {

type args struct {
req *fnv1.RunFunctionRequest
fieldRef v1alpha1.FieldRef
}

type want struct {
result string
err error
}

cases := map[string]struct {
reason string
args args
want want
}{
"FromCompositeValid": {
reason: "If composite path is valid, it should be returned.",
args: args{
req: &fnv1.RunFunctionRequest{
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{
"apiVersion": "",
"kind": "",
"spec": {
"foo": "bar"
}
}`),
},
},
},
fieldRef: v1alpha1.FieldRef{
Path: "spec.foo",
},
},
want: want{
result: "bar",
err: nil,
},
},
"FromCompositeMissingError": {
reason: "If composite path is invalid and Policy is required, return an error",
args: args{
req: &fnv1.RunFunctionRequest{
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{
"apiVersion": "",
"kind": "",
"spec": {
"bad": "bar"
}
}`),
},
},
},
fieldRef: v1alpha1.FieldRef{
Path: "spec.foo",
Policy: v1alpha1.FieldRefPolicyRequired,
},
},
want: want{
result: "",
err: errors.New("cannot get observed composite value at spec.foo: spec.foo: no such field"),
},
},
"FromCompositeMissingErrorDefaultPolicy": {
reason: "If composite path is invalid and Policy is not set, return an error",
args: args{
req: &fnv1.RunFunctionRequest{
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{
"apiVersion": "",
"kind": "",
"spec": {
"bad": "bar"
}
}`),
},
},
},
fieldRef: v1alpha1.FieldRef{
Path: "spec.foo",
},
},
want: want{
result: "",
err: errors.New("cannot get observed composite value at spec.foo: spec.foo: no such field"),
},
},
"FromCompositeMissingFieldOptionalPolicyDefaultValue": {
reason: "If composite path is invalid and Policy is set to Optional, return DefaultValue",
args: args{
req: &fnv1.RunFunctionRequest{
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{
"apiVersion": "",
"kind": "",
"spec": {
"bad": "bar"
}
}`),
},
},
},
fieldRef: v1alpha1.FieldRef{
DefaultValue: "default",
Path: "spec.foo",
Policy: v1alpha1.FieldRefPolicyOptional,
},
},
want: want{
result: "default",
err: nil,
},
},
"FromCompositeMissingFieldOptionalPolicyNoDefaultValue": {
reason: "If composite path is invalid, Policy is set to Optional, and no defaultValue return empty string",
args: args{
req: &fnv1.RunFunctionRequest{
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{
"apiVersion": "",
"kind": "",
"spec": {
"bad": "bar"
}
}`),
},
},
},
fieldRef: v1alpha1.FieldRef{
Path: "spec.foo",
Policy: v1alpha1.FieldRefPolicyOptional,
},
},
want: want{
result: "",
err: nil,
},
},
"FromContextValid": {
reason: "If context path is valid, it should be returned.",
args: args{
req: &fnv1.RunFunctionRequest{
Context: resource.MustStructJSON(`{
"apiextensions.crossplane.io/foo": {
"bar": "baz"
}
}`),
},
fieldRef: v1alpha1.FieldRef{
Path: "context[apiextensions.crossplane.io/foo].bar",
},
},
want: want{
result: "baz",
err: nil,
},
},
"FromContextMissingError": {
reason: "If context path is invalid and Policy is Required, return an error",
args: args{
req: &fnv1.RunFunctionRequest{
Context: resource.MustStructJSON(`{
"apiextensions.crossplane.io/foo": {
"bar": "baz"
}
}`),
},
fieldRef: v1alpha1.FieldRef{
Path: "context[apiextensions.crossplane.io/foo].bad",
Policy: v1alpha1.FieldRefPolicyRequired,
},
},
want: want{
result: "",
err: errors.New("cannot get context value at bad: bad: no such field"),
},
},
"FromContextMissingErrorDefaultPolicy": {
reason: "If context path is invalid and no Policy is defined, return an error",
args: args{
req: &fnv1.RunFunctionRequest{
Context: resource.MustStructJSON(`{
"apiextensions.crossplane.io/foo": {
"bar": "baz"
}
}`),
},
fieldRef: v1alpha1.FieldRef{
Path: "context[apiextensions.crossplane.io/foo].bad",
},
},
want: want{
result: "",
err: errors.New("cannot get context value at bad: bad: no such field"),
},
},
"FromContextMissingFieldOptionalPolicyNoDefaultValue": {
reason: "If context path is invalid, Policy is Optional, and no DefaultValue return empty string",
args: args{
req: &fnv1.RunFunctionRequest{
Context: resource.MustStructJSON(`{
"apiextensions.crossplane.io/foo": {
"bar": "baz"
}
}`),
},
fieldRef: v1alpha1.FieldRef{
Path: "context[apiextensions.crossplane.io/foo].bad",
Policy: v1alpha1.FieldRefPolicyOptional,
},
},
want: want{
result: "",
err: nil,
},
},
"FromContextMissingFieldOptionalPolicyDefaultValue": {
reason: "If context path is invalid, Policy is Optional, return DefaultValue",
args: args{
req: &fnv1.RunFunctionRequest{
Context: resource.MustStructJSON(`{
"apiextensions.crossplane.io/foo": {
"bar": "baz"
}
}`),
},
fieldRef: v1alpha1.FieldRef{
DefaultValue: "default",
Path: "context[apiextensions.crossplane.io/foo].bad",
Policy: v1alpha1.FieldRefPolicyOptional,
},
},
want: want{
result: "default",
err: nil,
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
result, err := fromFieldRef(tc.args.req, tc.args.fieldRef)

if diff := cmp.Diff(tc.want.result, result, protocmp.Transform()); diff != "" {
t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
}

// deal with internal fieldpath errors that are generated by a private method
if tc.want.err != nil && err != nil {
if diff := cmp.Diff(tc.want.err.Error(), err.Error()); diff != "" {
t.Errorf("%s\nf.RunFunction(...): -want err message, +got err message:\n%s", tc.reason, diff)
}
} else if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff)
}
})
}

}
Loading
Loading