diff --git a/example/functions/getCredentialData/README.md b/example/functions/getCredentialData/README.md new file mode 100644 index 0000000..97002a8 --- /dev/null +++ b/example/functions/getCredentialData/README.md @@ -0,0 +1,29 @@ +# getCredentialData +The getCredentialData function is a utility function used to facilitate the retrieval of a function credential. Upon successful retrieval, the function returns the data of the credential. If the credential cannot be located or is unreachable, it returns nil. + +## Testing This Function Locally + +You can run your function locally and test it with [`crossplane render`](https://docs.crossplane.io/v1.18/cli/command-reference/#render/) + +```shell {copy-lines="1-3"} +crossplane render xr.yaml composition.yaml functions.yaml \ + --function-credentials=credentials.yaml \ + --include-context +--- +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +status: + conditions: + - lastTransitionTime: "2024-01-01T00:00:00Z" + reason: Available + status: "True" + type: Ready +--- +apiVersion: render.crossplane.io/v1beta1 +fields: + password: bar + username: foo +kind: Context +``` diff --git a/example/functions/getCredentialData/composition.yaml b/example/functions/getCredentialData/composition.yaml new file mode 100644 index 0000000..fc3c482 --- /dev/null +++ b/example/functions/getCredentialData/composition.yaml @@ -0,0 +1,31 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: example-function-get-credential-data +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1beta1 + kind: XR + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + credentials: + - name: foo-creds + secretRef: + name: foo-creds + namespace: default + source: Secret + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 + kind: Context + data: + username: {{ ( getCredentialData . "foo-creds" ).username | toString }} + password: {{ ( getCredentialData . "foo-creds" ).password | toString }} diff --git a/example/functions/getCredentialData/credentials.yaml b/example/functions/getCredentialData/credentials.yaml new file mode 100644 index 0000000..2b4f04e --- /dev/null +++ b/example/functions/getCredentialData/credentials.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: foo-creds + namespace: default +type: Opaque +data: + username: Zm9v # foo + password: YmFy # bar diff --git a/example/functions/getCredentialData/functions.yaml b/example/functions/getCredentialData/functions.yaml new file mode 100644 index 0000000..62dcb22 --- /dev/null +++ b/example/functions/getCredentialData/functions.yaml @@ -0,0 +1,8 @@ +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-go-templating + annotations: + render.crossplane.io/runtime: Development +spec: + package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.8.0 diff --git a/example/functions/getCredentialData/xr.yaml b/example/functions/getCredentialData/xr.yaml new file mode 100644 index 0000000..2221440 --- /dev/null +++ b/example/functions/getCredentialData/xr.yaml @@ -0,0 +1,5 @@ +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +spec: {} diff --git a/function_maps.go b/function_maps.go index f4deb5b..bba45f3 100644 --- a/function_maps.go +++ b/function_maps.go @@ -12,7 +12,10 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/fieldpath" "github.com/crossplane/function-sdk-go/errors" + fnv1 "github.com/crossplane/function-sdk-go/proto/v1" + "google.golang.org/protobuf/encoding/protojson" "gopkg.in/yaml.v3" + "k8s.io/apimachinery/pkg/util/json" ) const recursionMaxNums = 1000 @@ -26,6 +29,7 @@ var funcMaps = []template.FuncMap{ "setResourceNameAnnotation": setResourceNameAnnotation, "getComposedResource": getComposedResource, "getCompositeResource": getCompositeResource, + "getCredentialData": getCredentialData, }, } @@ -130,3 +134,34 @@ func getCompositeResource(req map[string]any) map[string]any { return cr } + +func getCredentialData(mReq map[string]any, credName string) map[string][]byte { + req, err := convertFromMap(mReq) + if err != nil { + return nil + } + + var data map[string][]byte + switch req.GetCredentials()[credName].GetSource().(type) { + case *fnv1.Credentials_CredentialData: + data = req.GetCredentials()[credName].GetCredentialData().GetData() + default: + return nil + } + + return data +} + +func convertFromMap(mReq map[string]any) (*fnv1.RunFunctionRequest, error) { + jReq, err := json.Marshal(&mReq) + if err != nil { + return nil, errors.Wrap(err, "cannot marshal map[string]any to json") + } + + req := &fnv1.RunFunctionRequest{} + if err := protojson.Unmarshal(jReq, req); err != nil { + return nil, errors.Wrap(err, "cannot unmarshal request from json to proto") + } + + return req, nil +} diff --git a/function_maps_test.go b/function_maps_test.go index ef3777f..f4defa1 100644 --- a/function_maps_test.go +++ b/function_maps_test.go @@ -10,6 +10,7 @@ import ( "google.golang.org/protobuf/testing/protocmp" v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + fnv1 "github.com/crossplane/function-sdk-go/proto/v1" ) func Test_fromYaml(t *testing.T) { @@ -475,3 +476,62 @@ func Test_getCompositeResource(t *testing.T) { }) } } + +func Test_getCredentialData(t *testing.T) { + type args struct { + req *fnv1.RunFunctionRequest + } + + type want struct { + data map[string][]byte + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "RetrieveFunctionCredential": { + reason: "Should successfully retrieve the function credential", + args: args{ + req: &fnv1.RunFunctionRequest{ + Credentials: map[string]*fnv1.Credentials{ + "foo-creds": { + Source: &fnv1.Credentials_CredentialData{ + CredentialData: &fnv1.CredentialData{ + Data: map[string][]byte{ + "password": []byte("secret"), + }, + }, + }, + }, + }, + }, + }, + want: want{ + data: map[string][]byte{ + "password": []byte("secret"), + }, + }, + }, + "FunctionCredentialNotFound": { + reason: "Should return nil if the function credential is not found", + args: args{ + req: &fnv1.RunFunctionRequest{ + Credentials: map[string]*fnv1.Credentials{}, + }, + }, + want: want{data: nil}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + req, _ := convertToMap(tc.args.req) + got := getCredentialData(req, "foo-creds") + if diff := cmp.Diff(tc.want.data, got); diff != "" { + t.Errorf("%s\ngetCredentialData(...): -want data, +got data:\n%s", tc.reason, diff) + } + }) + } +}