Skip to content

Commit

Permalink
Merge pull request #128 from stevendborrelli/write-context
Browse files Browse the repository at this point in the history
Function Context support
  • Loading branch information
phisco authored Oct 8, 2024
2 parents 2c4bc6b + f01d533 commit 9c47d51
Show file tree
Hide file tree
Showing 11 changed files with 462 additions and 4 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,44 @@ example:
{{- end }}
```

### Writing to the Context

This function can write to the Composition [Context](https://docs.crossplane.io/latest/concepts/compositions/#function-pipeline-context). Subsequent pipeline steps will be able to access the data.

```yaml
---
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: Context
data:
region: {{ $spec.region }}
id: field
array:
- "1"
- "2"
```

To update Context data, match an existing key. For example, [function-environment-configs](https://github.com/crossplane-contrib/function-environment-configs)
stores data under the key `apiextensions.crossplane.io/environment`.

In this case, Environment fields `update` and `nestedEnvUpdate.hello` would be updated with new values.

```yaml
---
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: Context
data:
"apiextensions.crossplane.io/environment":
kind: Environment
apiVersion: internal.crossplane.io/v1alpha1
update: environment
nestedEnvUpdate:
hello: world
otherContextData:
test: field
```

For more information, see the example in [context](example/context).

## Additional functions

| Name | Description |
Expand Down
19 changes: 19 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package main

import (
"dario.cat/mergo"
"github.com/crossplane/function-sdk-go/errors"
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
)

// MergeContext merges existing Context with new values provided
func (f *Function) MergeContext(req *fnv1beta1.RunFunctionRequest, val map[string]interface{}) (map[string]interface{}, error) {
mergedContext := req.GetContext().AsMap()
if len(val) == 0 {
return mergedContext, nil
}
if err := mergo.Merge(&mergedContext, val, mergo.WithOverride); err != nil {
return mergedContext, errors.Wrapf(err, "cannot merge data %T", req)
}
return mergedContext, nil
}
93 changes: 93 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package main

import (
"testing"

"github.com/crossplane/crossplane-runtime/pkg/logging"
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
"github.com/crossplane/function-sdk-go/resource"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/testing/protocmp"
)

func TestMergeContext(t *testing.T) {
type args struct {
val map[string]interface{}
req *fnv1beta1.RunFunctionRequest
}
type want struct {
us map[string]any
err error
}

cases := map[string]struct {
reason string
args args
want want
}{
"NoContextAtKey": {
reason: "When there is no existing context data at the key to merge, return the value",
args: args{
req: &fnv1beta1.RunFunctionRequest{
Context: nil,
},
val: map[string]interface{}{"hello": "world"},
},
want: want{
us: map[string]interface{}{"hello": "world"},
err: nil,
},
},
"SuccessfulMerge": {
reason: "Confirm that keys are merged with source overwriting destination",
args: args{
req: &fnv1beta1.RunFunctionRequest{
Context: resource.MustStructJSON(`{"apiextensions.crossplane.io/environment":{"complex":{"a":"b","c":{"d":"e","f":"1","overWrite": "fromContext"}}}}`),
},
val: map[string]interface{}{
"newKey": "newValue",
"apiextensions.crossplane.io/environment": map[string]any{
"complex": map[string]any{
"c": map[string]any{
"overWrite": "fromFunction",
},
},
},
},
},
want: want{
us: map[string]interface{}{
"apiextensions.crossplane.io/environment": map[string]any{
"complex": map[string]any{
"a": "b",
"c": map[string]any{
"d": "e",
"f": "1",
"overWrite": "fromFunction",
},
},
},
"newKey": "newValue"},
err: nil,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
f := &Function{
log: logging.NewNopLogger(),
}
rsp, err := f.MergeContext(tc.args.req, tc.args.val)

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

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)
}
})
}

}
84 changes: 84 additions & 0 deletions example/context/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Writing to the Function Context

function-go-templating can write to the Function Context

## Testing This Function Locally

You can run your function locally and test it using [`crossplane render`](https://docs.crossplane.io/latest/cli/command-reference/#render)
with these example manifests.

```shell
crossplane render \
--extra-resources environmentConfigs.yaml \
--include-context \
xr.yaml composition.yaml functions.yaml
```

Will produce an output like:

```shell
---
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: example-xr
status:
conditions:
- lastTransitionTime: "2024-01-01T00:00:00Z"
reason: Available
status: "True"
type: Ready
fromEnv: e
---
apiVersion: render.crossplane.io/v1beta1
fields:
apiextensions.crossplane.io/environment:
apiVersion: internal.crossplane.io/v1alpha1
array:
- "1"
- "2"
complex:
a: b
c:
d: e
f: "1"
kind: Environment
nestedEnvUpdate:
hello: world
update: environment
newkey:
hello: world
other-context-key:
complex:
a: b
c:
d: e
f: "1"
kind: Context
```

## Debugging This Function

First we need to run the command in debug mode. In a terminal Window Run:

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

Next, set the go-templating function `render.crossplane.io/runtime: Development` annotation so that
`crossplane render` communicates with the local process instead of downloading an image:

```yaml
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: crossplane-contrib-function-go-templating
annotations:
render.crossplane.io/runtime: Development
spec:
package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.6.0
```
While the function is running in one terminal, open another terminal window and run `crossplane render`.
The function should output debug-level logs in the terminal.
58 changes: 58 additions & 0 deletions example/context/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: go-template-context.example.crossplane.io
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: XR
mode: Pipeline
pipeline:
- step: environmentConfigs
functionRef:
name: crossplane-contrib-function-environment-configs
input:
apiVersion: environmentconfigs.fn.crossplane.io/v1beta1
kind: Input
spec:
environmentConfigs:
- type: Reference
ref:
name: example-config
- step: go-templating-update-context
functionRef:
name: crossplane-contrib-function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: |
---
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: Context
data:
# update existing EnvironmentConfig by using the "apiextensions.crossplane.io/environment" key
"apiextensions.crossplane.io/environment":
kind: Environment
apiVersion: internal.crossplane.io/v1alpha1
update: environment
nestedEnvUpdate:
hello: world
array:
- "1"
- "2"
# read existing context and move it to another key
"other-context-key":
complex: {{ index .context "apiextensions.crossplane.io/environment" "complex" | toYaml | nindent 6 }}
# Create a new Context key and populate it with data
newkey:
hello: world
---
apiVersion: example.crossplane.io/v1
kind: XR
status:
fromEnv: {{ index .context "apiextensions.crossplane.io/environment" "complex" "c" "d" }}
- step: automatically-detect-ready-composed-resources
functionRef:
name: crossplane-contrib-function-auto-ready
10 changes: 10 additions & 0 deletions example/context/environmentConfigs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: apiextensions.crossplane.io/v1alpha1
kind: EnvironmentConfig
metadata:
name: example-config
data:
complex:
a: b
c:
d: e
f: "1"
25 changes: 25 additions & 0 deletions example/context/functions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: crossplane-contrib-function-environment-configs
spec:
# This is ignored when using the Development runtime.
package: xpkg.upbound.io/crossplane-contrib/function-environment-configs:v0.0.7
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: crossplane-contrib-function-go-templating
annotations:
# This tells crossplane beta render to connect to the function locally.
render.crossplane.io/runtime: Development
spec:
package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.6.0
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: crossplane-contrib-function-auto-ready
spec:
package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.2.1
5 changes: 5 additions & 0 deletions example/context/xr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: example-xr
spec: {}
25 changes: 25 additions & 0 deletions example/context/xrd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xrs.example.crossplane.io
spec:
group: example.crossplane.io
names:
kind: XR
plural: xrs
connectionSecretKeys:
- test
versions:
- name: v1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
fromEnv:
type: string
Loading

0 comments on commit 9c47d51

Please sign in to comment.