Skip to content

Commit

Permalink
Merge pull request #231 from ytsarev/write-to-context
Browse files Browse the repository at this point in the history
Implement writing to Context
  • Loading branch information
Peefy authored Jan 16, 2025
2 parents 5888db3 + 63ab271 commit e67112c
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 2 deletions.
2 changes: 2 additions & 0 deletions examples/default/context/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
run:
crossplane render --verbose xr.yaml composition.yaml functions.yaml -rc
32 changes: 32 additions & 0 deletions examples/default/context/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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 --verbose xr.yaml composition.yaml functions.yaml -rc
---
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:
contextField: contextValue
moreComplexField:
test: field
kind: Context
```
42 changes: 42 additions & 0 deletions examples/default/context/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: function-template-go
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1beta1
kind: XR
mode: Pipeline
pipeline:
- step: normal
functionRef:
name: kcl-function
input:
apiVersion: krm.kcl.dev/v1alpha1
kind: KCLInput
metadata:
annotations:
"krm.kcl.dev/default_ready": "True"
name: basic
spec:
source: |
oxr = option("params").oxr
dxr = {
**oxr
}
context = {
apiVersion: "meta.krm.kcl.dev/v1alpha1"
kind: "Context"
data = {
contextField = "contextValue"
moreComplexField = {
test: "field"
}
}
}
items = [
context
dxr
]
9 changes: 9 additions & 0 deletions examples/default/context/functions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: kcl-function
annotations:
# This tells crossplane render to connect to the function locally.
render.crossplane.io/runtime: Development
spec:
package: xpkg.upbound.io/crossplane-contrib/function-kcl:latest
6 changes: 6 additions & 0 deletions examples/default/context/xr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: example.crossplane.io/v1beta1
kind: XR
metadata:
name: example
spec:
count: 1
22 changes: 21 additions & 1 deletion fn.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"google.golang.org/protobuf/types/known/structpb"
"k8s.io/apimachinery/pkg/runtime"
"kcl-lang.io/krm-kcl/pkg/api"
"kcl-lang.io/krm-kcl/pkg/api/v1alpha1"
Expand Down Expand Up @@ -183,7 +184,8 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest)
extraResources := map[string]*fnv1.ResourceSelector{}
var conditions pkgresource.ConditionResources
var events pkgresource.EventResources
result, err := pkgresource.ProcessResources(dxr, oxr, desired, observed, extraResources, &conditions, &events, in.Spec.Target, resources, &pkgresource.AddResourcesOptions{
contextData := make(map[string]interface{})
result, err := pkgresource.ProcessResources(dxr, oxr, desired, observed, extraResources, &conditions, &events, &contextData, in.Spec.Target, resources, &pkgresource.AddResourcesOptions{
Basename: in.Name,
Data: data,
Overwrite: true,
Expand Down Expand Up @@ -213,6 +215,24 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest)
}
}

if len(contextData) > 0 {
mergedCtx, err := pkgresource.MergeContext(req, contextData)
if err != nil {
response.Fatal(rsp, errors.Wrapf(err, "cannot merge Context"))
return rsp, nil
}
for key, v := range mergedCtx {
vv, err := structpb.NewValue(v)
if err != nil {
response.Fatal(rsp, errors.Wrap(err, "cannot convert value to structpb.Value"))
return rsp, nil
}
f.log.Debug("Updating Composition environment", "key", key, "data", v)
response.SetContextKey(rsp, key, vv)
}

}

log.Debug(fmt.Sprintf("Set %d resource(s) to the desired state", result.MsgCount))
// Set dxr and desired state
log.Debug(fmt.Sprintf("Setting desired XR state to %+v", dxr.Resource))
Expand Down
78 changes: 78 additions & 0 deletions fn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,84 @@ func TestRunFunctionSimple(t *testing.T) {
},
},
},
"SetContext": {
reason: "The Function should be able to set context.",
args: args{
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "set-context"},
Input: resource.MustStructJSON(`{
"apiVersion": "krm.kcl.dev/v1alpha1",
"kind": "KCLInput",
"metadata": {
"name": "basic"
},
"spec": {
"target": "Default",
"source": "items = [{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"Context\"\n data = {contextField: \"contextValue\"}\n}]"
}
}`),
},
},
want: want{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "set-context", Ttl: durationpb.New(response.DefaultTTL)},
Results: []*fnv1.Result{},
Context: resource.MustStructJSON(
`{
"contextField": "contextValue"
}`,
),
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{"apiVersion":"","kind":""}`),
},
Resources: map[string]*fnv1.Resource{},
},
},
},
},
"MergeContext": {
reason: "The Function should be able to set context merging with the input one.",
args: args{
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "merge-context"},
Input: resource.MustStructJSON(`{
"apiVersion": "krm.kcl.dev/v1alpha1",
"kind": "KCLInput",
"metadata": {
"name": "basic"
},
"spec": {
"target": "Default",
"source": "items = [{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"Context\"\n data = {contextField: \"contextValue\"}\n}]"
}
}`),
Context: resource.MustStructJSON(
`{
"inputContext": "valueFromPreviousContext"
}`,
),
},
},
want: want{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "merge-context", Ttl: durationpb.New(response.DefaultTTL)},
Results: []*fnv1.Result{},
Context: resource.MustStructJSON(
`{
"contextField": "contextValue",
"inputContext": "valueFromPreviousContext"
}`,
),
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(`{"apiVersion":"","kind":""}`),
},
Resources: map[string]*fnv1.Resource{},
},
},
},
},
// TODO: disable the resource check, and fix the kcl dup resource evaluation issues.
// "MultipleResourceError": {
// reason: "The Function should return a fatal result if input resources have duplicate names",
Expand Down
19 changes: 19 additions & 0 deletions pkg/resource/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package resource

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

// MergeContext merges existing Context with new values provided
func MergeContext(req *fnv1.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
}
89 changes: 89 additions & 0 deletions pkg/resource/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package resource

import (
"testing"

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"
"google.golang.org/protobuf/testing/protocmp"
)

func TestMergeContext(t *testing.T) {
type args struct {
val map[string]interface{}
req *fnv1.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: &fnv1.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: &fnv1.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) {
rsp, err := 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)
}
})
}

}
7 changes: 6 additions & 1 deletion pkg/resource/res.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func SetData(data any, path string, o any, overwrite bool) error {
}

func ProcessResources(dxr *resource.Composite, oxr *resource.Composite, desired map[resource.Name]*resource.DesiredComposed, observed map[resource.Name]resource.ObservedComposed, extraResources map[string]*fnv1.ResourceSelector, conditions *ConditionResources,
events *EventResources, target Target, resources ResourceList, opts *AddResourcesOptions) (AddResourcesResult, error) {
events *EventResources, contextData *map[string]interface{}, target Target, resources ResourceList, opts *AddResourcesOptions) (AddResourcesResult, error) {
result := AddResourcesResult{
Target: target,
}
Expand Down Expand Up @@ -485,6 +485,11 @@ func ProcessResources(dxr *resource.Composite, oxr *resource.Composite, desired
if err := cd.Resource.GetValueInto("events", events); err != nil {
return result, errors.Wrap(err, "cannot get event resources")
}
case "Context":
// Returns events to add to the claim / composite
if err := cd.Resource.GetValueInto("data", contextData); err != nil {
return result, errors.Wrap(err, "cannot get context resource")
}
default:
return result, errors.Errorf("invalid kind %q for apiVersion %q - must be CompositeConnectionDetails or ExtraResources", obj.GetKind(), MetaApiVersion)
}
Expand Down

0 comments on commit e67112c

Please sign in to comment.