From ebf221d3ae5bc7f40ccba4df6c389e6b51a47f49 Mon Sep 17 00:00:00 2001 From: Mark Altmann Date: Mon, 9 Dec 2024 18:20:29 +0100 Subject: [PATCH 1/6] feat: add event and condition handling in resource processing Signed-off-by: Mark Altmann --- .gitignore | 3 ++ fn.go | 13 ++++- fn_test.go | 56 ++++++++++++++++++++++ pkg/resource/conditions.go | 97 ++++++++++++++++++++++++++++++++++++++ pkg/resource/events.go | 68 ++++++++++++++++++++++++++ pkg/resource/res.go | 7 ++- 6 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 pkg/resource/conditions.go create mode 100644 pkg/resource/events.go diff --git a/.gitignore b/.gitignore index 6bcd1ff..fd38ef7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ vendor/ .kclvm .DS_store package/*.xpkg + +.idea +.vscode \ No newline at end of file diff --git a/fn.go b/fn.go index 873c457..c04a9d7 100644 --- a/fn.go +++ b/fn.go @@ -181,7 +181,9 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) } log.Debug(fmt.Sprintf("Input resources: %v", resources)) extraResources := map[string]*fnv1.ResourceSelector{} - result, err := pkgresource.ProcessResources(dxr, oxr, desired, observed, extraResources, in.Spec.Target, resources, &pkgresource.AddResourcesOptions{ + 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{ Basename: in.Name, Data: data, Overwrite: true, @@ -196,6 +198,15 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) } rsp.Requirements = &fnv1.Requirements{ExtraResources: extraResources} } + + if len(conditions) > 0 { + pkgresource.SetConditions(rsp.Conditions, conditions, log) + } + + if len(events) > 0 { + pkgresource.SetEvents(rsp.Results, events, log) + } + 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)) diff --git a/fn_test.go b/fn_test.go index 1349893..7bb4d1c 100644 --- a/fn_test.go +++ b/fn_test.go @@ -442,6 +442,62 @@ func TestRunFunctionSimple(t *testing.T) { }, }, }, + "SetConditions": { + reason: "The Function should return the conditions from the request.", + args: args{ + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "set-conditions"}, + 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: \"ConditionResouce\"\n spec: {\n forProvider: {\n project: \"test-project\"\n settings: [{databaseFlags: [{\n name: \"log_checkpoints\"\n value: \"on\"\n }]}]\n }\n }\n}]\n" + + } + }`), + ExtraResources: map[string]*fnv1.Resources{ + "cool1": { + Items: []*fnv1.Resource{ + {Resource: resource.MustStructJSON(xr)}, + {Resource: resource.MustStructJSON(cd)}, + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: "extra-resources-in", Ttl: durationpb.New(response.DefaultTTL)}, + Conditions: []*fnv1.Condition{ + fnv1.Condition{ + Type: "", + Status: 0, + Reason: "", + Message: nil, + Target: nil, + }, + }, + Results: []*fnv1.Result{}, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"","kind":""}`), + }, + Resources: map[string]*fnv1.Resource{ + "cool-xr": { + Resource: resource.MustStructJSON(xr), + }, + "cool-cd": { + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd"}}`), + }, + }, + }, + }, + }, + }, // 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", diff --git a/pkg/resource/conditions.go b/pkg/resource/conditions.go new file mode 100644 index 0000000..c7a7b74 --- /dev/null +++ b/pkg/resource/conditions.go @@ -0,0 +1,97 @@ +package resource + +import ( + "github.com/crossplane/crossplane-runtime/pkg/logging" + fnv1 "github.com/crossplane/function-sdk-go/proto/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +// Target determines which objects to set the condition on. +type BindingTarget string + +const ( + // TargetComposite targets only the composite resource. + TargetComposite BindingTarget = "Composite" + + // TargetCompositeAndClaim targets both the composite and the claim. + TargetCompositeAndClaim BindingTarget = "CompositeAndClaim" +) + +type ConditionResources []ConditionResource + +// ConditionResource will set a condition on the target. +type ConditionResource struct { + // The target(s) to receive the condition. Can be Composite or + // CompositeAndClaim. + Target *BindingTarget `json:"target"` + // If true, the condition will override a condition of the same Type. Defaults + // to false. + Force *bool `json:"force"` + // Condition to set. + Condition Condition `json:"condition"` +} + +// Condition allows you to specify fields to set on a composite resource and +// claim. +type Condition struct { + // Type of the condition. Required. + Type string `json:"type"` + // Status of the condition. Required. + Status metav1.ConditionStatus `json:"status"` + // Reason of the condition. Required. + Reason string `json:"reason"` + // Message of the condition. Optional. A template can be used. The available + // template variables come from capturing groups in MatchCondition message + // regular expressions. + Message *string `json:"message"` +} + +func transformCondition(cs ConditionResource) *fnv1.Condition { + c := &fnv1.Condition{ + Type: cs.Condition.Type, + Reason: cs.Condition.Reason, + Target: transformTarget(cs.Target), + } + + switch cs.Condition.Status { + case metav1.ConditionTrue: + c.Status = fnv1.Status_STATUS_CONDITION_TRUE + case metav1.ConditionFalse: + c.Status = fnv1.Status_STATUS_CONDITION_FALSE + case metav1.ConditionUnknown: + fallthrough + default: + c.Status = fnv1.Status_STATUS_CONDITION_UNKNOWN + } + + c.Message = cs.Condition.Message + + return c +} + +func transformTarget(t *BindingTarget) *fnv1.Target { + target := ptr.Deref(t, TargetComposite) + if target == TargetCompositeAndClaim { + return fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum() + } + return fnv1.Target_TARGET_COMPOSITE.Enum() +} + +func SetConditions(conditions []*fnv1.Condition, cr ConditionResources, log logging.Logger) { + conditionsSet := map[string]bool{} + // All matchConditions matched, set the desired conditions. + for _, cs := range cr { + if conditionsSet[cs.Condition.Type] && (cs.Force == nil || !*cs.Force) { + // The condition is already set and this setter is not forceful. + log.Debug("skipping because condition is already set and setCondition is not forceful") + continue + } + log.Debug("setting condition") + + c := transformCondition(cs) + + conditions = append(conditions, c) + conditionsSet[cs.Condition.Type] = true + } +} diff --git a/pkg/resource/events.go b/pkg/resource/events.go new file mode 100644 index 0000000..4af232e --- /dev/null +++ b/pkg/resource/events.go @@ -0,0 +1,68 @@ +package resource + +import ( + "github.com/crossplane/crossplane-runtime/pkg/logging" + fnv1 "github.com/crossplane/function-sdk-go/proto/v1" + "k8s.io/utils/ptr" +) + +// EventType type of an event. +type EventType string + +const ( + // EventTypeNormal signifies a normal event. + EventTypeNormal EventType = "Normal" + + // EventTypeWarning signifies a warning event. + EventTypeWarning EventType = "Warning" +) + +type EventResources []CreateEvent + +// Event allows you to specify the fields of an event to create. +type Event struct { + // Type of the event. Optional. Should be either Normal or Warning. + Type *EventType `json:"type"` + // Reason of the event. Optional. + Reason *string `json:"reason"` + // Message of the event. Required. A template can be used. The available + // template variables come from capturing groups in MatchCondition message + // regular expressions. + Message string `json:"message"` +} + +// CreateEvent will create an event for the target(s). +type CreateEvent struct { + // The target(s) to create an event for. Can be Composite or + // CompositeAndClaim. + Target *BindingTarget `json:"target"` + + // Event to create. + Event Event `json:"event"` +} + +func SetEvents(results []*fnv1.Result, ers EventResources, log logging.Logger) { + for _, er := range ers { + r := transformEvent(er) + + results = append(results, r) + } +} + +func transformEvent(ec CreateEvent) *fnv1.Result { + e := &fnv1.Result{ + Reason: ec.Event.Reason, + Target: transformTarget(ec.Target), + } + + switch ptr.Deref(ec.Event.Type, EventTypeNormal) { + case EventTypeNormal: + e.Severity = fnv1.Severity_SEVERITY_NORMAL + case EventTypeWarning: + default: + e.Severity = fnv1.Severity_SEVERITY_WARNING + } + + e.Message = ec.Event.Message + return e +} diff --git a/pkg/resource/res.go b/pkg/resource/res.go index c4c937d..3f60d28 100644 --- a/pkg/resource/res.go +++ b/pkg/resource/res.go @@ -383,7 +383,7 @@ func SetData(data any, path string, o any, overwrite bool) error { return nil } -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, target Target, resources ResourceList, opts *AddResourcesOptions) (AddResourcesResult, 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, p *EventResources, target Target, resources ResourceList, opts *AddResourcesOptions) (AddResourcesResult, error) { result := AddResourcesResult{ Target: target, } @@ -474,6 +474,11 @@ func ProcessResources(dxr *resource.Composite, oxr *resource.Composite, desired } extraResources[k] = v.ToResourceSelector() } + case "Conditions": + // Returns conditions to add to the claim / composite + if err := cd.Resource.GetValueInto("conditions", conditions); err != nil { + return result, errors.Wrap(err, "cannot get condition resources") + } default: return result, errors.Errorf("invalid kind %q for apiVersion %q - must be CompositeConnectionDetails or ExtraResources", obj.GetKind(), MetaApiVersion) } From a7f4ad4d5e8acbad8cdec389f2a0abb4a3b1d842 Mon Sep 17 00:00:00 2001 From: Mark Altmann Date: Wed, 11 Dec 2024 20:39:58 +0100 Subject: [PATCH 2/6] fix: writing tests for conditions and events Signed-off-by: Mark Altmann --- examples/default/conditions/Makefile | 2 + examples/default/conditions/README.md | 62 +++++++++++++ examples/default/conditions/composition.yaml | 49 +++++++++++ examples/default/conditions/functions.yaml | 9 ++ examples/default/conditions/xr.yaml | 6 ++ examples/default/events/Makefile | 2 + examples/default/events/README.md | 62 +++++++++++++ examples/default/events/composition.yaml | 46 ++++++++++ examples/default/events/functions.yaml | 9 ++ examples/default/events/xr.yaml | 6 ++ fn.go | 4 +- fn_test.go | 91 ++++++++++++++------ pkg/resource/conditions.go | 4 +- pkg/resource/events.go | 5 +- pkg/resource/res.go | 8 +- 15 files changed, 329 insertions(+), 36 deletions(-) create mode 100644 examples/default/conditions/Makefile create mode 100644 examples/default/conditions/README.md create mode 100644 examples/default/conditions/composition.yaml create mode 100644 examples/default/conditions/functions.yaml create mode 100644 examples/default/conditions/xr.yaml create mode 100644 examples/default/events/Makefile create mode 100644 examples/default/events/README.md create mode 100644 examples/default/events/composition.yaml create mode 100644 examples/default/events/functions.yaml create mode 100644 examples/default/events/xr.yaml diff --git a/examples/default/conditions/Makefile b/examples/default/conditions/Makefile new file mode 100644 index 0000000..8a8c414 --- /dev/null +++ b/examples/default/conditions/Makefile @@ -0,0 +1,2 @@ +run: + crossplane render --verbose xr.yaml composition.yaml functions.yaml -r diff --git a/examples/default/conditions/README.md b/examples/default/conditions/README.md new file mode 100644 index 0000000..380cf1d --- /dev/null +++ b/examples/default/conditions/README.md @@ -0,0 +1,62 @@ +# 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 -r --extra-resources extra_resources.yaml +--- +--- +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +status: + conditions: + - lastTransitionTime: "2024-01-01T00:00:00Z" + message: 'Unready resources: another-awesome-dev-bucket, my-awesome-dev-bucket' + reason: Creating + status: "False" + type: Ready +--- +apiVersion: example/v1alpha1 +kind: Foo +metadata: + annotations: + crossplane.io/composition-resource-name: another-awesome-dev-bucket + generateName: example- + labels: + crossplane.io/composite: example + name: another-awesome-dev-bucket + ownerReferences: + - apiVersion: example.crossplane.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example + uid: "" +--- +apiVersion: example/v1alpha1 +kind: Foo +metadata: + annotations: + crossplane.io/composition-resource-name: my-awesome-dev-bucket + generateName: example- + labels: + crossplane.io/composite: example + name: my-awesome-dev-bucket + ownerReferences: + - apiVersion: example.crossplane.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example + uid: "" + +``` diff --git a/examples/default/conditions/composition.yaml b/examples/default/conditions/composition.yaml new file mode 100644 index 0000000..a277ff5 --- /dev/null +++ b/examples/default/conditions/composition.yaml @@ -0,0 +1,49 @@ +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 + } + + conditions = { + apiVersion: "meta.krm.kcl.dev/v1alpha1" + kind: "Conditions" + conditions = [ + { + target: "CompositeAndClaim" + force: False + condition = { + type: "DatabaseReady" + status: "False" + reason: "FailedToCreate" + message: "Encountered an error creating the database" + } + } + ] + + } + items = [ + conditions + dxr + ] diff --git a/examples/default/conditions/functions.yaml b/examples/default/conditions/functions.yaml new file mode 100644 index 0000000..d5679cb --- /dev/null +++ b/examples/default/conditions/functions.yaml @@ -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 diff --git a/examples/default/conditions/xr.yaml b/examples/default/conditions/xr.yaml new file mode 100644 index 0000000..67aa59f --- /dev/null +++ b/examples/default/conditions/xr.yaml @@ -0,0 +1,6 @@ +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +spec: + count: 1 diff --git a/examples/default/events/Makefile b/examples/default/events/Makefile new file mode 100644 index 0000000..8a8c414 --- /dev/null +++ b/examples/default/events/Makefile @@ -0,0 +1,2 @@ +run: + crossplane render --verbose xr.yaml composition.yaml functions.yaml -r diff --git a/examples/default/events/README.md b/examples/default/events/README.md new file mode 100644 index 0000000..380cf1d --- /dev/null +++ b/examples/default/events/README.md @@ -0,0 +1,62 @@ +# 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 -r --extra-resources extra_resources.yaml +--- +--- +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +status: + conditions: + - lastTransitionTime: "2024-01-01T00:00:00Z" + message: 'Unready resources: another-awesome-dev-bucket, my-awesome-dev-bucket' + reason: Creating + status: "False" + type: Ready +--- +apiVersion: example/v1alpha1 +kind: Foo +metadata: + annotations: + crossplane.io/composition-resource-name: another-awesome-dev-bucket + generateName: example- + labels: + crossplane.io/composite: example + name: another-awesome-dev-bucket + ownerReferences: + - apiVersion: example.crossplane.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example + uid: "" +--- +apiVersion: example/v1alpha1 +kind: Foo +metadata: + annotations: + crossplane.io/composition-resource-name: my-awesome-dev-bucket + generateName: example- + labels: + crossplane.io/composite: example + name: my-awesome-dev-bucket + ownerReferences: + - apiVersion: example.crossplane.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example + uid: "" + +``` diff --git a/examples/default/events/composition.yaml b/examples/default/events/composition.yaml new file mode 100644 index 0000000..cc81fa1 --- /dev/null +++ b/examples/default/events/composition.yaml @@ -0,0 +1,46 @@ +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 + } + + events = { + apiVersion: "meta.krm.kcl.dev/v1alpha1" + kind: "Events" + events = [ + { + target: "CompositeAndClaim" + event = { + type: "Warning" + reason: "ResourceLimitExceeded" + message: "The resource limit has been exceeded" + } + } + ] + } + items = [ + events + dxr + ] diff --git a/examples/default/events/functions.yaml b/examples/default/events/functions.yaml new file mode 100644 index 0000000..d5679cb --- /dev/null +++ b/examples/default/events/functions.yaml @@ -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 diff --git a/examples/default/events/xr.yaml b/examples/default/events/xr.yaml new file mode 100644 index 0000000..67aa59f --- /dev/null +++ b/examples/default/events/xr.yaml @@ -0,0 +1,6 @@ +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +spec: + count: 1 diff --git a/fn.go b/fn.go index c04a9d7..ea5db54 100644 --- a/fn.go +++ b/fn.go @@ -200,11 +200,11 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) } if len(conditions) > 0 { - pkgresource.SetConditions(rsp.Conditions, conditions, log) + pkgresource.SetConditions(rsp, conditions, log) } if len(events) > 0 { - pkgresource.SetEvents(rsp.Results, events, log) + pkgresource.SetEvents(rsp, events) } log.Debug(fmt.Sprintf("Set %d resource(s) to the desired state", result.MsgCount)) diff --git a/fn_test.go b/fn_test.go index 7bb4d1c..5b59133 100644 --- a/fn_test.go +++ b/fn_test.go @@ -6,14 +6,14 @@ import ( "path/filepath" "testing" + "github.com/crossplane/crossplane-runtime/pkg/logging" "github.com/go-logr/logr/testr" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" - - "github.com/crossplane/crossplane-runtime/pkg/logging" + "k8s.io/utils/ptr" fnv1 "github.com/crossplane/function-sdk-go/proto/v1" "github.com/crossplane/function-sdk-go/resource" @@ -94,7 +94,7 @@ func TestRunFunctionSimple(t *testing.T) { "name": "basic" }, "spec": { - "source": "items = [{ \n apiVersion: \"sql.gcp.upbound.io/v1beta1\"\n kind: \"DatabaseInstance\"\n spec: {\n forProvider: {\n project: \"test-project\"\n settings: [{databaseFlags: [{\n name: \"log_checkpoints\"\n value: \"on\"\n }]}]\n }\n }\n}]\n" + "source": "items = [{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ConditionsAndEvents\"\n conditions = [{\n target: \"CompositeAndClaim\"\n force: False\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"FailedToCreate\"\n message: \"Encountered an error creating the database\"\n }\n }]\n}]" } }`), Observed: &fnv1.State{ @@ -345,7 +345,7 @@ func TestRunFunctionSimple(t *testing.T) { }, "spec": { "target": "Default", - "source": "items = [\n{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ExtraResources\"\n requirements = {\n \"cool-extra-resource\" = {\n apiVersion: \"example.org/v1\"\n kind: \"CoolExtraResource\"\n matchName: \"cool-extra-resource\"\n }\n }\n}\n{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ExtraResources\"\n requirements = {\n \"cool-extra-resource\" = {\n apiVersion: \"example.org/v1\"\n kind: \"CoolExtraResource\"\n matchName: \"another-cool-extra-resource\"\n }\n }\n}\n]\n" + "source": "items = [\n{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ConditionsAndEvents\"\n conditions = [\n {\n target: \"CompositeAndClaim\"\n force: False\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"FailedToCreate\"\n message: \"Encountered an error creating the database\"\n }\n }\n ]\n}]" } }`), Observed: &fnv1.State{ @@ -455,30 +455,27 @@ func TestRunFunctionSimple(t *testing.T) { }, "spec": { "target": "Default", - "source": "items = [{ \n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ConditionResouce\"\n spec: {\n forProvider: {\n project: \"test-project\"\n settings: [{databaseFlags: [{\n name: \"log_checkpoints\"\n value: \"on\"\n }]}]\n }\n }\n}]\n" - + "source": "items = [{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"Conditions\"\n conditions = [{\n target: \"CompositeAndClaim\"\n force: False\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"FailedToCreate\"\n message: \"Encountered an error creating the database\"\n }\n },{\n target: \"Composite\"\n force: False\n condition = {\n type: \"ValidationError\"\n status: \"False\"\n reason: \"FailedToValidate\"\n message: \"Encountered an error during validation\"\n }\n }]\n}]" } }`), - ExtraResources: map[string]*fnv1.Resources{ - "cool1": { - Items: []*fnv1.Resource{ - {Resource: resource.MustStructJSON(xr)}, - {Resource: resource.MustStructJSON(cd)}, - }, - }, - }, }, }, want: want{ rsp: &fnv1.RunFunctionResponse{ - Meta: &fnv1.ResponseMeta{Tag: "extra-resources-in", Ttl: durationpb.New(response.DefaultTTL)}, + Meta: &fnv1.ResponseMeta{Tag: "set-conditions", Ttl: durationpb.New(response.DefaultTTL)}, Conditions: []*fnv1.Condition{ - fnv1.Condition{ - Type: "", - Status: 0, - Reason: "", - Message: nil, - Target: nil, + { + Type: "DatabaseReady", + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Reason: "FailedToCreate", + Message: ptr.To("Encountered an error creating the database"), + Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + }, { + Type: "ValidationError", + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Reason: "FailedToValidate", + Message: ptr.To("Encountered an error during validation"), + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), }, }, Results: []*fnv1.Result{}, @@ -486,14 +483,52 @@ func TestRunFunctionSimple(t *testing.T) { Composite: &fnv1.Resource{ Resource: resource.MustStructJSON(`{"apiVersion":"","kind":""}`), }, - Resources: map[string]*fnv1.Resource{ - "cool-xr": { - Resource: resource.MustStructJSON(xr), - }, - "cool-cd": { - Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd"}}`), - }, + Resources: map[string]*fnv1.Resource{}, + }, + }, + }, + }, + "SetEvents": { + reason: "The Function should return the events from the request.", + args: args{ + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "set-conditions"}, + 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: \"Events\"\n events = [{\n target: \"CompositeAndClaim\"\n event = {\n type: \"Warning\"\n reason: \"ResourceLimitExceeded\"\n message: \"The resource limit has been exceeded\"\n }\n },{\n target: \"Composite\"\n event = {\n type: \"Warning\"\n reason: \"ValidationFailed\"\n message: \"The validation failed\"\n }\n }]\n}]" + } + }`), + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: "set-conditions", Ttl: durationpb.New(response.DefaultTTL)}, + Conditions: []*fnv1.Condition{}, + Results: []*fnv1.Result{ + { + Severity: 0, + Message: "The resource limit has been exceeded", + Reason: ptr.To("ResourceLimitExceeded"), + Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + }, + { + Severity: 0, + Message: "The validation failed", + Reason: ptr.To("ValidationFailed"), + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"","kind":""}`), }, + Resources: map[string]*fnv1.Resource{}, }, }, }, diff --git a/pkg/resource/conditions.go b/pkg/resource/conditions.go index c7a7b74..811a16d 100644 --- a/pkg/resource/conditions.go +++ b/pkg/resource/conditions.go @@ -78,7 +78,7 @@ func transformTarget(t *BindingTarget) *fnv1.Target { return fnv1.Target_TARGET_COMPOSITE.Enum() } -func SetConditions(conditions []*fnv1.Condition, cr ConditionResources, log logging.Logger) { +func SetConditions(rsp *fnv1.RunFunctionResponse, cr ConditionResources, log logging.Logger) { conditionsSet := map[string]bool{} // All matchConditions matched, set the desired conditions. for _, cs := range cr { @@ -91,7 +91,7 @@ func SetConditions(conditions []*fnv1.Condition, cr ConditionResources, log logg c := transformCondition(cs) - conditions = append(conditions, c) + rsp.Conditions = append(rsp.Conditions, c) conditionsSet[cs.Condition.Type] = true } } diff --git a/pkg/resource/events.go b/pkg/resource/events.go index 4af232e..4cbd4c9 100644 --- a/pkg/resource/events.go +++ b/pkg/resource/events.go @@ -1,7 +1,6 @@ package resource import ( - "github.com/crossplane/crossplane-runtime/pkg/logging" fnv1 "github.com/crossplane/function-sdk-go/proto/v1" "k8s.io/utils/ptr" ) @@ -41,11 +40,11 @@ type CreateEvent struct { Event Event `json:"event"` } -func SetEvents(results []*fnv1.Result, ers EventResources, log logging.Logger) { +func SetEvents(rsp *fnv1.RunFunctionResponse, ers EventResources) { for _, er := range ers { r := transformEvent(er) - results = append(results, r) + rsp.Results = append(rsp.Results, r) } } diff --git a/pkg/resource/res.go b/pkg/resource/res.go index 3f60d28..cd11f81 100644 --- a/pkg/resource/res.go +++ b/pkg/resource/res.go @@ -383,7 +383,8 @@ func SetData(data any, path string, o any, overwrite bool) error { return nil } -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, p *EventResources, target Target, resources ResourceList, opts *AddResourcesOptions) (AddResourcesResult, 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) { result := AddResourcesResult{ Target: target, } @@ -479,6 +480,11 @@ func ProcessResources(dxr *resource.Composite, oxr *resource.Composite, desired if err := cd.Resource.GetValueInto("conditions", conditions); err != nil { return result, errors.Wrap(err, "cannot get condition resources") } + case "Events": + // Returns events to add to the claim / composite + if err := cd.Resource.GetValueInto("events", events); err != nil { + return result, errors.Wrap(err, "cannot get event resources") + } default: return result, errors.Errorf("invalid kind %q for apiVersion %q - must be CompositeConnectionDetails or ExtraResources", obj.GetKind(), MetaApiVersion) } From ac5e210171651170852d85c94ed71266e91917c8 Mon Sep 17 00:00:00 2001 From: Mark Altmann Date: Thu, 2 Jan 2025 16:56:50 +0100 Subject: [PATCH 3/6] fix: minor fixes and tests Signed-off-by: Mark Altmann --- fn.go | 5 +- fn_test.go | 57 ++++++++++++++++--- go.mod | 2 +- .../template.fn.crossplane.io_kclinputs.yaml | 2 +- pkg/resource/events.go | 22 ++++--- 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/fn.go b/fn.go index ea5db54..64a8322 100644 --- a/fn.go +++ b/fn.go @@ -204,7 +204,10 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) } if len(events) > 0 { - pkgresource.SetEvents(rsp, events) + err := pkgresource.SetEvents(rsp, events) + if err != nil { + return rsp, err + } } log.Debug(fmt.Sprintf("Set %d resource(s) to the desired state", result.MsgCount)) diff --git a/fn_test.go b/fn_test.go index 5b59133..4adf426 100644 --- a/fn_test.go +++ b/fn_test.go @@ -94,7 +94,7 @@ func TestRunFunctionSimple(t *testing.T) { "name": "basic" }, "spec": { - "source": "items = [{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ConditionsAndEvents\"\n conditions = [{\n target: \"CompositeAndClaim\"\n force: False\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"FailedToCreate\"\n message: \"Encountered an error creating the database\"\n }\n }]\n}]" + "source": "items = [{ \n apiVersion: \"sql.gcp.upbound.io/v1beta1\"\n kind: \"DatabaseInstance\"\n spec: {\n forProvider: {\n project: \"test-project\"\n settings: [{databaseFlags: [{\n name: \"log_checkpoints\"\n value: \"on\"\n }]}]\n }\n }\n}]\n" } }`), Observed: &fnv1.State{ @@ -345,7 +345,7 @@ func TestRunFunctionSimple(t *testing.T) { }, "spec": { "target": "Default", - "source": "items = [\n{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ConditionsAndEvents\"\n conditions = [\n {\n target: \"CompositeAndClaim\"\n force: False\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"FailedToCreate\"\n message: \"Encountered an error creating the database\"\n }\n }\n ]\n}]" + "source": "items = [\n{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ExtraResources\"\n requirements = {\n \"cool-extra-resource\" = {\n apiVersion: \"example.org/v1\"\n kind: \"CoolExtraResource\"\n matchName: \"cool-extra-resource\"\n }\n }\n}\n{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"ExtraResources\"\n requirements = {\n \"cool-extra-resource\" = {\n apiVersion: \"example.org/v1\"\n kind: \"CoolExtraResource\"\n matchName: \"another-cool-extra-resource\"\n }\n }\n}\n]\n" } }`), Observed: &fnv1.State{ @@ -455,7 +455,7 @@ func TestRunFunctionSimple(t *testing.T) { }, "spec": { "target": "Default", - "source": "items = [{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"Conditions\"\n conditions = [{\n target: \"CompositeAndClaim\"\n force: False\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"FailedToCreate\"\n message: \"Encountered an error creating the database\"\n }\n },{\n target: \"Composite\"\n force: False\n condition = {\n type: \"ValidationError\"\n status: \"False\"\n reason: \"FailedToValidate\"\n message: \"Encountered an error during validation\"\n }\n }]\n}]" + "source": "items = [{\n apiVersion: \"meta.krm.kcl.dev/v1alpha1\"\n kind: \"Conditions\"\n conditions = [{\n target: \"CompositeAndClaim\"\n force: False\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"FailedToCreate\"\n message: \"Encountered an error creating the database\"\n }\n },{\n target: \"Composite\"\n force: False\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"FailedToValidate\"\n message: \"Encountered an error during validation\"\n }\n }]\n}]" } }`), }, @@ -470,10 +470,51 @@ func TestRunFunctionSimple(t *testing.T) { Reason: "FailedToCreate", Message: ptr.To("Encountered an error creating the database"), Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), - }, { - Type: "ValidationError", + }, + }, + Results: []*fnv1.Result{}, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"","kind":""}`), + }, + Resources: map[string]*fnv1.Resource{}, + }, + }, + }, + }, + "OberwriteCondition": { + reason: "The Function should overwrite the first condition with the same target.", + args: args{ + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "overwrite-conditions"}, + 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: \"Conditions\"\n conditions = [{\n target: \"CompositeAndClaim\"\n force: False\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"FailedToCreate\"\n message: \"Encountered an error creating the database\"\n }\n },{\n target: \"Composite\"\n force: True\n condition = {\n type: \"DatabaseReady\"\n status: \"False\"\n reason: \"DatabaseValidation\"\n message: \"Encountered an error during validation\"\n }\n }]\n}]" + } + }`), + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: "overwrite-conditions", Ttl: durationpb.New(response.DefaultTTL)}, + Conditions: []*fnv1.Condition{ + { + Type: "DatabaseReady", + Status: fnv1.Status_STATUS_CONDITION_FALSE, + Reason: "FailedToCreate", + Message: ptr.To("Encountered an error creating the database"), + Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + }, + { + Type: "DatabaseReady", Status: fnv1.Status_STATUS_CONDITION_FALSE, - Reason: "FailedToValidate", + Reason: "DatabaseValidation", Message: ptr.To("Encountered an error during validation"), Target: fnv1.Target_TARGET_COMPOSITE.Enum(), }, @@ -512,13 +553,13 @@ func TestRunFunctionSimple(t *testing.T) { Conditions: []*fnv1.Condition{}, Results: []*fnv1.Result{ { - Severity: 0, + Severity: fnv1.Severity_SEVERITY_WARNING, Message: "The resource limit has been exceeded", Reason: ptr.To("ResourceLimitExceeded"), Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), }, { - Severity: 0, + Severity: fnv1.Severity_SEVERITY_WARNING, Message: "The validation failed", Reason: ptr.To("ValidationFailed"), Target: fnv1.Target_TARGET_COMPOSITE.Enum(), diff --git a/go.mod b/go.mod index fcba5e6..7ad3baf 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( google.golang.org/protobuf v1.36.1 gopkg.in/yaml.v2 v2.4.0 k8s.io/apimachinery v0.32.0 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 kcl-lang.io/krm-kcl v0.11.0 sigs.k8s.io/controller-tools v0.17.0 sigs.k8s.io/yaml v1.4.0 @@ -205,7 +206,6 @@ require ( k8s.io/component-base v0.32.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect kcl-lang.io/cli v0.11.0 // indirect kcl-lang.io/kcl-go v0.11.0 // indirect kcl-lang.io/kcl-openapi v0.10.0 // indirect diff --git a/package/input/template.fn.crossplane.io_kclinputs.yaml b/package/input/template.fn.crossplane.io_kclinputs.yaml index ad029ac..765f1a3 100644 --- a/package/input/template.fn.crossplane.io_kclinputs.yaml +++ b/package/input/template.fn.crossplane.io_kclinputs.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.16.5 name: kclinputs.template.fn.crossplane.io spec: group: template.fn.crossplane.io diff --git a/pkg/resource/events.go b/pkg/resource/events.go index 4cbd4c9..1af7872 100644 --- a/pkg/resource/events.go +++ b/pkg/resource/events.go @@ -1,6 +1,7 @@ package resource import ( + "github.com/crossplane/crossplane-runtime/pkg/errors" fnv1 "github.com/crossplane/function-sdk-go/proto/v1" "k8s.io/utils/ptr" ) @@ -40,28 +41,35 @@ type CreateEvent struct { Event Event `json:"event"` } -func SetEvents(rsp *fnv1.RunFunctionResponse, ers EventResources) { +func SetEvents(rsp *fnv1.RunFunctionResponse, ers EventResources) error { for _, er := range ers { - r := transformEvent(er) - + r, err := transformEvent(er) + if err != nil { + return err + } rsp.Results = append(rsp.Results, r) } + return nil } -func transformEvent(ec CreateEvent) *fnv1.Result { +func transformEvent(ec CreateEvent) (*fnv1.Result, error) { e := &fnv1.Result{ Reason: ec.Event.Reason, Target: transformTarget(ec.Target), } - switch ptr.Deref(ec.Event.Type, EventTypeNormal) { + deref := ptr.Deref(ec.Event.Type, EventTypeNormal) + switch deref { case EventTypeNormal: e.Severity = fnv1.Severity_SEVERITY_NORMAL + break case EventTypeWarning: - default: e.Severity = fnv1.Severity_SEVERITY_WARNING + break + default: + return &fnv1.Result{}, errors.Errorf("invalid type %s, must be one of [Normal, Warning]", *ec.Event.Type) } e.Message = ec.Event.Message - return e + return e, nil } From 467dd83917f63edbaba84ac952905bb1157e5d5c Mon Sep 17 00:00:00 2001 From: Mark Altmann Date: Thu, 2 Jan 2025 16:56:58 +0100 Subject: [PATCH 4/6] chore: docs Signed-off-by: Mark Altmann --- README.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 81e8c53..b47c3cc 100644 --- a/README.md +++ b/README.md @@ -513,6 +513,8 @@ bar: # Omitted for brevity ``` You can access the retrieved resources in your code like this: +> Note that Crossplane performs an additional reconciliation pass for extra resources. +> Consequently, during the initial execution, these resources may be uninitialized. It is essential to implement checks to handle this scenario. ```yaml apiVersion: krm.kcl.dev/v1alpha1 kind: KCLInput @@ -522,7 +524,8 @@ spec: source: | er = option("params")?.extraResources - name = er?.bar[0]?.Resource?.metadata?.name or "" + if er?.bar: + name = er?.bar[0]?.Resource?.metadata?.name or "" # Omit other logic ``` @@ -544,6 +547,89 @@ spec: items = [dxr] # Omit other resources ``` +### Settings conditions and events + +> This feature requires Crossplane v1.17 or newer. + +You can set conditions and events directly from KCL, either in the composite resource or both the composite and claim resources. +To set one or more conditions, use the following approach: +```yaml +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 + } + + conditions = { + apiVersion: "meta.krm.kcl.dev/v1alpha1" + kind: "Conditions" + conditions = [ + { + target: "CompositeAndClaim" + force: False + condition = { + type: "DatabaseReady" + status: "False" + reason: "FailedToCreate" + message: "Encountered an error creating the database" + } + } + ] + } + + items = [ + conditions + dxr + ] +``` + +- **target**: Specifies whether the condition should be present in the composite resource or both the composite and claim resources. Possible values are `CompositeAndClaim` and `Composite` +- **force**: Forces the overwrite of existing conditions. If a condition with the same `type` already exists, it will not be overwritten by default. Setting force to `True` will overwrite the first condition. + +You can also set events as follows: +```yaml +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 + } + + events = { + apiVersion: "meta.krm.kcl.dev/v1alpha1" + kind: "Events" + events = [ + { + target: "CompositeAndClaim" + event = { + type: "Warning" + reason: "ResourceLimitExceeded" + message: "The resource limit has been exceeded" + } + } + ] + } + items = [ + events + dxr + ] +``` + ## Library You can directly use [KCL standard libraries](https://kcl-lang.io/docs/reference/model/overview) such as `regex.match`, `math.log`. From 1bb6428e23f5020268729193eb5d5f898b582368 Mon Sep 17 00:00:00 2001 From: Mark Altmann Date: Fri, 3 Jan 2025 09:45:31 +0100 Subject: [PATCH 5/6] fix: improve error handling Signed-off-by: Mark Altmann --- fn.go | 7 +++++-- pkg/resource/conditions.go | 11 +++++++++-- pkg/resource/events.go | 4 +++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/fn.go b/fn.go index 64a8322..d80e006 100644 --- a/fn.go +++ b/fn.go @@ -200,13 +200,16 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) } if len(conditions) > 0 { - pkgresource.SetConditions(rsp, conditions, log) + err := pkgresource.SetConditions(rsp, conditions, log) + if err != nil { + return rsp, nil + } } if len(events) > 0 { err := pkgresource.SetEvents(rsp, events) if err != nil { - return rsp, err + return rsp, nil } } diff --git a/pkg/resource/conditions.go b/pkg/resource/conditions.go index 811a16d..88a2c44 100644 --- a/pkg/resource/conditions.go +++ b/pkg/resource/conditions.go @@ -1,8 +1,11 @@ package resource import ( + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/logging" fnv1 "github.com/crossplane/function-sdk-go/proto/v1" + "github.com/crossplane/function-sdk-go/response" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" ) @@ -78,10 +81,14 @@ func transformTarget(t *BindingTarget) *fnv1.Target { return fnv1.Target_TARGET_COMPOSITE.Enum() } -func SetConditions(rsp *fnv1.RunFunctionResponse, cr ConditionResources, log logging.Logger) { +func SetConditions(rsp *fnv1.RunFunctionResponse, cr ConditionResources, log logging.Logger) error { conditionsSet := map[string]bool{} // All matchConditions matched, set the desired conditions. for _, cs := range cr { + if xpv1.IsSystemConditionType(xpv1.ConditionType(cs.Condition.Type)) { + response.Fatal(rsp, errors.Errorf("cannot set ClaimCondition type: %s is a reserved Crossplane Condition", cs.Condition.Type)) + return errors.New("error updating response") + } if conditionsSet[cs.Condition.Type] && (cs.Force == nil || !*cs.Force) { // The condition is already set and this setter is not forceful. log.Debug("skipping because condition is already set and setCondition is not forceful") @@ -90,8 +97,8 @@ func SetConditions(rsp *fnv1.RunFunctionResponse, cr ConditionResources, log log log.Debug("setting condition") c := transformCondition(cs) - rsp.Conditions = append(rsp.Conditions, c) conditionsSet[cs.Condition.Type] = true } + return nil } diff --git a/pkg/resource/events.go b/pkg/resource/events.go index 1af7872..f6c4204 100644 --- a/pkg/resource/events.go +++ b/pkg/resource/events.go @@ -3,6 +3,7 @@ package resource import ( "github.com/crossplane/crossplane-runtime/pkg/errors" fnv1 "github.com/crossplane/function-sdk-go/proto/v1" + "github.com/crossplane/function-sdk-go/response" "k8s.io/utils/ptr" ) @@ -45,7 +46,8 @@ func SetEvents(rsp *fnv1.RunFunctionResponse, ers EventResources) error { for _, er := range ers { r, err := transformEvent(er) if err != nil { - return err + response.Fatal(rsp, err) + return errors.New("error updating response") } rsp.Results = append(rsp.Results, r) } From a7bc48bda814137b46a7f45fc6210f9b19711b72 Mon Sep 17 00:00:00 2001 From: Mark Altmann Date: Fri, 3 Jan 2025 10:01:35 +0100 Subject: [PATCH 6/6] fix: improve docs Signed-off-by: Mark Altmann --- package/input/template.fn.crossplane.io_kclinputs.yaml | 2 +- pkg/resource/conditions.go | 5 ++++- pkg/resource/events.go | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/package/input/template.fn.crossplane.io_kclinputs.yaml b/package/input/template.fn.crossplane.io_kclinputs.yaml index 765f1a3..9fce3c6 100644 --- a/package/input/template.fn.crossplane.io_kclinputs.yaml +++ b/package/input/template.fn.crossplane.io_kclinputs.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.5 + controller-gen.kubebuilder.io/version: v0.17.0 name: kclinputs.template.fn.crossplane.io spec: group: template.fn.crossplane.io diff --git a/pkg/resource/conditions.go b/pkg/resource/conditions.go index 88a2c44..59312f9 100644 --- a/pkg/resource/conditions.go +++ b/pkg/resource/conditions.go @@ -26,7 +26,7 @@ type ConditionResources []ConditionResource // ConditionResource will set a condition on the target. type ConditionResource struct { // The target(s) to receive the condition. Can be Composite or - // CompositeAndClaim. + // CompositeAndClaim. Defaults to Composite Target *BindingTarget `json:"target"` // If true, the condition will override a condition of the same Type. Defaults // to false. @@ -50,6 +50,7 @@ type Condition struct { Message *string `json:"message"` } +// transformCondition converts a ConditionResource into an fnv1.Condition while mapping status and target accordingly. func transformCondition(cs ConditionResource) *fnv1.Condition { c := &fnv1.Condition{ Type: cs.Condition.Type, @@ -81,6 +82,8 @@ func transformTarget(t *BindingTarget) *fnv1.Target { return fnv1.Target_TARGET_COMPOSITE.Enum() } +// SetConditions updates the RunFunctionResponse with specified conditions from ConditionResources, ensuring no duplicates. +// It validates that system-reserved Crossplane condition types are not set and permits forced updates when specified. func SetConditions(rsp *fnv1.RunFunctionResponse, cr ConditionResources, log logging.Logger) error { conditionsSet := map[string]bool{} // All matchConditions matched, set the desired conditions. diff --git a/pkg/resource/events.go b/pkg/resource/events.go index f6c4204..bb0c61d 100644 --- a/pkg/resource/events.go +++ b/pkg/resource/events.go @@ -35,13 +35,15 @@ type Event struct { // CreateEvent will create an event for the target(s). type CreateEvent struct { // The target(s) to create an event for. Can be Composite or - // CompositeAndClaim. + // CompositeAndClaim. Defaults to Composite Target *BindingTarget `json:"target"` // Event to create. Event Event `json:"event"` } +// SetEvents processes a list of EventResources, transforms them into Results, and appends them to the RunFunctionResponse. +// Returns an error if any transformation fails. func SetEvents(rsp *fnv1.RunFunctionResponse, ers EventResources) error { for _, er := range ers { r, err := transformEvent(er) @@ -54,6 +56,8 @@ func SetEvents(rsp *fnv1.RunFunctionResponse, ers EventResources) error { return nil } +// transformEvent converts a CreateEvent into a fnv1.Result object, handling event severity, reason, message, and target. +// Returns a fnv1.Result object or an error if the event type is invalid. func transformEvent(ec CreateEvent) (*fnv1.Result, error) { e := &fnv1.Result{ Reason: ec.Event.Reason,