Skip to content
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

Function Context support #128

Merged
merged 12 commits into from
Oct 8, 2024
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ env:

jobs:
lint:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -57,7 +57,7 @@ jobs:
version: ${{ env.GOLANGCI_VERSION }}

unit-test:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -75,7 +75,7 @@ jobs:
# those packages to GitHub as a build artifact. The push job downloads those
# artifacts and pushes them as a single multi-platform package.
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
fail-fast: true
matrix:
Expand Down Expand Up @@ -130,7 +130,7 @@ jobs:
# pushes them as a multi-platform package. We only push the package it the
# XPKG_ACCESS_ID and XPKG_TOKEN secrets were provided.
push:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- build
steps:
Expand Down
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
stevendborrelli marked this conversation as resolved.
Show resolved Hide resolved
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: {}
Loading
Loading