diff --git a/README.md b/README.md index 410a05e..595a4c4 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,47 @@ spec: For more information, see the example in [recursive](example/recursive). +## Setting Conditions on the Claim and Composite + +Starting with Crossplane 1.17, Composition authors can set custom Conditions on the +Composite and the Claim. + +Add a `ClaimConditions` to your template to set Conditions: + +```yaml +apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 +kind: ClaimConditions +conditions: +# Guide to ClaimConditions fields: +# Type of the condition, e.g. DatabaseReady. +# 'Healthy', 'Ready' and 'Synced' are reserved for use by Crossplane and this function will raise an error if used +# - type: +# Status of the condition. String of "True"/"False"/"Unknown" +# status: +# Machine-readable PascalCase reason, for example "ErrorProvisioning" +# reason: +# Optional Target. Publish Condition only to the Composite, or the Composite and the Claim (CompositeAndClaim). +# Defaults to Composite +# target: +# Optional message: +# message: +- type: TestCondition + status: "False" + reason: InstallFail + message: "failed to install" + target: CompositeAndClaim +- type: ConditionTrue + status: "True" + reason: TrueCondition + message: we are true + target: Composite +- type: DatabaseReady + status: "True" + reason: Ready + message: Database is ready + target: CompositeAndClaim +``` + ## Additional functions | Name | Description | diff --git a/claimconditions.go b/claimconditions.go new file mode 100644 index 0000000..3788e28 --- /dev/null +++ b/claimconditions.go @@ -0,0 +1,88 @@ +package main + +import ( + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/function-sdk-go/errors" + fnv1 "github.com/crossplane/function-sdk-go/proto/v1" + "github.com/crossplane/function-sdk-go/response" + corev1 "k8s.io/api/core/v1" +) + +// A CompositionTarget is the target of a composition event or condition. +type CompositionTarget string + +// A TargetedCondition represents a condition produced by the composition +// process. It can target either the XR only, or both the XR and the claim. +type TargetedCondition struct { + xpv1.Condition `json:",inline"` + Target CompositionTarget `json:"target"` +} + +// Composition event and condition targets. +const ( + CompositionTargetComposite CompositionTarget = "Composite" + CompositionTargetCompositeAndClaim CompositionTarget = "CompositeAndClaim" +) + +// UpdateClaimConditions updates Conditions in the Claim and Composite +func UpdateClaimConditions(rsp *fnv1.RunFunctionResponse, conditions ...TargetedCondition) error { + if rsp == nil { + return nil + } + for _, c := range conditions { + if xpv1.IsSystemConditionType(xpv1.ConditionType(c.Type)) { + response.Fatal(rsp, errors.Errorf("cannot set ClaimCondition type: %s is a reserved Crossplane Condition", c.Type)) + return errors.New("error updating response") + } + co := transformCondition(c) + UpdateResponseWithCondition(rsp, co) + } + return nil +} + +// transformCondition converts a TargetedCondition to be compatible with the Protobuf SDK +func transformCondition(tc TargetedCondition) *fnv1.Condition { + c := &fnv1.Condition{ + Type: string(tc.Condition.Type), + Reason: string(tc.Condition.Reason), + Target: transformTarget(tc.Target), + } + + switch tc.Condition.Status { + case corev1.ConditionTrue: + c.Status = fnv1.Status_STATUS_CONDITION_TRUE + case corev1.ConditionFalse: + c.Status = fnv1.Status_STATUS_CONDITION_FALSE + case corev1.ConditionUnknown: + fallthrough + default: + c.Status = fnv1.Status_STATUS_CONDITION_UNKNOWN + } + + if tc.Message != "" { + c.Message = &tc.Message + } + return c +} + +// transformTarget converts the input into a target Go SDK Enum +// Default to TARGET_COMPOSITE +func transformTarget(ct CompositionTarget) *fnv1.Target { + if ct == CompositionTargetCompositeAndClaim { + return fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum().Enum() + } + return fnv1.Target_TARGET_COMPOSITE.Enum() +} + +// UpdateResponseWithCondition updates the RunFunctionResponse with a Condition +func UpdateResponseWithCondition(rsp *fnv1.RunFunctionResponse, c *fnv1.Condition) { + if rsp == nil { + return + } + if rsp.GetConditions() == nil { + rsp.Conditions = make([]*fnv1.Condition, 0, 1) + } + if c != nil { + rsp.Conditions = append(rsp.GetConditions(), c) + } +} diff --git a/claimconditions_test.go b/claimconditions_test.go new file mode 100644 index 0000000..d948d90 --- /dev/null +++ b/claimconditions_test.go @@ -0,0 +1,333 @@ +package main + +import ( + "reflect" + "testing" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/crossplane/function-sdk-go/errors" + fnv1 "github.com/crossplane/function-sdk-go/proto/v1" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + v1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" +) + +func Test_UpdateClaimConditions(t *testing.T) { + type args struct { + rsp *fnv1.RunFunctionResponse + c []TargetedCondition + } + type want struct { + rsp *fnv1.RunFunctionResponse + err error + } + cases := map[string]struct { + reason string + args args + want want + }{ + "EmptyResponseNoConditions": { + reason: "When No Response or Conditions are provided, return a nil response", + args: args{}, + want: want{}, + }, + "ErrorOnReadyReservedType": { + reason: "Return an error if a Reserved Condition Type is being used", + args: args{ + rsp: &fnv1.RunFunctionResponse{}, + c: []TargetedCondition{ + { + Condition: xpv1.Condition{ + Message: "Ready Message", + Status: v1.ConditionTrue, + Type: "Ready", + }, + Target: CompositionTargetComposite, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Results: []*fnv1.Result{ + { + Severity: fnv1.Severity_SEVERITY_FATAL, + Message: "cannot set ClaimCondition type: Ready is a reserved Crossplane Condition", + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + }, + err: errors.New("error updating response"), + }, + }, + "SuccessfullyAddConditions": { + reason: "Add Conditions Successfully", + args: args{ + rsp: &fnv1.RunFunctionResponse{}, + c: []TargetedCondition{ + { + Condition: xpv1.Condition{ + Message: "Creating Resource", + Status: v1.ConditionFalse, + Type: "NetworkReady", + }, + Target: CompositionTargetCompositeAndClaim, + }, + { + Condition: xpv1.Condition{ + Message: "Ready Message", + Status: v1.ConditionTrue, + Type: "DatabaseReady", + }, + Target: CompositionTargetComposite, + }, + { + Condition: xpv1.Condition{ + Message: "No Target should add CompositeAndClaim", + Status: v1.ConditionTrue, + Type: "NoTarget", + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Conditions: []*fnv1.Condition{ + { + Message: ptr.To("Creating Resource"), + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + Type: "NetworkReady", + }, + { + Message: ptr.To("Ready Message"), + Status: fnv1.Status_STATUS_CONDITION_TRUE, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + Type: "DatabaseReady", + }, + { + Message: ptr.To("No Target should add CompositeAndClaim"), + Status: fnv1.Status_STATUS_CONDITION_TRUE, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + Type: "NoTarget", + }, + }, + }, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := UpdateClaimConditions(tc.args.rsp, tc.args.c...) + if diff := cmp.Diff(tc.args.rsp, tc.want.rsp, cmpopts.IgnoreUnexported(fnv1.RunFunctionResponse{}, fnv1.Result{}, fnv1.Condition{})); diff != "" { + t.Errorf("%s\nUpdateClaimConditions(...): -want rsp, +got rsp:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("%s\nUpdateClaimConditions(...): -want err, +got err:\n%s", tc.reason, diff) + } + + }) + } +} + +func Test_transformCondition(t *testing.T) { + type args struct { + tc TargetedCondition + } + cases := map[string]struct { + reason string + args args + want *fnv1.Condition + }{ + "Basic": { + reason: "Basic Target", + args: args{ + tc: TargetedCondition{ + Condition: xpv1.Condition{ + Message: "Basic Message", + Status: v1.ConditionTrue, + Type: "TestType", + }, + Target: CompositionTargetComposite, + }, + }, + want: &fnv1.Condition{ + Message: ptr.To("Basic Message"), + Status: fnv1.Status_STATUS_CONDITION_TRUE, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + Type: "TestType", + }, + }, + "Defaults": { + reason: "Default Settings", + args: args{ + tc: TargetedCondition{ + Condition: xpv1.Condition{}, + }, + }, + want: &fnv1.Condition{ + Status: fnv1.Status_STATUS_CONDITION_UNKNOWN, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + "StatusFalseNoTarget": { + reason: "When Status is false and no target set", + args: args{ + tc: TargetedCondition{ + Condition: xpv1.Condition{ + Message: "Basic Message", + Status: v1.ConditionFalse, + Type: "TestType", + }, + }, + }, + want: &fnv1.Condition{ + Message: ptr.To("Basic Message"), + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + Type: "TestType", + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + if got := transformCondition(tc.args.tc); !reflect.DeepEqual(got, tc.want) { + t.Errorf("transformCondition() = %v, want %v", got, tc.want) + } + }) + } +} + +func Test_transformTarget(t *testing.T) { + type args struct { + ct CompositionTarget + } + cases := map[string]struct { + reason string + args args + want *fnv1.Target + }{ + "DefaultToComposite": { + reason: "unknown target will default to Composite", + args: args{ + ct: "COMPOSE", + }, + want: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + "Composite": { + reason: "Composite target correctly set", + args: args{ + ct: "Composite", + }, + want: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + "CompositeAndClaim": { + reason: "CompositeAndClaim target correctly set", + args: args{ + ct: "CompositeAndClaim", + }, + want: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + if got := transformTarget(tc.args.ct); !reflect.DeepEqual(got, tc.want) { + t.Errorf("transformTarget() = %v, want %v", got, tc.want) + } + }) + } +} + +func Test_UpdateResponseWithCondition(t *testing.T) { + type args struct { + rsp *fnv1.RunFunctionResponse + c *fnv1.Condition + } + cases := map[string]struct { + reason string + args args + want *fnv1.RunFunctionResponse + }{ + "EmptyResponseNoConditions": { + reason: "When No Response or Conditions are provided, return a nil response", + args: args{}, + }, + "ResponseWithNoConditions": { + reason: "A response with no conditions should initialize an array before adding the condition", + args: args{ + rsp: &fnv1.RunFunctionResponse{}, + }, + want: &fnv1.RunFunctionResponse{ + Conditions: []*fnv1.Condition{}, + }, + }, + "ResponseAddCondition": { + reason: "A response with no conditions should initialize an array before adding the condition", + args: args{ + rsp: &fnv1.RunFunctionResponse{}, + c: &fnv1.Condition{ + Message: ptr.To("Basic Message"), + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + Type: "TestType", + }, + }, + want: &fnv1.RunFunctionResponse{ + Conditions: []*fnv1.Condition{ + { + Message: ptr.To("Basic Message"), + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + Type: "TestType", + }, + }, + }, + }, + "ResponseAppCondition": { + reason: "A response with existing conditions should append the condition", + args: args{ + rsp: &fnv1.RunFunctionResponse{ + Conditions: []*fnv1.Condition{ + { + Message: ptr.To("Existing Message"), + Status: fnv1.Status_STATUS_CONDITION_TRUE, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + Type: "ExistingTestType", + }, + }, + }, + c: &fnv1.Condition{ + Message: ptr.To("Basic Message"), + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + Type: "TestType", + }, + }, + want: &fnv1.RunFunctionResponse{ + Conditions: []*fnv1.Condition{ + { + Message: ptr.To("Existing Message"), + Status: fnv1.Status_STATUS_CONDITION_TRUE, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + Type: "ExistingTestType", + }, + { + Message: ptr.To("Basic Message"), + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + Type: "TestType", + }, + }, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + UpdateResponseWithCondition(tc.args.rsp, tc.args.c) + if diff := cmp.Diff(tc.args.rsp, tc.want, cmpopts.IgnoreUnexported(fnv1.RunFunctionResponse{}, fnv1.Condition{})); diff != "" { + t.Errorf("%s\nUpdateResponseWithCondition(...): -want rsp, +got rsp:\n%s", tc.reason, diff) + } + }) + } +} diff --git a/example/conditions/README.md b/example/conditions/README.md new file mode 100644 index 0000000..60de2cc --- /dev/null +++ b/example/conditions/README.md @@ -0,0 +1,39 @@ +# Writing to the Composite or Claim Status + +function-go-templating can write to the Composite or Claim Status. See [Communication Between Composition Functions and the Claim](https://github.com/crossplane/crossplane/blob/main/design/one-pager-fn-claim-conditions.md) for more information. + +## 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 \ + xr.yaml composition.yaml functions.yaml +``` + +## 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.9.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. diff --git a/example/conditions/composition.yaml b/example/conditions/composition.yaml new file mode 100644 index 0000000..d034db6 --- /dev/null +++ b/example/conditions/composition.yaml @@ -0,0 +1,54 @@ +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: go-templating-update-conditions + 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: ClaimConditions + conditions: + # Guide to ClaimConditions fields: + # Type of the condition, e.g. DatabaseReady. + # 'Healthy', 'Ready' and 'Synced' are reserved for use by Crossplane and this function will raise an error if used + # - type: + # Status of the condition. String of "True"/"False"/"Unknown" + # status: + # Machine-readable PascalCase reason, for example "ErrorProvisioning" + # reason: + # Optional Target. Publish Condition only to the Composite, or the Composite and the Claim (CompositeAndClaim). + # Defaults to Composite + # target: + # Optional message: + # message: + - type: TestCondition + status: "False" + reason: InstallFail + message: "failed to install" + target: CompositeAndClaim + - type: ConditionTrue + status: "True" + reason: TrueCondition + message: we are true + target: Composite + - type: DatabaseReady + status: "True" + reason: Ready + message: Database is ready + target: CompositeAndClaim + - step: automatically-detect-ready-composed-resources + functionRef: + name: crossplane-contrib-function-auto-ready diff --git a/example/conditions/functions.yaml b/example/conditions/functions.yaml new file mode 100644 index 0000000..82ebe3b --- /dev/null +++ b/example/conditions/functions.yaml @@ -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.7.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.3.0 diff --git a/example/conditions/xr.yaml b/example/conditions/xr.yaml new file mode 100644 index 0000000..35a7feb --- /dev/null +++ b/example/conditions/xr.yaml @@ -0,0 +1,5 @@ +apiVersion: example.crossplane.io/v1 +kind: XR +metadata: + name: example-xr +spec: {} \ No newline at end of file diff --git a/example/conditions/xrd.yaml b/example/conditions/xrd.yaml new file mode 100644 index 0000000..8955af2 --- /dev/null +++ b/example/conditions/xrd.yaml @@ -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 \ No newline at end of file diff --git a/fn.go b/fn.go index 7d05468..fd1376d 100644 --- a/fn.go +++ b/fn.go @@ -192,6 +192,17 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) d, _ := base64.StdEncoding.DecodeString(v) //nolint:errcheck // k8s returns secret values encoded desiredComposite.ConnectionDetails[k] = d } + case "ClaimConditions": + var conditions []TargetedCondition + if err = cd.Resource.GetValueInto("conditions", &conditions); err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot get Conditions from input")) + return rsp, nil + } + err := UpdateClaimConditions(rsp, conditions...) + if err != nil { + return rsp, nil + } + f.log.Debug("updating ClaimConditions", "conditions", rsp.Conditions) case "Context": contextData := make(map[string]interface{}) if err = cd.Resource.GetValueInto("data", &contextData); err != nil { diff --git a/fn_test.go b/fn_test.go index 1c56ddb..227d208 100644 --- a/fn_test.go +++ b/fn_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/durationpb" + "k8s.io/utils/ptr" "github.com/crossplane/crossplane-runtime/pkg/logging" @@ -40,6 +41,9 @@ var ( xrWithNestedStatusBaz = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2},"status":{"state":{"baz":"qux"}}}` xrRecursiveTmpl = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"annotations":{"gotemplating.fn.crossplane.io/composition-resource-name":"recursive-xr"},"name":"recursive-xr","labels":{"belongsTo":{{.observed.composite.resource.metadata.name|quote}}}},"spec":{"count":2}}` + claimConditions = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"ClaimConditions","conditions":[{"type":"TestCondition","status":"False","reason":"InstallFail","message":"failed to install","target":"ClaimAndComposite"},{"type":"ConditionTrue","status":"True","reason":"this condition is true","message":"we are true","target":"Composite"},{"type":"DatabaseReady","status":"True","reason":"Ready","message":"Database is ready"}]}` + claimConditionsReservedKey = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"ClaimConditions","conditions":[{"type":"Ready","status":"False","reason":"InstallFail","message":"I am using a reserved Condition","target":"ClaimAndComposite"}]}` + extraResources = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"ExtraResources","requirements":{"cool-extra-resource":{"apiVersion":"example.org/v1","kind":"CoolExtraResource","matchName":"cool-extra-resource"}}} {"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"ExtraResources","requirements":{"another-cool-extra-resource":{"apiVersion":"example.org/v1","kind":"CoolExtraResource","matchLabels":{"key": "value"}},"yet-another-cool-extra-resource":{"apiVersion":"example.org/v1","kind":"CoolExtraResource","matchName":"foo"}}} {"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"ExtraResources","requirements":{"all-cool-resources":{"apiVersion":"example.org/v1","kind":"CoolExtraResource","matchLabels":{}}}}` @@ -611,6 +615,100 @@ func TestRunFunction(t *testing.T) { }, }, }, + "ClaimConditionsError": { + reason: "The Function should return a fatal result if a reserved Condition is set.", + args: args{ + req: &fnv1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.GoTemplate{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.TemplateSourceInline{Template: claimConditionsReservedKey}, + }), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1.Result{ + { + Severity: fnv1.Severity_SEVERITY_FATAL, + Message: "cannot set ClaimCondition type: Ready is a reserved Crossplane Condition", + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + "ClaimConditions": { + reason: "The Function should correctly set ClaimConditions.", + args: args{ + req: &fnv1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.GoTemplate{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.TemplateSourceInline{Template: claimConditions}, + }), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Conditions: []*fnv1.Condition{ + { + Type: "TestCondition", + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Reason: "InstallFail", + Message: ptr.To("failed to install"), + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + { + Type: "ConditionTrue", + Status: fnv1.Status_STATUS_CONDITION_TRUE, + Reason: "this condition is true", + Message: ptr.To("we are true"), + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + { + Type: "DatabaseReady", + Status: fnv1.Status_STATUS_CONDITION_TRUE, + Reason: "Ready", + Message: ptr.To("Database is ready"), + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + Meta: &fnv1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, "CompositeConnectionDetails": { reason: "The Function should return the desired composite with CompositeConnectionDetails.", args: args{ diff --git a/go.mod b/go.mod index e65951c..358aee8 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/crossplane-contrib/function-go-templating go 1.23 -toolchain go1.23.2 +toolchain go1.23.3 require ( dario.cat/mergo v1.0.1 @@ -13,13 +13,16 @@ require ( github.com/google/go-cmp v0.6.0 google.golang.org/protobuf v1.34.3-0.20240816073751-94ecbc261689 gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.30.0 k8s.io/apimachinery v0.30.0 + k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 sigs.k8s.io/controller-tools v0.14.0 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/alecthomas/assert/v2 v2.11.0 // 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.9.0 // indirect @@ -49,6 +52,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect @@ -61,7 +65,7 @@ require ( golang.org/x/net v0.29.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.24.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.5.0 // indirect @@ -70,12 +74,10 @@ require ( google.golang.org/grpc v1.66.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/api v0.30.0 // indirect k8s.io/apiextensions-apiserver v0.30.0 // indirect k8s.io/client-go v0.30.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 // indirect sigs.k8s.io/controller-runtime v0.18.2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect diff --git a/go.sum b/go.sum index 69e3aa3..0a58f2b 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,14 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= -github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= @@ -177,8 +177,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= @@ -250,8 +250,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=