diff --git a/fn.go b/fn.go index 27ccfad..7db3132 100644 --- a/fn.go +++ b/fn.go @@ -86,27 +86,10 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* 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)) + if err := generateUsagesFromObserved(usages, observedComposed, sequence[i-1], r, in); err != nil { + response.Fatal(rsp, err) 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. @@ -142,6 +125,15 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* return rsp, nil } + if in.EnableDeletionSequencing { + // Generate deletion usages from observed resources before any + // creation short-circuiting so dependencies remain stable during teardown. + if err := generateUsagesFromObserved(usages, observedComposed, before, r, in); err != nil { + response.Fatal(rsp, err) + 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) @@ -173,23 +165,6 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* } break } - if in.EnableDeletionSequencing { - for c, o := range observedComposed { - if currentRegex.MatchString(string(c)) && !isUsage(o, in.UsageVersion) { - for _, k := range keys { - f.log.Debug("Generate Usage of ", "k:", k, "by c:", c) - usage := GenerateUsage(&observedComposed[k].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)) - 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} - } - } - } - } } } } @@ -198,6 +173,48 @@ func (f *Function) RunFunction(_ context.Context, req *v1.RunFunctionRequest) (* return rsp, response.SetDesiredComposedResources(rsp, desiredComposed) } +func generateUsagesFromObserved( + usages map[resource.Name]*resource.DesiredComposed, + observedComposed map[resource.Name]resource.ObservedComposed, + ofPattern, byPattern resource.Name, + in *v1beta1.Input, +) error { + ofRegex, err := getStrictRegex(string(ofPattern)) + if err != nil { + return errors.Wrapf(err, "cannot compile regex %s", ofPattern) + } + byRegex, err := getStrictRegex(string(byPattern)) + if err != nil { + return errors.Wrapf(err, "cannot compile regex %s", byPattern) + } + + for byName, byObserved := range observedComposed { + if !byRegex.MatchString(string(byName)) || isUsage(byObserved, in.UsageVersion) { + continue + } + for ofName, ofObserved := range observedComposed { + if !ofRegex.MatchString(string(ofName)) || isUsage(ofObserved, in.UsageVersion) { + continue + } + usage := GenerateUsage( + &ofObserved.Resource.Unstructured, + &byObserved.Resource.Unstructured, + in.ReplayDeletion, + in.UsageVersion, + ) + usageComposed := composed.New() + if err := convertViaJSON(usageComposed, usage); err != nil { + return errors.Wrapf(err, "cannot convert to JSON %s", usage) + } + usages[byName+"-"+ofName+"-usage"] = &resource.DesiredComposed{ + Resource: usageComposed, + Ready: resource.ReadyTrue, + } + } + } + return nil +} + func getStrictRegex(pattern string) (*regexp.Regexp, error) { if !strings.HasPrefix(pattern, START) && !strings.HasSuffix(pattern, END) { // if the user provides a delimited regex, we'll use it as is diff --git a/fn_test.go b/fn_test.go index bd35bb3..12c1bd2 100644 --- a/fn_test.go +++ b/fn_test.go @@ -1678,6 +1678,79 @@ func TestRunFunction(t *testing.T) { }, }, }, + "DeletionUsageGeneratedWhenReadinessGateShortCircuitsForRegexCurrent": { + reason: "The function should still emit deletion usage edges from observed resources even when creation gating short-circuits for regex current resources", + args: args{ + req: &v1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + EnableDeletionSequencing: true, + ReplayDeletion: true, + Rules: []v1beta1.SequencingRule{ + { + Sequence: []resource.Name{ + "provider", + "^consumer-.*$", + }, + }, + }, + UsageVersion: v1beta1.UsageV2, + }), + Observed: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "provider": { + Resource: resource.MustStructJSON(xr), + Ready: v1.Ready_READY_TRUE, + }, + "consumer-a": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "consumer-a": { + 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{ + { + Severity: v1.Severity_SEVERITY_NORMAL, + Message: "Delaying creation of resource(s) matching \"^consumer-.*$\" because \"provider\" does not exist yet", + Target: &target, + }, + }, + Desired: &v1.State{ + Composite: &v1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*v1.Resource{ + "consumer-a": { + Resource: resource.MustStructJSON(mr), + Ready: v1.Ready_READY_TRUE, + }, + "consumer-a-provider-usage": { + Resource: resource.MustStructJSON(uv2), + Ready: v1.Ready_READY_TRUE, + }, + }, + }, + }, + }, + }, "MarkCompositeNotReady": { reason: "Set the Composite ready flag to false", args: args{