Skip to content

Commit 9c47d51

Browse files
authored
Merge pull request #128 from stevendborrelli/write-context
Function Context support
2 parents 2c4bc6b + f01d533 commit 9c47d51

File tree

11 files changed

+462
-4
lines changed

11 files changed

+462
-4
lines changed

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,44 @@ example:
166166
{{- end }}
167167
```
168168

169+
### Writing to the Context
170+
171+
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.
172+
173+
```yaml
174+
---
175+
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
176+
kind: Context
177+
data:
178+
region: {{ $spec.region }}
179+
id: field
180+
array:
181+
- "1"
182+
- "2"
183+
```
184+
185+
To update Context data, match an existing key. For example, [function-environment-configs](https://github.com/crossplane-contrib/function-environment-configs)
186+
stores data under the key `apiextensions.crossplane.io/environment`.
187+
188+
In this case, Environment fields `update` and `nestedEnvUpdate.hello` would be updated with new values.
189+
190+
```yaml
191+
---
192+
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
193+
kind: Context
194+
data:
195+
"apiextensions.crossplane.io/environment":
196+
kind: Environment
197+
apiVersion: internal.crossplane.io/v1alpha1
198+
update: environment
199+
nestedEnvUpdate:
200+
hello: world
201+
otherContextData:
202+
test: field
203+
```
204+
205+
For more information, see the example in [context](example/context).
206+
169207
## Additional functions
170208

171209
| Name | Description |

context.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package main
2+
3+
import (
4+
"dario.cat/mergo"
5+
"github.com/crossplane/function-sdk-go/errors"
6+
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
7+
)
8+
9+
// MergeContext merges existing Context with new values provided
10+
func (f *Function) MergeContext(req *fnv1beta1.RunFunctionRequest, val map[string]interface{}) (map[string]interface{}, error) {
11+
mergedContext := req.GetContext().AsMap()
12+
if len(val) == 0 {
13+
return mergedContext, nil
14+
}
15+
if err := mergo.Merge(&mergedContext, val, mergo.WithOverride); err != nil {
16+
return mergedContext, errors.Wrapf(err, "cannot merge data %T", req)
17+
}
18+
return mergedContext, nil
19+
}

context_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/crossplane/crossplane-runtime/pkg/logging"
7+
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
8+
"github.com/crossplane/function-sdk-go/resource"
9+
"github.com/google/go-cmp/cmp"
10+
"github.com/google/go-cmp/cmp/cmpopts"
11+
"google.golang.org/protobuf/testing/protocmp"
12+
)
13+
14+
func TestMergeContext(t *testing.T) {
15+
type args struct {
16+
val map[string]interface{}
17+
req *fnv1beta1.RunFunctionRequest
18+
}
19+
type want struct {
20+
us map[string]any
21+
err error
22+
}
23+
24+
cases := map[string]struct {
25+
reason string
26+
args args
27+
want want
28+
}{
29+
"NoContextAtKey": {
30+
reason: "When there is no existing context data at the key to merge, return the value",
31+
args: args{
32+
req: &fnv1beta1.RunFunctionRequest{
33+
Context: nil,
34+
},
35+
val: map[string]interface{}{"hello": "world"},
36+
},
37+
want: want{
38+
us: map[string]interface{}{"hello": "world"},
39+
err: nil,
40+
},
41+
},
42+
"SuccessfulMerge": {
43+
reason: "Confirm that keys are merged with source overwriting destination",
44+
args: args{
45+
req: &fnv1beta1.RunFunctionRequest{
46+
Context: resource.MustStructJSON(`{"apiextensions.crossplane.io/environment":{"complex":{"a":"b","c":{"d":"e","f":"1","overWrite": "fromContext"}}}}`),
47+
},
48+
val: map[string]interface{}{
49+
"newKey": "newValue",
50+
"apiextensions.crossplane.io/environment": map[string]any{
51+
"complex": map[string]any{
52+
"c": map[string]any{
53+
"overWrite": "fromFunction",
54+
},
55+
},
56+
},
57+
},
58+
},
59+
want: want{
60+
us: map[string]interface{}{
61+
"apiextensions.crossplane.io/environment": map[string]any{
62+
"complex": map[string]any{
63+
"a": "b",
64+
"c": map[string]any{
65+
"d": "e",
66+
"f": "1",
67+
"overWrite": "fromFunction",
68+
},
69+
},
70+
},
71+
"newKey": "newValue"},
72+
err: nil,
73+
},
74+
},
75+
}
76+
for name, tc := range cases {
77+
t.Run(name, func(t *testing.T) {
78+
f := &Function{
79+
log: logging.NewNopLogger(),
80+
}
81+
rsp, err := f.MergeContext(tc.args.req, tc.args.val)
82+
83+
if diff := cmp.Diff(tc.want.us, rsp, protocmp.Transform()); diff != "" {
84+
t.Errorf("%s\nf.MergeContext(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
85+
}
86+
87+
if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
88+
t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff)
89+
}
90+
})
91+
}
92+
93+
}

example/context/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Writing to the Function Context
2+
3+
function-go-templating can write to the Function Context
4+
5+
## Testing This Function Locally
6+
7+
You can run your function locally and test it using [`crossplane render`](https://docs.crossplane.io/latest/cli/command-reference/#render)
8+
with these example manifests.
9+
10+
```shell
11+
crossplane render \
12+
--extra-resources environmentConfigs.yaml \
13+
--include-context \
14+
xr.yaml composition.yaml functions.yaml
15+
```
16+
17+
Will produce an output like:
18+
19+
```shell
20+
---
21+
apiVersion: example.crossplane.io/v1
22+
kind: XR
23+
metadata:
24+
name: example-xr
25+
status:
26+
conditions:
27+
- lastTransitionTime: "2024-01-01T00:00:00Z"
28+
reason: Available
29+
status: "True"
30+
type: Ready
31+
fromEnv: e
32+
---
33+
apiVersion: render.crossplane.io/v1beta1
34+
fields:
35+
apiextensions.crossplane.io/environment:
36+
apiVersion: internal.crossplane.io/v1alpha1
37+
array:
38+
- "1"
39+
- "2"
40+
complex:
41+
a: b
42+
c:
43+
d: e
44+
f: "1"
45+
kind: Environment
46+
nestedEnvUpdate:
47+
hello: world
48+
update: environment
49+
newkey:
50+
hello: world
51+
other-context-key:
52+
complex:
53+
a: b
54+
c:
55+
d: e
56+
f: "1"
57+
kind: Context
58+
```
59+
60+
## Debugging This Function
61+
62+
First we need to run the command in debug mode. In a terminal Window Run:
63+
64+
```shell
65+
# Run the function locally
66+
$ go run . --insecure --debug
67+
```
68+
69+
Next, set the go-templating function `render.crossplane.io/runtime: Development` annotation so that
70+
`crossplane render` communicates with the local process instead of downloading an image:
71+
72+
```yaml
73+
apiVersion: pkg.crossplane.io/v1beta1
74+
kind: Function
75+
metadata:
76+
name: crossplane-contrib-function-go-templating
77+
annotations:
78+
render.crossplane.io/runtime: Development
79+
spec:
80+
package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.6.0
81+
```
82+
83+
While the function is running in one terminal, open another terminal window and run `crossplane render`.
84+
The function should output debug-level logs in the terminal.

example/context/composition.yaml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
apiVersion: apiextensions.crossplane.io/v1
2+
kind: Composition
3+
metadata:
4+
name: go-template-context.example.crossplane.io
5+
spec:
6+
compositeTypeRef:
7+
apiVersion: example.crossplane.io/v1
8+
kind: XR
9+
mode: Pipeline
10+
pipeline:
11+
- step: environmentConfigs
12+
functionRef:
13+
name: crossplane-contrib-function-environment-configs
14+
input:
15+
apiVersion: environmentconfigs.fn.crossplane.io/v1beta1
16+
kind: Input
17+
spec:
18+
environmentConfigs:
19+
- type: Reference
20+
ref:
21+
name: example-config
22+
- step: go-templating-update-context
23+
functionRef:
24+
name: crossplane-contrib-function-go-templating
25+
input:
26+
apiVersion: gotemplating.fn.crossplane.io/v1beta1
27+
kind: GoTemplate
28+
source: Inline
29+
inline:
30+
template: |
31+
---
32+
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
33+
kind: Context
34+
data:
35+
# update existing EnvironmentConfig by using the "apiextensions.crossplane.io/environment" key
36+
"apiextensions.crossplane.io/environment":
37+
kind: Environment
38+
apiVersion: internal.crossplane.io/v1alpha1
39+
update: environment
40+
nestedEnvUpdate:
41+
hello: world
42+
array:
43+
- "1"
44+
- "2"
45+
# read existing context and move it to another key
46+
"other-context-key":
47+
complex: {{ index .context "apiextensions.crossplane.io/environment" "complex" | toYaml | nindent 6 }}
48+
# Create a new Context key and populate it with data
49+
newkey:
50+
hello: world
51+
---
52+
apiVersion: example.crossplane.io/v1
53+
kind: XR
54+
status:
55+
fromEnv: {{ index .context "apiextensions.crossplane.io/environment" "complex" "c" "d" }}
56+
- step: automatically-detect-ready-composed-resources
57+
functionRef:
58+
name: crossplane-contrib-function-auto-ready
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: apiextensions.crossplane.io/v1alpha1
2+
kind: EnvironmentConfig
3+
metadata:
4+
name: example-config
5+
data:
6+
complex:
7+
a: b
8+
c:
9+
d: e
10+
f: "1"

example/context/functions.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
apiVersion: pkg.crossplane.io/v1beta1
3+
kind: Function
4+
metadata:
5+
name: crossplane-contrib-function-environment-configs
6+
spec:
7+
# This is ignored when using the Development runtime.
8+
package: xpkg.upbound.io/crossplane-contrib/function-environment-configs:v0.0.7
9+
---
10+
apiVersion: pkg.crossplane.io/v1beta1
11+
kind: Function
12+
metadata:
13+
name: crossplane-contrib-function-go-templating
14+
annotations:
15+
# This tells crossplane beta render to connect to the function locally.
16+
render.crossplane.io/runtime: Development
17+
spec:
18+
package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.6.0
19+
---
20+
apiVersion: pkg.crossplane.io/v1beta1
21+
kind: Function
22+
metadata:
23+
name: crossplane-contrib-function-auto-ready
24+
spec:
25+
package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.2.1

example/context/xr.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
apiVersion: example.crossplane.io/v1
2+
kind: XR
3+
metadata:
4+
name: example-xr
5+
spec: {}

example/context/xrd.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
apiVersion: apiextensions.crossplane.io/v1
3+
kind: CompositeResourceDefinition
4+
metadata:
5+
name: xrs.example.crossplane.io
6+
spec:
7+
group: example.crossplane.io
8+
names:
9+
kind: XR
10+
plural: xrs
11+
connectionSecretKeys:
12+
- test
13+
versions:
14+
- name: v1
15+
served: true
16+
referenceable: true
17+
schema:
18+
openAPIV3Schema:
19+
type: object
20+
properties:
21+
status:
22+
type: object
23+
properties:
24+
fromEnv:
25+
type: string

0 commit comments

Comments
 (0)