From aa28aecfcbeb9bc89b066b4d9162659b0ef15b64 Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Sat, 13 Dec 2025 16:20:54 -0600 Subject: [PATCH 1/2] Add support for creating Usages for deletion sequencing Signed-off-by: Bob Haddleton --- README.md | 66 +- example/README.md | 267 +++++++- .../composition-with-deletion-sequencing.yaml | 82 +++ example/observed-deletion-sequencing.yaml | 83 +++ fn.go | 155 ++++- fn_test.go | 573 +++++++++++++++++- go.mod | 21 +- go.sum | 54 +- input/v1beta1/input.go | 22 + name.go | 36 ++ .../sequencer.fn.crossplane.io_inputs.yaml | 13 + 11 files changed, 1328 insertions(+), 44 deletions(-) create mode 100644 example/composition-with-deletion-sequencing.yaml create mode 100644 example/observed-deletion-sequencing.yaml create mode 100644 name.go diff --git a/README.md b/README.md index 90f2347..b3f3a35 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # function-sequencer Function Sequencer is a Crossplane function that enables Composition authors to define sequencing rules delaying the -creation of resources until other resources are ready. +creation of resources until other resources are ready. The same sequencing rules can be used, in reverse, to define the order that resources are deleted, when foreground cascading deletion is used. -For example, the pipeline step below, will ensure that `second-resource` and `third-resource` not to be created until +For example, the pipeline step below will ensure that `second-resource` and `third-resource` not to be created until the `first-resource` is ready. ```yaml @@ -71,6 +71,68 @@ state prematurely when there are pending resources that the composite reconciler - first-subresource-.* - second-resource ``` +## Deletion Sequencing +The same rule sequences can be used to determine the order in which the resources should be deleted. +Deletion Sequencing is enabled by setting the `enableDeletionSequencing` input to `true` and causes the function to create +`Usage` and `ClusterUsage` resources to enforce the proper order of resource deletion. + +Deletion Sequencing requires that +[foreground cascading deletion](https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion) +is used when the composite resource is deleted. + +The `usageVersion` input attribute controls +whether the function creates Crossplane v1 `Usage.apiextensions.crossplane.io` resources or Crossplane v2 +`Usage.protection.crossplane.io` and `ClusterUsage.protection.crossplane.io` resources. + +The `replayDeletion` input is mapped to the `Usage`/`ClusterUsage` `replayDeletion` attribute which determines whether +deletion of a resource is retried after the initial attempt. This can significantly reduce the amount of time +required to delete all the resources in a composite and defaults to `true` for the deletion +sequencing use case. This can be disabled by setting `replayDeletion` to `false`. + +```yaml + - step: sequence-creation-and-deletion + functionRef: + name: function-sequencer + input: + apiVersion: sequencer.fn.crossplane.io/v1beta1 + kind: Input + enableDeletionSequencing: true + replayDeletion: true + rules: + - sequence: + - first + - second + - third + usageVersion: v1 +``` +creates two `Usage` resources, one for the `third`->`second` dependency and one for the `second`->`first` dependency. +When the composite is deleted with the option `--cascade=foreground` the `third` resource will be deleted, followed by +the `second` and finally the `first`. + +### Regular Expressions + +Deletion sequencing creates `Usage`/`ClusterUsage` resources for all dependencies identified by the input sequences, including +those defined by pattern matching. For example: + +```yaml + - step: sequence-creation-and-deletion + functionRef: + name: function-sequencer + input: + apiVersion: sequencer.fn.crossplane.io/v1beta1 + kind: Input + enableDeletionSequencing: true + replayDeletion: true + rules: + - sequence: + - first-subresource-.* + - second-resource + usageVersion: v1 +``` + +creates a `Usage` resource for every resource that matches `first-subresource-*`, with `by` set to the `second-resource`. +This ensures that `second-resource` is deleted before any of the `first-resource-*` resources are deleted. + ## Installation It can be installed as follows from the Upbound marketplace: https://marketplace.upbound.io/functions/crossplane-contrib/function-sequencer diff --git a/example/README.md b/example/README.md index 8b6a134..c449b70 100644 --- a/example/README.md +++ b/example/README.md @@ -3,14 +3,16 @@ You can run your function locally and test it using `crossplane beta render` with these example manifests. +### Run the function locally + ```shell -# Run the function locally $ go run . --insecure --debug ``` +### Then, in another terminal, call it with these example manifests + ```shell -# Then, in another terminal, call it with these example manifests -$ crossplane beta render xr.yaml composition.yaml functions.yaml -r +$ crossplane render xr.yaml composition.yaml functions.yaml -r --- apiVersion: example.crossplane.io/v1 kind: XR @@ -23,3 +25,262 @@ message: I was run with input "Hello world"! severity: SEVERITY_NORMAL step: run-the-template ``` + +### Regex Pattern Example +```shell +$ crossplane render xr.yaml composition-regex.yaml functions.yaml -r -o observed-regex.yaml +--- +apiVersion: example.crossplane.io/v1 +kind: XR +metadata: + name: example-xr +status: + conditions: + - lastTransitionTime: "2024-01-01T00:00:00Z" + message: 'Unready resources: second-object' + reason: Creating + status: "False" + type: Ready +--- +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/composition-resource-name: first-subresource-1 + labels: + crossplane.io/composite: example-xr + name: first-subresource-1 + ownerReferences: + - apiVersion: example.crossplane.io/v1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example-xr + uid: "" +spec: + forProvider: + conditionAfter: + - conditionStatus: "False" + conditionType: Ready + time: 5s + - conditionStatus: "True" + conditionType: Ready + time: 10s + - conditionStatus: "False" + conditionType: Ready + time: 30s + - conditionStatus: "True" + conditionType: Ready + time: 90s +--- +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/composition-resource-name: first-subresource-2 + labels: + crossplane.io/composite: example-xr + name: first-subresource-2 + ownerReferences: + - apiVersion: example.crossplane.io/v1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example-xr + uid: "" +spec: + forProvider: + conditionAfter: + - conditionStatus: "False" + conditionType: Ready + time: 5s + - conditionStatus: "True" + conditionType: Ready + time: 10s +--- +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/composition-resource-name: second-object + labels: + crossplane.io/composite: example-xr + name: second-object + ownerReferences: + - apiVersion: example.crossplane.io/v1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example-xr + uid: "" +spec: + forProvider: + conditionAfter: + - conditionStatus: "False" + conditionType: Ready + time: 5s + - conditionStatus: "True" + conditionType: Ready + time: 10s +--- +apiVersion: render.crossplane.io/v1beta1 +kind: Result +message: Delaying creation of resource(s) matching "third-resource" because "object$" + is not fully ready (0 of 1) +severity: SEVERITY_NORMAL +step: sequence-creation +``` +### Deletion Sequencing Example +```shell +$ crossplane render xr.yaml composition-with-deletion-sequencing.yaml functions.yaml -r -o observed-deletion-sequencing.yaml +--- +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 +--- +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/composition-resource-name: first-resource + labels: + crossplane.io/composite: example-xr + name: first + ownerReferences: + - apiVersion: example.crossplane.io/v1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example-xr + uid: "" +spec: + forProvider: + conditionAfter: + - conditionStatus: "False" + conditionType: Ready + time: 5s + - conditionStatus: "True" + conditionType: Ready + time: 10s + - conditionStatus: "False" + conditionType: Ready + time: 30s + - conditionStatus: "True" + conditionType: Ready + time: 90s +--- +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/composition-resource-name: second-resource + labels: + crossplane.io/composite: example-xr + name: second + ownerReferences: + - apiVersion: example.crossplane.io/v1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example-xr + uid: "" +spec: + forProvider: + conditionAfter: + - conditionStatus: "False" + conditionType: Ready + time: 5s + - conditionStatus: "True" + conditionType: Ready + time: 10s +--- +apiVersion: apiextensions.crossplane.io/v1beta1 +kind: Usage +metadata: + annotations: + crossplane.io/composition-resource-name: second-resource-first-resource-usage + labels: + crossplane.io/composite: example-xr + name: nopresource-second-nopresource-first-4f1a57-dependency + ownerReferences: + - apiVersion: example.crossplane.io/v1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example-xr + uid: "" +spec: + by: + apiVersion: nop.crossplane.io/v1alpha1 + kind: NopResource + resourceRef: + name: second + of: + apiVersion: nop.crossplane.io/v1alpha1 + kind: NopResource + resourceRef: + name: first + reason: dependency + replayDeletion: true +--- +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/composition-resource-name: third-resource + labels: + crossplane.io/composite: example-xr + name: third + ownerReferences: + - apiVersion: example.crossplane.io/v1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example-xr + uid: "" +spec: + forProvider: + conditionAfter: + - conditionStatus: "False" + conditionType: Ready + time: 5s + - conditionStatus: "True" + conditionType: Ready + time: 10s +--- +apiVersion: apiextensions.crossplane.io/v1beta1 +kind: Usage +metadata: + annotations: + crossplane.io/composition-resource-name: third-resource-first-resource-usage + labels: + crossplane.io/composite: example-xr + name: nopresource-third-nopresource-first-1b4fc5-dependency + ownerReferences: + - apiVersion: example.crossplane.io/v1 + blockOwnerDeletion: true + controller: true + kind: XR + name: example-xr + uid: "" +spec: + by: + apiVersion: nop.crossplane.io/v1alpha1 + kind: NopResource + resourceRef: + name: third + of: + apiVersion: nop.crossplane.io/v1alpha1 + kind: NopResource + resourceRef: + name: first + reason: dependency + replayDeletion: true +``` diff --git a/example/composition-with-deletion-sequencing.yaml b/example/composition-with-deletion-sequencing.yaml new file mode 100644 index 0000000..64dd26e --- /dev/null +++ b/example/composition-with-deletion-sequencing.yaml @@ -0,0 +1,82 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: function-sequencer-with-deletion +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: XR + mode: Pipeline + pipeline: + - step: patch-and-transform + functionRef: + name: function-patch-and-transform + input: + apiVersion: pt.fn.crossplane.io/v1beta1 + kind: Resources + resources: + - name: first-resource + base: + apiVersion: nop.crossplane.io/v1alpha1 + kind: NopResource + spec: + forProvider: + conditionAfter: + - time: 5s + conditionType: Ready + conditionStatus: "False" + - time: 10s + conditionType: Ready + conditionStatus: "True" + # We should not delete the dependent resources if this turns back to unready. + - time: 30s + conditionType: Ready + conditionStatus: "False" + - time: 90s + conditionType: Ready + conditionStatus: "True" + - name: second-resource + base: + apiVersion: nop.crossplane.io/v1alpha1 + kind: NopResource + spec: + forProvider: + conditionAfter: + - time: 5s + conditionType: Ready + conditionStatus: "False" + - time: 10s + conditionType: Ready + conditionStatus: "True" + - name: third-resource + base: + apiVersion: nop.crossplane.io/v1alpha1 + kind: NopResource + spec: + forProvider: + conditionAfter: + - time: 5s + conditionType: Ready + conditionStatus: "False" + - time: 10s + conditionType: Ready + conditionStatus: "True" + - step: detect-readiness + functionRef: + name: function-auto-ready + - step: sequence-creation + functionRef: + name: function-sequencer + input: + apiVersion: sequencer.fn.crossplane.io/v1beta1 + kind: Input + enableDeletionSequencing: true + replayDeletion: true + rules: + - sequence: + - first-resource + - second-resource + - sequence: + - first-resource + - third-resource + usageVersion: v1 diff --git a/example/observed-deletion-sequencing.yaml b/example/observed-deletion-sequencing.yaml new file mode 100644 index 0000000..0441911 --- /dev/null +++ b/example/observed-deletion-sequencing.yaml @@ -0,0 +1,83 @@ +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/external-name: foo + crossplane.io/composition-resource-name: first-resource + name: first +spec: + forProvider: + conditionAfter: + - conditionStatus: "True" + conditionType: Ready + time: 1s + - conditionStatus: "False" + conditionType: Ready + time: 0s +status: + atProvider: {} + conditions: + - lastTransitionTime: "2024-02-17T11:56:27Z" + reason: ReconcileSuccess + status: "True" + type: Synced + - lastTransitionTime: "2024-02-17T11:56:28Z" + reason: "" + status: "True" + type: Ready +--- +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/external-name: bar + crossplane.io/composition-resource-name: second-resource + name: second +spec: + forProvider: + conditionAfter: + - conditionStatus: "True" + conditionType: Ready + time: 1s + - conditionStatus: "False" + conditionType: Ready + time: 0s +status: + atProvider: {} + conditions: + - lastTransitionTime: "2024-02-17T11:56:27Z" + reason: ReconcileSuccess + status: "True" + type: Synced + - lastTransitionTime: "2024-02-17T11:56:28Z" + reason: "" + status: "True" + type: Ready +--- +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/external-name: baz + crossplane.io/composition-resource-name: third-resource + name: third +spec: + forProvider: + conditionAfter: + - conditionStatus: "True" + conditionType: Ready + time: 1s + - conditionStatus: "False" + conditionType: Ready + time: 0s +status: + atProvider: {} + conditions: + - lastTransitionTime: "2024-02-17T11:56:27Z" + reason: ReconcileSuccess + status: "True" + type: Synced + - lastTransitionTime: "2024-02-17T11:56:28Z" + reason: "" + status: "True" + type: Ready diff --git a/fn.go b/fn.go index 4b3db6a..be35ec8 100644 --- a/fn.go +++ b/fn.go @@ -2,17 +2,23 @@ package main import ( "context" + "encoding/json" "fmt" + "maps" "regexp" "strings" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + apiextensionsv1beta1 "github.com/crossplane/crossplane/v2/apis/apiextensions/v1beta1" + protectionv1beta1 "github.com/crossplane/crossplane/v2/apis/protection/v1beta1" "github.com/crossplane/function-sequencer/input/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" v1 "github.com/crossplane/function-sdk-go/proto/v1" "github.com/crossplane/function-sdk-go/request" "github.com/crossplane/function-sdk-go/resource" + "github.com/crossplane/function-sdk-go/resource/composed" "github.com/crossplane/function-sdk-go/response" ) @@ -30,6 +36,16 @@ const ( END = "$" ) +const ( + DependencyReason = "dependency" + ProtectionGroupVersion = protectionv1beta1.Group + "/" + protectionv1beta1.Version + ProtectionV1GroupVersion = apiextensionsv1beta1.Group + "/" + apiextensionsv1beta1.Version + // UsageNameSuffix is the suffix applied when generating Usage names. + UsageNameSuffix = "dependency" + // V1ModeError Error when trying to protect a namespaced resource when in v1 mode. + V1ModeError = "cannot protect namespaced resource (kind: %s, name: %s, namespace: %s) with enableV1Mode=true. v1 usages only support cluster-scoped resources." +) + // RunFunction runs the Function. func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (*v1.RunFunctionResponse, error) { //nolint:gocognit // This function is unavoidably complex. f.log.Info("Running function", "tag", req.GetMeta().GetTag()) @@ -59,6 +75,7 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* for _, rule := range in.Rules { sequences = append(sequences, rule.Sequence) } + usages := make(map[resource.Name]*resource.DesiredComposed) for _, sequence := range sequences { for i, r := range sequence { @@ -67,6 +84,30 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* continue } if _, created := observedComposed[r]; created { + f.log.Debug("Processing ", "r:", r) + if in.EnableDeletionSequencing { + of := sequence[i-1] + ofRegex, err := getStrictRegex(string(of)) + if err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot compile regex %s", of)) + return rsp, nil + } + for k := range desiredComposed { + if ofRegex.MatchString(string(k)) { + if _, ok := observedComposed[k]; ok { + f.log.Debug("Generate Usage", "of:", k, "by r:", r) + usage := GenerateUsage(&observedComposed[k].Resource.Unstructured, &observedComposed[r].Resource.Unstructured, in.ReplayDeletion, in.UsageVersion) + usageComposed := composed.New() + if err := convertViaJSON(usageComposed, usage); err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot convert to JSON %s", usage)) + return rsp, err + } + f.log.Debug("created usage", "kind", usageComposed.GetKind(), "name", usageComposed.GetName(), "namespace", usageComposed.GetNamespace()) + usages[r+"-"+k+"-usage"] = &resource.DesiredComposed{Resource: usageComposed, Ready: resource.ReadyTrue} + } + } + } + } // We've already created this resource, so we don't need to do anything. // We only sequence creation of resources that don't exist yet. continue @@ -95,6 +136,12 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* } } + currentRegex, err := getStrictRegex(string(r)) + if err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot compile regex %s", r)) + return rsp, nil + } + if desired == 0 || desired != readyResources { // no resource created msg := fmt.Sprintf("Delaying creation of resource(s) matching %q because %q does not exist yet", r, before) @@ -111,7 +158,6 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* response.Normal(rsp, msg) f.log.Info(msg) // find all objects that match the regex and delete them from the desiredComposed map - currentRegex, _ := getStrictRegex(string(r)) for k := range desiredComposed { if currentRegex.MatchString(string(k)) { if _, ok := observedComposed[k]; ok { @@ -127,10 +173,27 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* } break } + if in.EnableDeletionSequencing { + for c := range observedComposed { + if currentRegex.MatchString(string(c)) { + for _, k := range keys { + f.log.Debug("Generate Usage of ", "k:", k, "by c:", c) + usage := GenerateUsage(&observedComposed[k].Resource.Unstructured, &observedComposed[c].Resource.Unstructured, in.ReplayDeletion, in.UsageVersion) + usageComposed := composed.New() + if err := convertViaJSON(usageComposed, usage); err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot convert to JSON %s", usage)) + return rsp, err + } + f.log.Debug("created usage", "kind", usageComposed.GetKind(), "name", usageComposed.GetName(), "namespace", usageComposed.GetNamespace()) + usages[c+"-"+k+"-usage"] = &resource.DesiredComposed{Resource: usageComposed, Ready: resource.ReadyTrue} + } + } + } + } } } } - + maps.Copy(desiredComposed, usages) rsp.Desired.Resources = nil return rsp, response.SetDesiredComposedResources(rsp, desiredComposed) } @@ -144,3 +207,91 @@ func getStrictRegex(pattern string) (*regexp.Regexp, error) { } return regexp.Compile(pattern) } + +// GenerateUsage determines whether to return a v1 or v2 Crossplane usage. +func GenerateUsage(of *unstructured.Unstructured, by *unstructured.Unstructured, rd bool, usageVersion v1beta1.UsageVersion) map[string]any { + if usageVersion == v1beta1.UsageV1 { + return GenerateV1Usage(of, by, rd) + } + return GenerateV2Usage(of, by, rd) +} + +// GenerateV2Usage creates a v2 Usage for a resource. +func GenerateV2Usage(of *unstructured.Unstructured, by *unstructured.Unstructured, rd bool) map[string]any { + name := strings.ToLower(by.GetKind() + "-" + by.GetName() + "-" + of.GetKind() + "-" + of.GetName()) + usageType := protectionv1beta1.ClusterUsageKind + usageMeta := map[string]any{ + "name": GenerateName(name, UsageNameSuffix), + } + + namespace := of.GetNamespace() + if namespace != "" { + usageType = protectionv1beta1.UsageKind + usageMeta["namespace"] = namespace + } + + usage := map[string]any{ + "apiVersion": ProtectionGroupVersion, + "kind": usageType, + "metadata": usageMeta, + "spec": map[string]any{ + "by": map[string]any{ + "apiVersion": by.GetAPIVersion(), + "kind": by.GetKind(), + "resourceRef": map[string]any{ + "name": by.GetName(), + }, + }, + "of": map[string]any{ + "apiVersion": of.GetAPIVersion(), + "kind": of.GetKind(), + "resourceRef": map[string]any{ + "name": of.GetName(), + }, + }, + "reason": DependencyReason, + "replayDeletion": rd, + }, + } + return usage +} + +// GenerateV1Usage creates a Crossplane v1 Usage for a resource. +// Only Cluster Scoped Resources are supported. +func GenerateV1Usage(of *unstructured.Unstructured, by *unstructured.Unstructured, rd bool) map[string]any { + name := strings.ToLower(by.GetKind() + "-" + by.GetName() + "-" + of.GetKind() + "-" + of.GetName()) + usage := map[string]any{ + "apiVersion": ProtectionV1GroupVersion, + "kind": apiextensionsv1beta1.UsageKind, + "metadata": map[string]any{ + "name": GenerateName(name, UsageNameSuffix), + }, + "spec": map[string]any{ + "by": map[string]any{ + "apiVersion": by.GetAPIVersion(), + "kind": by.GetKind(), + "resourceRef": map[string]any{ + "name": by.GetName(), + }, + }, + "of": map[string]any{ + "apiVersion": of.GetAPIVersion(), + "kind": of.GetKind(), + "resourceRef": map[string]any{ + "name": of.GetName(), + }, + }, + "reason": DependencyReason, + "replayDeletion": rd, + }, + } + return usage +} + +func convertViaJSON(to, from any) error { + bs, err := json.Marshal(from) + if err != nil { + return err + } + return json.Unmarshal(bs, to) +} diff --git a/fn_test.go b/fn_test.go index dd2038b..bd35bb3 100644 --- a/fn_test.go +++ b/fn_test.go @@ -18,8 +18,16 @@ import ( func TestRunFunction(t *testing.T) { var ( - xr = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2}}` - mr = `{"apiVersion":"example.org/v1","kind":"MR","metadata":{"name":"cool-mr"}}` + xr = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2}}` + mr = `{"apiVersion":"example.org/v1","kind":"MR","metadata":{"name":"cool-mr"}}` + nxr = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr","namespace":"cool-namespace"},"spec":{"count":2}}` + nmr = `{"apiVersion":"example.org/v1","kind":"MR","metadata":{"name":"cool-mr","namespace":"cool-namespace"}}` + uv1 = `{"apiVersion":"apiextensions.crossplane.io/v1beta1","kind":"Usage","metadata":{"name":"mr-cool-mr-xr-cool-xr-d9f469-dependency"},"spec":{"by":{"apiVersion":"example.org/v1","kind":"MR","resourceRef":{"name":"cool-mr"}},"of":{"apiVersion":"example.org/v1","kind":"XR","resourceRef":{"name":"cool-xr"}},"reason":"dependency","replayDeletion":true}}` + u2v1 = `{"apiVersion":"apiextensions.crossplane.io/v1beta1","kind":"Usage","metadata":{"name":"mr-cool-mr-mr-cool-mr-91201d-dependency"},"spec":{"by":{"apiVersion":"example.org/v1","kind":"MR","resourceRef":{"name":"cool-mr"}},"of":{"apiVersion":"example.org/v1","kind":"MR","resourceRef":{"name":"cool-mr"}},"reason":"dependency","replayDeletion":true}}` + uv2 = `{"apiVersion":"protection.crossplane.io/v1beta1","kind":"ClusterUsage","metadata":{"name":"mr-cool-mr-xr-cool-xr-d9f469-dependency"},"spec":{"by":{"apiVersion":"example.org/v1","kind":"MR","resourceRef":{"name":"cool-mr"}},"of":{"apiVersion":"example.org/v1","kind":"XR","resourceRef":{"name":"cool-xr"}},"reason":"dependency","replayDeletion":true}}` + u2v2 = `{"apiVersion":"protection.crossplane.io/v1beta1","kind":"ClusterUsage","metadata":{"name":"mr-cool-mr-mr-cool-mr-91201d-dependency"},"spec":{"by":{"apiVersion":"example.org/v1","kind":"MR","resourceRef":{"name":"cool-mr"}},"of":{"apiVersion":"example.org/v1","kind":"MR","resourceRef":{"name":"cool-mr"}},"reason":"dependency","replayDeletion":true}}` + nuv2 = `{"apiVersion":"protection.crossplane.io/v1beta1","kind":"Usage","metadata":{"name":"mr-cool-mr-xr-cool-xr-d9f469-dependency","namespace":"cool-namespace"},"spec":{"by":{"apiVersion":"example.org/v1","kind":"MR","resourceRef":{"name":"cool-mr"}},"of":{"apiVersion":"example.org/v1","kind":"XR","resourceRef":{"name":"cool-xr"}},"reason":"dependency","replayDeletion":true}}` + nu2v2 = `{"apiVersion":"protection.crossplane.io/v1beta1","kind":"Usage","metadata":{"name":"mr-cool-mr-mr-cool-mr-91201d-dependency","namespace":"cool-namespace"},"spec":{"by":{"apiVersion":"example.org/v1","kind":"MR","resourceRef":{"name":"cool-mr"}},"of":{"apiVersion":"example.org/v1","kind":"MR","resourceRef":{"name":"cool-mr"}},"reason":"dependency","replayDeletion":true}}` ) target := v1.Target_TARGET_COMPOSITE @@ -1109,6 +1117,567 @@ func TestRunFunction(t *testing.T) { }, }, }, + "FirstReadyUsageV1": { + reason: "The function should create a V1 Usage when the first resource is ready", + args: args{ + req: &v1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + EnableDeletionSequencing: true, + ReplayDeletion: true, + Rules: []v1beta1.SequencingRule{ + { + Sequence: []resource.Name{ + "first", + "second", + }, + }, + }, + UsageVersion: v1beta1.UsageV1, + }), + Observed: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + want: want{ + rsp: &v1.RunFunctionResponse{ + Meta: &v1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*v1.Result{}, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-first-usage": { + Resource: resource.MustStructJSON(uv1), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + }, + "FirstReadyUsageV2Cluster": { + reason: "The function should create a V2 ClusterUsage when the first resource is ready", + args: args{ + req: &v1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + EnableDeletionSequencing: true, + ReplayDeletion: true, + Rules: []v1beta1.SequencingRule{ + { + Sequence: []resource.Name{ + "first", + "second", + }, + }, + }, + UsageVersion: v1beta1.UsageV2, + }), + Observed: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + want: want{ + rsp: &v1.RunFunctionResponse{ + Meta: &v1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*v1.Result{}, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-first-usage": { + Resource: resource.MustStructJSON(uv2), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + }, + "FirstReadyUsageV2Namespaced": { + reason: "The function should create a V2 Namespaced Usage when the first resource is ready", + args: args{ + req: &v1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + EnableDeletionSequencing: true, + ReplayDeletion: true, + Rules: []v1beta1.SequencingRule{ + { + Sequence: []resource.Name{ + "first", + "second", + }, + }, + }, + UsageVersion: v1beta1.UsageV2, + }), + Observed: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(nxr), + Ready: v1.Ready_READY_TRUE, + }, + "second": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(nxr), + Ready: v1.Ready_READY_TRUE, + }, + "second": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + want: want{ + rsp: &v1.RunFunctionResponse{ + Meta: &v1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*v1.Result{}, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(nxr), + Ready: v1.Ready_READY_TRUE, + }, + "second": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + "second-first-usage": { + Resource: resource.MustStructJSON(nuv2), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + }, + "MixedRegexUsageV1": { + reason: "The function should delay the creation of second and fourth resources because the first and third are not ready", + args: args{ + req: &v1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + EnableDeletionSequencing: true, + ReplayDeletion: true, + Rules: []v1beta1.SequencingRule{ + { + Sequence: []resource.Name{ + "first", + "second-.*", + "third", + }, + }, + }, + UsageVersion: v1beta1.UsageV1, + }), + Observed: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-1": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "third": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-1": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "third": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + want: want{ + rsp: &v1.RunFunctionResponse{ + Meta: &v1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*v1.Result{}, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-1": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0-first-usage": { + Resource: resource.MustStructJSON(uv1), + Ready: v1.Ready_READY_TRUE, + }, + "second-1-first-usage": { + Resource: resource.MustStructJSON(uv1), + Ready: v1.Ready_READY_TRUE, + }, + "third-second-0-usage": { + Resource: resource.MustStructJSON(u2v1), + Ready: v1.Ready_READY_TRUE, + }, + "third-second-1-usage": { + Resource: resource.MustStructJSON(u2v1), + Ready: v1.Ready_READY_TRUE, + }, + "third": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + }, + "MixedRegexUsageV2Cluster": { + reason: "The function should delay the creation of second and fourth resources because the first and third are not ready", + args: args{ + req: &v1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + EnableDeletionSequencing: true, + ReplayDeletion: true, + Rules: []v1beta1.SequencingRule{ + { + Sequence: []resource.Name{ + "first", + "second-.*", + "third", + }, + }, + }, + UsageVersion: v1beta1.UsageV2, + }), + Observed: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-1": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "third": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-1": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "third": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + want: want{ + rsp: &v1.RunFunctionResponse{ + Meta: &v1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*v1.Result{}, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-1": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0-first-usage": { + Resource: resource.MustStructJSON(uv2), + Ready: v1.Ready_READY_TRUE, + }, + "second-1-first-usage": { + Resource: resource.MustStructJSON(uv2), + Ready: v1.Ready_READY_TRUE, + }, + "third-second-0-usage": { + Resource: resource.MustStructJSON(u2v2), + Ready: v1.Ready_READY_TRUE, + }, + "third-second-1-usage": { + Resource: resource.MustStructJSON(u2v2), + Ready: v1.Ready_READY_TRUE, + }, + "third": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + }, + "MixedRegexUsageV2Namespaced": { + reason: "The function should delay the creation of second and fourth resources because the first and third are not ready", + args: args{ + req: &v1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + EnableDeletionSequencing: true, + ReplayDeletion: true, + Rules: []v1beta1.SequencingRule{ + { + Sequence: []resource.Name{ + "first", + "second-.*", + "third", + }, + }, + }, + UsageVersion: v1beta1.UsageV2, + }), + Observed: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(nxr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + "second-1": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + "third": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(nxr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + "second-1": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + "third": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + want: want{ + rsp: &v1.RunFunctionResponse{ + Meta: &v1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*v1.Result{}, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "first": { + Resource: resource.MustStructJSON(nxr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + "second-1": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + "second-0-first-usage": { + Resource: resource.MustStructJSON(nuv2), + Ready: v1.Ready_READY_TRUE, + }, + "second-1-first-usage": { + Resource: resource.MustStructJSON(nuv2), + Ready: v1.Ready_READY_TRUE, + }, + "third-second-0-usage": { + Resource: resource.MustStructJSON(nu2v2), + Ready: v1.Ready_READY_TRUE, + }, + "third-second-1-usage": { + Resource: resource.MustStructJSON(nu2v2), + Ready: v1.Ready_READY_TRUE, + }, + "third": { + Resource: resource.MustStructJSON(nmr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + }, "MarkCompositeNotReady": { reason: "Set the Composite ready flag to false", args: args{ diff --git a/go.mod b/go.mod index 1aca663..1a1b5bc 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.11 require ( github.com/alecthomas/kong v1.13.0 github.com/crossplane/crossplane-runtime/v2 v2.1.0 + github.com/crossplane/crossplane/v2 v2.1.3 github.com/crossplane/function-sdk-go v0.5.0 github.com/google/go-cmp v0.7.0 google.golang.org/protobuf v1.36.11 @@ -56,12 +57,12 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.46.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect @@ -69,15 +70,15 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.0 // indirect - k8s.io/apiextensions-apiserver v0.34.0 // indirect - k8s.io/client-go v0.34.0 // indirect - k8s.io/code-generator v0.34.0 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect + k8s.io/code-generator v0.34.1 // indirect k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect - sigs.k8s.io/controller-runtime v0.22.0 // indirect + sigs.k8s.io/controller-runtime v0.22.2 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect diff --git a/go.sum b/go.sum index 721cf19..8d296ca 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,7 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -21,6 +22,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crossplane/crossplane-runtime/v2 v2.1.0 h1:JBMhL9T+/PfyjLAQEdZWlKLvA3jJVtza8zLLwd9Gs4k= github.com/crossplane/crossplane-runtime/v2 v2.1.0/go.mod h1:j78pmk0qlI//Ur7zHhqTr8iePHFcwJKrZnzZB+Fg4t0= +github.com/crossplane/crossplane/v2 v2.1.3 h1:2fc5xHF/J0qrpe+WYKwIa4MEa9IjQh7R4I+jAIc50rs= +github.com/crossplane/crossplane/v2 v2.1.3/go.mod h1:9+J8aLPlE2ELeqpIXgkMJMuF9Oxj1czJAy4E9aBVsa0= github.com/crossplane/function-sdk-go v0.5.0 h1:wF+pOsR6jlIUHZjpSL6tbuSP0UB7s25+4AGkNytsHKk= github.com/crossplane/function-sdk-go v0.5.0/go.mod h1:bIvGe17dIdpZ/YULrg5xAP8MK+eS3ot5BAuQEntaeWc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -122,8 +125,9 @@ github.com/onsi/gomega v1.38.1 h1:FaLA8GlcpXDwsb7m0h2A9ew2aTk3vnZMlzFgg5tz/pk= github.com/onsi/gomega v1.38.1/go.mod h1:LfcV8wZLvwcYRwPiJysphKAEsmcFnLMK/9c+PjvlX8g= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= @@ -202,27 +206,27 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -266,20 +270,20 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= -k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= -k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= -k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= -k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= -k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= -k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= -k8s.io/code-generator v0.34.0 h1:Ze2i1QsvUprIlX3oHiGv09BFQRLCz+StA8qKwwFzees= -k8s.io/code-generator v0.34.0/go.mod h1:Py2+4w2HXItL8CGhks8uI/wS3Y93wPKO/9mBQUYNua0= -k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= -k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc= +k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg= +k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q= k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= @@ -290,8 +294,8 @@ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzk k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0= -sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4= +sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/controller-tools v0.19.0 h1:OU7jrPPiZusryu6YK0jYSjPqg8Vhf8cAzluP9XGI5uk= sigs.k8s.io/controller-tools v0.19.0/go.mod h1:y5HY/iNDFkmFla2CfQoVb2AQXMsBk4ad84iR1PLANB0= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= diff --git a/input/v1beta1/input.go b/input/v1beta1/input.go index 9283524..62482b9 100644 --- a/input/v1beta1/input.go +++ b/input/v1beta1/input.go @@ -22,6 +22,17 @@ type SequencingRule struct { Sequence []resource.Name `json:"sequence,omitempty"` } +// UsageVersion defines the version of the Usage resource. +type UsageVersion string + +const ( + // UsageV1 indicates that Crossplane v1 apiextensions Usage should be used. + UsageV1 UsageVersion = "v1" + + // UsageV2 indicates that Crossplane v2 protection Usage should be user. + UsageV2 UsageVersion = "v2" +) + // Input can be used to provide input to this Function. // +kubebuilder:object:root=true // +kubebuilder:storageversion @@ -30,6 +41,17 @@ type Input struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` + // EnableDeletionSequencing controls the automatic creation of Usage/ClusterUsage resources from the dependency tree + // defined by the rule sequences. + // +kubebuilder:object:default=false + EnableDeletionSequencing bool `json:"enableDeletionSequencing,omitempty"` + // ReplayDeletion sets the Usage/ClusterUsage replayDeletion attribute. + // +kubebuilder:object:default=true + ReplayDeletion bool `json:"replayDeletion,omitempty"` + // UsageVersion specifies the version of Usage/ClusterUsage resource to be created. + // +kubebuilder:object:default="v2" + UsageVersion UsageVersion `json:"usageVersion,omitempty"` + // ResetCompositeReadiness sets the composite ready state to false if desired resources are removed from the request. // +kubebuilder:object:default=false ResetCompositeReadiness bool `json:"resetCompositeReadiness,omitempty"` diff --git a/name.go b/name.go new file mode 100644 index 0000000..94fc143 --- /dev/null +++ b/name.go @@ -0,0 +1,36 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "strings" +) + +const ( + // maxKubernetesNameLength is the maximum length allowed for Kubernetes resource names. + maxKubernetesNameLength = 63 + // hashLength is the length of the hash to apply to names. + hashLength = 6 +) + +// GenerateName generates a valid Kubernetes name. +func GenerateName(name, suffix string) string { + h := sha256.Sum256([]byte(name)) + hEncoded := hex.EncodeToString(h[:])[:hashLength] + fullSuffix := hEncoded + "-" + suffix + fullName := name + "-" + fullSuffix + + if len(fullName) <= maxKubernetesNameLength { + return fullName + } + + maxNameLength := maxKubernetesNameLength - len(fullSuffix) - 1 // -1 for the hyphen separator + truncatedName := name[:maxNameLength] + + // Ensure the truncated name ends with a hyphen + if !strings.HasSuffix(truncatedName, "-") { + truncatedName += "-" + } + + return truncatedName + fullSuffix +} diff --git a/package/input/sequencer.fn.crossplane.io_inputs.yaml b/package/input/sequencer.fn.crossplane.io_inputs.yaml index 6c48d6e..5f0149c 100644 --- a/package/input/sequencer.fn.crossplane.io_inputs.yaml +++ b/package/input/sequencer.fn.crossplane.io_inputs.yaml @@ -28,6 +28,11 @@ spec: may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string + enableDeletionSequencing: + description: |- + EnableDeletionSequencing controls the automatic creation of Usage/ClusterUsage resources from the dependency tree + defined by the rule sequences. + type: boolean kind: description: |- Kind is a string value representing the REST resource this object represents. @@ -38,6 +43,10 @@ spec: type: string metadata: type: object + replayDeletion: + description: ReplayDeletion sets the Usage/ClusterUsage replayDeletion + attribute. + type: boolean resetCompositeReadiness: description: ResetCompositeReadiness sets the composite ready state to false if desired resources are removed from the request. @@ -57,6 +66,10 @@ spec: type: array type: object type: array + usageVersion: + description: UsageVersion specifies the version of Usage/ClusterUsage + resource to be created. + type: string required: - rules type: object From a788fd3a425061ffaed0f1dc5cabae3fc1ec2687 Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Fri, 16 Jan 2026 09:13:37 -0600 Subject: [PATCH 2/2] Update fn.go Incorporate review feedback Co-authored-by: Nic Cope --- fn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fn.go b/fn.go index be35ec8..03c7e4b 100644 --- a/fn.go +++ b/fn.go @@ -209,7 +209,7 @@ func getStrictRegex(pattern string) (*regexp.Regexp, error) { } // GenerateUsage determines whether to return a v1 or v2 Crossplane usage. -func GenerateUsage(of *unstructured.Unstructured, by *unstructured.Unstructured, rd bool, usageVersion v1beta1.UsageVersion) map[string]any { +func GenerateUsage(of, by *unstructured.Unstructured, rd bool, usageVersion v1beta1.UsageVersion) map[string]any { if usageVersion == v1beta1.UsageV1 { return GenerateV1Usage(of, by, rd) }