From c9d85264e6d5b23c448322f3310f93a3cc1d1f8d Mon Sep 17 00:00:00 2001 From: Gabriel Gaudreau Date: Wed, 6 May 2026 08:59:49 -0400 Subject: [PATCH] fix: nil pointer dereference in deletion sequencing when a before-resource is not yet observed when enableDeletionSequencing=true, the function panics when a before-resource exists in desired but not yet in observed Signed-off-by: Gabriel Gaudreau --- fn.go | 7 +++++- fn_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/fn.go b/fn.go index f08b563..4a6aa98 100644 --- a/fn.go +++ b/fn.go @@ -187,8 +187,13 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* for c, o := range observedComposed { if currentRegex.MatchString(string(c)) && !isUsage(o, in.UsageVersion) { for _, k := range keys { + obs, ok := observedComposed[k] + if !ok { + f.log.Debug("Skipping usage; before-resource not yet observed", "k:", k, "by c:", c) + continue + } f.log.Debug("Generate Usage of ", "k:", k, "by c:", c) - usage := GenerateUsage(&observedComposed[k].Resource.Unstructured, &o.Resource.Unstructured, in.ReplayDeletion, in.UsageVersion) + usage := GenerateUsage(&obs.Resource.Unstructured, &o.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)) diff --git a/fn_test.go b/fn_test.go index a21c9bd..10df34a 100644 --- a/fn_test.go +++ b/fn_test.go @@ -1679,6 +1679,72 @@ func TestRunFunction(t *testing.T) { }, }, }, + "DeletionSequencingBeforeResourceNotObserved": { + reason: "The function should not panic when a before-resource is in desiredComposed but not in observedComposed with deletion sequencing enabled", + args: args{ + req: &v1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + EnableDeletionSequencing: 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{ + "second-foo": { + 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-foo": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second-foo": { + 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-foo": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "second-foo": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + }, "MarkCompositeNotReady": { reason: "Set the Composite ready flag to false", args: args{