Skip to content

feat: integrate function credentials #39

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 1 commit into
base: main
Choose a base branch
from
Open
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 Makefile
Original file line number Diff line number Diff line change
@@ -44,7 +44,7 @@ test: ## Run Code Tests
go test -v -cover .

render: ## Render Examples, Requires make debug first
crossplane beta render \
crossplane \
example/echo/xr.yaml \
example/echo/composition.yaml \
example/echo/functions.yaml
95 changes: 29 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
@@ -15,50 +15,8 @@ is expected to be enhanced with the above pattern.
The `function-shell` accepts commands to run in a shell and it
returns the output to specified fields. It accepts the following parameters:

- `shellEnvVarsRef` - referencing environment variables in the
function-shell Kubernetes pod that were loaded through a
`deploymentRuntimeConfig`. The file MUST be in `JSON` format.
It can be a Kubernetes secret. `shellEnvVarsRef` requires a `name`
for the pod environment variable, and `keys` for the keys Inside
of the JSON formatted pod environment variable that have associated
values.

Example secret:

```json
{
"ENV_FOO": "foo value",
"ENV_BAR": "bar value"
}
```

Example `deploymentRuntimeConfig`:

```yaml
---
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: function-shell
spec:
deploymentTemplate:
spec:
selector: {}
replicas: 1
template:
spec:
containers:
- name: package-runtime
args:
- --debug
env:
- name: DATADOG_SECRET
valueFrom:
secretKeyRef:
key: credentials
name: datadog-secret
```
- `shellCredentialRefs` - referencing function credentials
and the required keys.
- `shellEnvVars` - an array of environment variables with a
`key` and `value` each.
- `shellCommand` - a shell command line that can contain pipes
@@ -81,12 +39,9 @@ This repository includes the following examples

The composition calls the `function-shell` instructing it to obtain dashboard
ids from a [Datadog](https://www.datadoghq.com/) account.
For this, the composition specifies the name of a Kubernetes
pod environment variable called `DATADOG_SECRET`. This environment
variable was populated with the `JSON` of a Kubernetes datadog-secret
through a deploymentRuntimeConfig. The `JSON` includes the
`DATADOG_API_KEY` and `DATADOG_APP_KEY`
keys and their values. The Datadog API endpoint is passed
For this, the composition specifies the name of a function credential called
`DATADOG_SECRET` referencing a Kubernetes secret containing `DATADOG_API_KEY`
and `DATADOG_APP_KEY` keys and their values. The Datadog API endpoint is passed
in a clear text `DATADOG_API_URL` environment variable. The shell command
uses a `curl` to the endpoint with a header that contains the access
credentials. The command output is piped into
@@ -101,15 +56,18 @@ The composition is for illustration purposes only. When using the
you may want to patch function input
from claim and other composition field values.

The `deploymentRuntimeConfig` reads a datadog secret
that looks like below.
Replace `YOUR_API_KEY` and `YOUR_APP_KEY` with your respective keys.

```json
{
"DATADOG_API_KEY": "YOUR_API_KEY",
"DATADOG_APP_KEY": "YOIR_APP_KEY"
}
```yaml
apiVersion: v1
kind: Secret
metadata:
name: datadog-secret
namespace: default
type: Opaque
data:
DATADOG_API_KEY: Zm9v # your api key
DATADOG_APP_KEY: YmFy # your app key
```
```yaml
@@ -125,21 +83,26 @@ spec:
mode: Pipeline
pipeline:
- step: shell
credentials:
- name: DATADOG_SECRET
secretRef:
namespace: default
name: datadog-secret
source: Secret
functionRef:
# When installed through a package manager, use
# name: crossplane-contrib-function-shell
name: function-shell
input:
apiVersion: shell.fn.crossplane.io/v1beta1
kind: Parameters
# Load shellEnvVarsRef from a Kubernetes secret
# through a deploymentRuntimeConfig into the
# function-shell pod.
shellEnvVarsRef:
name: DATADOG_SECRET
keys:
- DATADOG_API_KEY
- DATADOG_APP_KEY
# Load shellCredentialsRef from a Kubernetes secret
# into the function-shell pod.
shellCredentialRefs:
- name: DATADOG_SECRET
keys:
- DATADOG_API_KEY
- DATADOG_APP_KEY
shellEnvVars:
- key: DATADOG_API_URL
value: "https://api.datadoghq.com/api/v1/dashboard"
@@ -296,7 +259,7 @@ go run . --insecure --debug
In Terminal 2

```shell
crossplane beta render \
crossplane \
example/out-of-cluster/xr.yaml \
example/out-of-cluster/composition.yaml \
example/out-of-cluster/functions.yaml
20 changes: 2 additions & 18 deletions env.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,18 @@
package main

import (
"encoding/json"
"fmt"
"os"
"regexp"

"github.com/crossplane-contrib/function-shell/input/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
"github.com/crossplane/function-sdk-go/request"
"github.com/crossplane/function-sdk-go/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func addShellEnvVarsFromRef(envVarsRef v1alpha1.ShellEnvVarsRef, shellEnvVars map[string]string) (map[string]string, error) {
var envVarsData map[string]string

envVars := os.Getenv(envVarsRef.Name)
if err := json.Unmarshal([]byte(envVars), &envVarsData); err != nil {
return shellEnvVars, err
}
for _, key := range envVarsRef.Keys {
shellEnvVars[key] = envVarsData[key]
}
return shellEnvVars, nil
}

func fromValueRef(req *fnv1beta1.RunFunctionRequest, path string) (string, error) {
func fromValueRef(req *fnv1.RunFunctionRequest, path string) (string, error) {
// Check for context key presence and capture context key and path
contextRegex := regexp.MustCompile(`^context\[(.+?)].(.+)$`)
if match := contextRegex.FindStringSubmatch(path); match != nil {
12 changes: 6 additions & 6 deletions env_test.go
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ package main
import (
"testing"

fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
"github.com/crossplane/function-sdk-go/resource"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
@@ -13,7 +13,7 @@ import (
func TestFromValueRef(t *testing.T) {

type args struct {
req *fnv1beta1.RunFunctionRequest
req *fnv1.RunFunctionRequest
path string
}

@@ -30,9 +30,9 @@ func TestFromValueRef(t *testing.T) {
"FromCompositeValid": {
reason: "If composite path is valid, it should be returned.",
args: args{
req: &fnv1beta1.RunFunctionRequest{
Observed: &fnv1beta1.State{
Composite: &fnv1beta1.Resource{
req: &fnv1.RunFunctionRequest{
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{
"apiVersion": "",
"kind": "",
@@ -53,7 +53,7 @@ func TestFromValueRef(t *testing.T) {
"FromContextValid": {
reason: "If composite path is valid, it should be returned.",
args: args{
req: &fnv1beta1.RunFunctionRequest{
req: &fnv1.RunFunctionRequest{
Context: resource.MustStructJSON(`{
"apiextensions.crossplane.io/foo": {
"bar": "baz"
44 changes: 0 additions & 44 deletions example/README.md

This file was deleted.

2 changes: 1 addition & 1 deletion example/aws/functions.yaml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ kind: Function
metadata:
name: function-shell
annotations:
# This tells crossplane beta render to connect to the function locally.
# This tells crossplane render to connect to the function locally.
render.crossplane.io/runtime: Development
spec:
# This is ignored when using the Development runtime.
27 changes: 27 additions & 0 deletions example/datadog-dashboard-ids/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Example manifests

You can run your function locally and test it using `crossplane render`
with these example manifests.

```shell
# Run the function locally
$ go run . --insecure --debug
```

```shell
# Then, in another terminal, call it with these example manifests
$ crossplane render xr.yaml composition.yaml functions.yaml \
--function-credentials=credentials.yaml \
--include-function-results
---
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: example-xr
---
apiVersion: render.crossplane.io/v1beta1
kind: Result
message: I was run with input "Hello world"!
severity: SEVERITY_NORMAL
step: run-the-template
```
26 changes: 14 additions & 12 deletions example/datadog-dashboard-ids/composition.yaml
Original file line number Diff line number Diff line change
@@ -10,28 +10,30 @@ spec:
mode: Pipeline
pipeline:
- step: shell
credentials:
- name: DATADOG_SECRET
secretRef:
namespace: default
name: datadog-secret
source: Secret
functionRef:
# When installed through a package manager, use
# name: crossplane-contrib-function-shell
name: function-shell
input:
apiVersion: shell.fn.crossplane.io/v1beta1
kind: Parameters
# Load shellEnvVarsRef from a Kubernetes secret
# through a deploymentRuntimeConfig into the
# function-shell pod.
shellEnvVarsRef:
name: DATADOG_SECRET
keys:
- DATADOG_API_KEY
- DATADOG_APP_KEY
# Load shellCredentialsRef from a Kubernetes secret
# into the function-shell pod.
shellCredentialRefs:
- name: DATADOG_SECRET
keys:
- credentials
shellEnvVars:
- key: DATADOG_API_URL
value: "https://api.datadoghq.com/api/v1/dashboard"
shellCommand: |
curl -X GET "${DATADOG_API_URL}" \
-H "Accept: application/json" \
-H "DD-API-KEY: ${DATADOG_API_KEY}" \
-H "DD-APPLICATION-KEY: ${DATADOG_APP_KEY}"|jq '.dashboards[] .id'
echo "url=${DATADOG_API_URL}"
echo "credentials=$(echo ${credentials})"
stdoutField: status.atFunction.shell.stdout
stderrField: status.atFunction.shell.stderr
8 changes: 8 additions & 0 deletions example/datadog-dashboard-ids/credentials.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: datadog-secret
namespace: default
type: Opaque
data:
credentials: ewogICJEQVRBRE9HX0FQSV9LRVkiOiAiZm9vIiwKICAiREFUQURPR19BUFBfS0VZIjogImJhciIKfQo=
21 changes: 0 additions & 21 deletions example/datadog-dashboard-ids/deployment-runtime-config.yaml

This file was deleted.

8 changes: 2 additions & 6 deletions example/datadog-dashboard-ids/functions.yaml
Original file line number Diff line number Diff line change
@@ -4,13 +4,9 @@ kind: Function
metadata:
name: function-shell
annotations:
# This tells crossplane beta render to connect to the functi on locally.
# This tells crossplane render to connect to the function locally.
render.crossplane.io/runtime: Development
spec:
# This is ignored when using the Development runtime.
package: package: xpkg.upbound.io/crossplane-contrib/function-shell:v0.3.0
package: xpkg.upbound.io/crossplane-contrib/function-shell:v0.3.0
packagePullPolicy: Always
runtimeConfigRef:
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
name: function-shell
2 changes: 1 addition & 1 deletion example/echo/functions.yaml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ kind: Function
metadata:
name: function-shell
annotations:
# This tells crossplane beta render to connect to the function locally.
# This tells crossplane render to connect to the function locally.
render.crossplane.io/runtime: Development
spec:
# This is ignored when using the Development runtime.
2 changes: 1 addition & 1 deletion example/ip-addr-validation/functions.yaml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ kind: Function
metadata:
name: function-shell
annotations:
# This tells crossplane beta render to connect to the function locally.
# This tells crossplane render to connect to the function locally.
render.crossplane.io/runtime: Development
spec:
# This is ignored when using the Development runtime.
20 changes: 12 additions & 8 deletions fn.go
Original file line number Diff line number Diff line change
@@ -8,21 +8,21 @@ import (
"github.com/crossplane-contrib/function-shell/input/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/logging"
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
"github.com/crossplane/function-sdk-go/request"
"github.com/crossplane/function-sdk-go/response"
"github.com/keegancsmith/shell"
)

// Function returns whatever response you ask it to.
type Function struct {
fnv1beta1.UnimplementedFunctionRunnerServiceServer
fnv1.UnimplementedFunctionRunnerServiceServer

log logging.Logger
}

// RunFunction runs the Function.
func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequest) (*fnv1beta1.RunFunctionResponse, error) {
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
f.log.Info("Running function", "tag", req.GetMeta().GetTag())

rsp := response.To(req, response.DefaultTTL)
@@ -98,11 +98,15 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ
}
}

if len(in.ShellEnvVarsRef.Keys) > 0 {
shellEnvVars, err = addShellEnvVarsFromRef(in.ShellEnvVarsRef, shellEnvVars)
if err != nil {
response.Fatal(rsp, errors.Wrapf(err, "cannot process contents of shellEnvVarsRef %s", in.ShellEnvVarsRef.Name))
return rsp, nil
if len(in.ShellCredentialRefs) > 0 {
for _, credRef := range in.ShellCredentialRefs {
if len(credRef.Keys) > 0 {
shellEnvVars, err = addShellEnvVarsFromCredentialRefs(req, credRef, shellEnvVars)
if err != nil {
response.Fatal(rsp, errors.Wrapf(err, "cannot process contents of shellCredentialRefs %s", credRef.Name))
return rsp, nil
}
}
}
}

69 changes: 36 additions & 33 deletions fn_test.go
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import (
"google.golang.org/protobuf/types/known/durationpb"

"github.com/crossplane/crossplane-runtime/pkg/logging"
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
"github.com/crossplane/function-sdk-go/resource"
"github.com/crossplane/function-sdk-go/response"
)
@@ -19,10 +19,10 @@ func TestRunFunction(t *testing.T) {

type args struct {
ctx context.Context
req *fnv1beta1.RunFunctionRequest
req *fnv1.RunFunctionRequest
}
type want struct {
rsp *fnv1beta1.RunFunctionResponse
rsp *fnv1.RunFunctionResponse
err error
}

@@ -34,21 +34,22 @@ func TestRunFunction(t *testing.T) {
"ResponseIsParametersRequired": {
reason: "The Function should return a fatal result if no input was specified",
args: args{
req: &fnv1beta1.RunFunctionRequest{
Meta: &fnv1beta1.RequestMeta{Tag: "hello"},
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "hello"},
Input: resource.MustStructJSON(`{
"apiVersion": "template.fn.crossplane.io/v1alpha1",
"kind": "Parameters"
}`),
},
},
want: want{
rsp: &fnv1beta1.RunFunctionResponse{
Meta: &fnv1beta1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Results: []*fnv1beta1.Result{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Results: []*fnv1.Result{
{
Severity: fnv1beta1.Severity_SEVERITY_FATAL,
Severity: fnv1.Severity_SEVERITY_FATAL,
Message: "invalid Function input: parameters: Required value: one of ShellCommand or ShellCommandField is required",
Target: fnv1.Target_TARGET_COMPOSITE.Enum(),
},
},
},
@@ -57,8 +58,8 @@ func TestRunFunction(t *testing.T) {
"ResponseIsEmptyShellCommand": {
reason: "The Function should return a response when after a script is run",
args: args{
req: &fnv1beta1.RunFunctionRequest{
Meta: &fnv1beta1.RequestMeta{Tag: "hello"},
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "hello"},
Input: resource.MustStructJSON(`{
"apiVersion": "template.fn.crossplane.io/v1alpha1",
"kind": "Parameters",
@@ -67,12 +68,13 @@ func TestRunFunction(t *testing.T) {
},
},
want: want{
rsp: &fnv1beta1.RunFunctionResponse{
Meta: &fnv1beta1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Results: []*fnv1beta1.Result{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Results: []*fnv1.Result{
{
Severity: fnv1beta1.Severity_SEVERITY_FATAL,
Severity: fnv1.Severity_SEVERITY_FATAL,
Message: "invalid Function input: parameters: Required value: one of ShellCommand or ShellCommandField is required",
Target: fnv1.Target_TARGET_COMPOSITE.Enum(),
},
},
},
@@ -81,8 +83,8 @@ func TestRunFunction(t *testing.T) {
"ResponseIsEcho": {
reason: "The Function should write stdout to the specified field",
args: args{
req: &fnv1beta1.RunFunctionRequest{
Meta: &fnv1beta1.RequestMeta{Tag: "hello"},
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "hello"},
Input: resource.MustStructJSON(`{
"apiVersion": "template.fn.crossplane.io/v1alpha1",
"kind": "Parameters",
@@ -92,10 +94,10 @@ func TestRunFunction(t *testing.T) {
},
},
want: want{
rsp: &fnv1beta1.RunFunctionResponse{
Meta: &fnv1beta1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Desired: &fnv1beta1.State{
Composite: &fnv1beta1.Resource{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{
"apiVersion": "",
"kind": "",
@@ -122,8 +124,8 @@ func TestRunFunction(t *testing.T) {
"ResponseIsErrorWhenShellCommandNotFound": {
reason: "The Function should write to the specified stderr field when the shellCommand is not found",
args: args{
req: &fnv1beta1.RunFunctionRequest{
Meta: &fnv1beta1.RequestMeta{Tag: "hello"},
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "hello"},
Input: resource.MustStructJSON(`{
"apiVersion": "template.fn.crossplane.io/v1alpha1",
"kind": "Parameters",
@@ -134,12 +136,13 @@ func TestRunFunction(t *testing.T) {
},
},
want: want{
rsp: &fnv1beta1.RunFunctionResponse{
Meta: &fnv1beta1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Results: []*fnv1beta1.Result{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Results: []*fnv1.Result{
{
Severity: fnv1beta1.Severity_SEVERITY_FATAL,
Severity: fnv1.Severity_SEVERITY_FATAL,
Message: "shellCmd unkown-shell-command for failed: exit status 127",
Target: fnv1.Target_TARGET_COMPOSITE.Enum(),
},
},
},
@@ -148,8 +151,8 @@ func TestRunFunction(t *testing.T) {
"ResponseIsEchoEnvVar": {
reason: "The Function should accept and use environment variables",
args: args{
req: &fnv1beta1.RunFunctionRequest{
Meta: &fnv1beta1.RequestMeta{Tag: "hello"},
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "hello"},
Input: resource.MustStructJSON(`{
"apiVersion": "template.fn.crossplane.io/v1alpha1",
"kind": "Parameters",
@@ -160,10 +163,10 @@ func TestRunFunction(t *testing.T) {
},
},
want: want{
rsp: &fnv1beta1.RunFunctionResponse{
Meta: &fnv1beta1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Desired: &fnv1beta1.State{
Composite: &fnv1beta1.Resource{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{
"apiVersion": "",
"kind": "",
26 changes: 26 additions & 0 deletions get_credential_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"github.com/crossplane-contrib/function-shell/input/v1alpha1"
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
)

func getCredentialData(req *fnv1.RunFunctionRequest, credName string) map[string][]byte {
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 addShellEnvVarsFromCredentialRefs(req *fnv1.RunFunctionRequest, credentialRefs v1alpha1.ShellCredentialRef, shellEnvVars map[string]string) (map[string]string, error) {
var credentialData = getCredentialData(req, credentialRefs.Name)
for _, key := range credentialRefs.Keys {
shellEnvVars[key] = string(credentialData[key])
}
return shellEnvVars, nil
}
89 changes: 89 additions & 0 deletions get_credential_data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"testing"

"github.com/crossplane-contrib/function-shell/input/v1alpha1"
"github.com/google/go-cmp/cmp"

fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
"github.com/crossplane/function-sdk-go/resource"
)

func TestGetCredentialData(t *testing.T) {
type args struct {
req *fnv1.RunFunctionRequest
}

type want struct {
data map[string]string
}

cases := map[string]struct {
reason string
args args
want want
}{
"RetrieveFunctionCredential": {
reason: "Should successfully retrieve the function credential",
args: args{
req: &fnv1.RunFunctionRequest{
Input: resource.MustStructJSON(`{
"apiVersion": "template.fn.crossplane.io/v1alpha1",
"kind": "Parameters",
"shellCredentialRefs": [{"name": "foo-creds", "keys": ["password"]}],
"shellEnvVars": [{"key": "TEST_ENV_VAR", "value": "foo"}],
"shellCommand": "echo ${TEST_ENV_VAR}",
"stdoutField": "spec.atFunction.shell.stdout"
}`),
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]string{
"password": "secret",
},
},
},
"FunctionCredentialNotFound": {
reason: "Should return nil if the function credential is not found",
args: args{
req: &fnv1.RunFunctionRequest{
Input: resource.MustStructJSON(`{
"apiVersion": "template.fn.crossplane.io/v1alpha1",
"kind": "Parameters",
"shellCredentialRefs": [{"name": "foo-creds", "keys": ["password"]}],
"shellEnvVars": [{"key": "TEST_ENV_VAR", "value": "foo"}],
"shellCommand": "echo ${TEST_ENV_VAR}",
"stdoutField": "spec.atFunction.shell.stdout"
}`),
Credentials: map[string]*fnv1.Credentials{},
},
},
want: want{
data: map[string]string{
"password": "",
},
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, _ := addShellEnvVarsFromCredentialRefs(tc.args.req, v1alpha1.ShellCredentialRef{Name: "foo-creds", Keys: []string{"password"}}, map[string]string{})
if diff := cmp.Diff(tc.want.data, got); diff != "" {
t.Errorf("%s\naddShellEnvVarsFromCredentialRefs(...): -want data, +got data:\n%s", tc.reason, diff)
}
})
}
}
68 changes: 35 additions & 33 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
module github.com/crossplane-contrib/function-shell

go 1.21
go 1.23.0

toolchain go1.21.3
toolchain go1.23.3

require (
github.com/alecthomas/kong v1.6.0
github.com/crossplane/crossplane-runtime v1.15.1
github.com/crossplane/function-sdk-go v0.2.0
github.com/crossplane/crossplane-runtime v1.18.0
github.com/crossplane/function-sdk-go v0.4.0
github.com/google/go-cmp v0.6.0
github.com/keegancsmith/shell v0.0.0-20160208231706-ccb53e0c7c5c
google.golang.org/protobuf v1.36.0
k8s.io/apimachinery v0.29.2
sigs.k8s.io/controller-tools v0.14.0
k8s.io/apimachinery v0.32.1
sigs.k8s.io/controller-tools v0.16.0
)

require (
dario.cat/mergo v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.8.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/go-json-experiment/json v0.0.0-20231013223334-54c864be5b8d // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gobuffalo/flect v1.0.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -44,33 +45,34 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/mod v0.17.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.26.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.67.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.29.1 // indirect
k8s.io/apiextensions-apiserver v0.29.1 // indirect
k8s.io/client-go v0.29.1 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
sigs.k8s.io/controller-runtime v0.17.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
k8s.io/api v0.31.0 // indirect
k8s.io/apiextensions-apiserver v0.31.0 // indirect
k8s.io/client-go v0.31.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/controller-runtime v0.19.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
72 changes: 72 additions & 0 deletions go.sum

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions input/v1alpha1/parameters.go
Original file line number Diff line number Diff line change
@@ -19,9 +19,9 @@ type Parameters struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// shellEnvVarsRef
// shellCredentialRefs
// +optional
ShellEnvVarsRef ShellEnvVarsRef `json:"shellEnvVarsRef,omitempty"`
ShellCredentialRefs []ShellCredentialRef `json:"shellCredentialRefs,omitempty"`

// shellEnvVars
// +optional
@@ -44,15 +44,15 @@ type Parameters struct {
StderrField string `json:"stderrField,omitempty"`
}

type ShellEnvVar struct {
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
ValueRef string `json:"valueRef,omitempty"`
}

type ShellEnvVarsRef struct {
type ShellCredentialRef struct {
// The Key whose value is the secret
Keys []string `json:"keys,omitempty"`
// Name of the enviroment variable
Name string `json:"name,omitempty"`
}

type ShellEnvVar struct {
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
ValueRef string `json:"valueRef,omitempty"`
}
34 changes: 20 additions & 14 deletions input/v1alpha1/zz_generated.deepcopy.go
28 changes: 15 additions & 13 deletions package/input/template.fn.crossplane.io_parameters.yaml
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
controller-gen.kubebuilder.io/version: v0.16.0
name: parameters.template.fn.crossplane.io
spec:
group: template.fn.crossplane.io
@@ -44,6 +44,20 @@ spec:
shellCommandField:
description: shellCmdField
type: string
shellCredentialRefs:
description: shellCredentialRefs
items:
properties:
keys:
description: The Key whose value is the secret
items:
type: string
type: array
name:
description: Name of the enviroment variable
type: string
type: object
type: array
shellEnvVars:
description: shellEnvVars
items:
@@ -56,18 +70,6 @@ spec:
type: string
type: object
type: array
shellEnvVarsRef:
description: shellEnvVarsRef
properties:
keys:
description: The Key whose value is the secret
items:
type: string
type: array
name:
description: Name of the enviroment variable
type: string
type: object
stderrField:
description: stderrField
type: string