Skip to content

Commit 52889eb

Browse files
author
Mark Altmann
committed
feat: implement transitive skipping and deferred dependency gating for conditional resources
1 parent b0f5255 commit 52889eb

15 files changed

Lines changed: 1098 additions & 65 deletions

builtins/builtins.go

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -485,18 +485,21 @@ func readyToProto(r resource.Ready) fnv1.Ready {
485485
}
486486

487487
const (
488-
CompositeReadyConditionType = "ComposedResourcesReady"
489-
compositeReadyReasonPending = "PendingConditionalResources"
488+
CompositeReadyConditionType = "ComposedResourcesReady"
489+
compositeReadyReasonPending = "PendingConditionalResources"
490+
compositeReadyReasonWaitingDep = "WaitingForDependencies"
491+
compositeReadyReasonComposite = "CompositeNotReady"
490492
)
491493

492494
// ApplyCompositeReady sets rsp.Desired.Composite.Ready. An explicit
493-
// set_composite_ready() call wins; otherwise any non-optional Resource(when=False)
494-
// skip flips Ready to False and emits a ComposedResourcesReady=False condition.
495-
// With neither, Ready is left UNSPECIFIED.
495+
// set_composite_ready() call wins. Otherwise any non-optional
496+
// Resource(when=False) skip OR sequencer-deferred resource flips Ready to
497+
// False and emits a ComposedResourcesReady=False condition. With none of
498+
// these, Ready is left UNSPECIFIED.
496499
func ApplyCompositeReady(rsp *fnv1.RunFunctionResponse, collector *Collector, cc *ConditionCollector) {
497-
override, skips := collector.compositeReadyState()
500+
override, skips, defers := collector.compositeReadyState()
498501

499-
if !override.Set && len(skips) == 0 {
502+
if !override.Set && len(skips) == 0 && len(defers) == 0 {
500503
return
501504
}
502505

@@ -531,6 +534,37 @@ func ApplyCompositeReady(rsp *fnv1.RunFunctionResponse, collector *Collector, cc
531534

532535
rsp.Desired.Composite.Ready = fnv1.Ready_READY_FALSE
533536

537+
reason, message := buildGatingConditionFields(skips, defers)
538+
cc.AddCondition(CollectedCondition{
539+
Type: CompositeReadyConditionType,
540+
Status: "False",
541+
Reason: reason,
542+
Message: message,
543+
Target: "CompositeAndClaim",
544+
})
545+
}
546+
547+
// buildGatingConditionFields composes the reason and message for the
548+
// auto-emitted ComposedResourcesReady=False condition based on which gating
549+
// categories are populated.
550+
func buildGatingConditionFields(skips []GatingSkip, defers []GatingDefer) (string, string) {
551+
skipPart := formatGatingSkips(skips)
552+
deferPart := formatGatingDefers(defers)
553+
554+
switch {
555+
case len(skips) > 0 && len(defers) > 0:
556+
return compositeReadyReasonComposite, skipPart + "; " + deferPart
557+
case len(skips) > 0:
558+
return compositeReadyReasonPending, skipPart
559+
default:
560+
return compositeReadyReasonWaitingDep, deferPart
561+
}
562+
}
563+
564+
func formatGatingSkips(skips []GatingSkip) string {
565+
if len(skips) == 0 {
566+
return ""
567+
}
534568
parts := make([]string, 0, len(skips))
535569
for _, s := range skips {
536570
if s.Reason != "" {
@@ -539,11 +573,20 @@ func ApplyCompositeReady(rsp *fnv1.RunFunctionResponse, collector *Collector, cc
539573
parts = append(parts, fmt.Sprintf("%q", s.Name))
540574
}
541575
}
542-
cc.AddCondition(CollectedCondition{
543-
Type: CompositeReadyConditionType,
544-
Status: "False",
545-
Reason: compositeReadyReasonPending,
546-
Message: fmt.Sprintf("Pending resources: %s", strings.Join(parts, "; ")),
547-
Target: "CompositeAndClaim",
548-
})
576+
return "Pending resources: " + strings.Join(parts, "; ")
577+
}
578+
579+
func formatGatingDefers(defers []GatingDefer) string {
580+
if len(defers) == 0 {
581+
return ""
582+
}
583+
parts := make([]string, 0, len(defers))
584+
for _, d := range defers {
585+
if d.Reason != "" {
586+
parts = append(parts, fmt.Sprintf("%q (%s)", d.Name, d.Reason))
587+
} else {
588+
parts = append(parts, fmt.Sprintf("%q", d.Name))
589+
}
590+
}
591+
return "Waiting on dependencies: " + strings.Join(parts, "; ")
549592
}

builtins/builtins_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2029,3 +2029,88 @@ func TestApplyCompositeReady_PreservesExistingComposite(t *testing.T) {
20292029
t.Error("Composite.Resource was clobbered by ApplyCompositeReady")
20302030
}
20312031
}
2032+
2033+
func TestApplyCompositeReady_DefersOnly(t *testing.T) {
2034+
cc := NewConditionCollector()
2035+
c := NewCollector(cc, "test.star", nil, nil)
2036+
2037+
c.AddGatingDefers([]GatingDefer{
2038+
{Name: "app", Reason: `waiting for "db" to have Ready=True`},
2039+
})
2040+
2041+
rsp := &fnv1.RunFunctionResponse{}
2042+
ApplyCompositeReady(rsp, c, cc)
2043+
2044+
if got := rsp.Desired.Composite.Ready; got != fnv1.Ready_READY_FALSE {
2045+
t.Errorf("Composite.Ready = %v, want READY_FALSE", got)
2046+
}
2047+
conds := cc.Conditions()
2048+
if len(conds) != 1 {
2049+
t.Fatalf("expected 1 condition, got %d", len(conds))
2050+
}
2051+
if conds[0].Type != CompositeReadyConditionType ||
2052+
conds[0].Status != "False" ||
2053+
conds[0].Reason != "WaitingForDependencies" {
2054+
t.Errorf("condition = %+v, want WaitingForDependencies/False", conds[0])
2055+
}
2056+
if !strings.Contains(conds[0].Message, "app") ||
2057+
!strings.Contains(conds[0].Message, "Waiting on dependencies") {
2058+
t.Errorf("condition message = %q, want it to mention 'app' under 'Waiting on dependencies'", conds[0].Message)
2059+
}
2060+
}
2061+
2062+
func TestApplyCompositeReady_SkipsAndDefers(t *testing.T) {
2063+
cc := NewConditionCollector()
2064+
c := NewCollector(cc, "test.star", nil, nil)
2065+
2066+
c.recordSkip("backup", "feature pending", true)
2067+
c.AddGatingDefers([]GatingDefer{
2068+
{Name: "app", Reason: `waiting for "db" to have Ready=True`},
2069+
})
2070+
2071+
rsp := &fnv1.RunFunctionResponse{}
2072+
ApplyCompositeReady(rsp, c, cc)
2073+
2074+
if got := rsp.Desired.Composite.Ready; got != fnv1.Ready_READY_FALSE {
2075+
t.Errorf("Composite.Ready = %v, want READY_FALSE", got)
2076+
}
2077+
conds := cc.Conditions()
2078+
if len(conds) != 1 {
2079+
t.Fatalf("expected 1 aggregated condition, got %d", len(conds))
2080+
}
2081+
if conds[0].Reason != "CompositeNotReady" {
2082+
t.Errorf("Reason = %q, want CompositeNotReady when both skips and defers are present", conds[0].Reason)
2083+
}
2084+
msg := conds[0].Message
2085+
if !strings.Contains(msg, "Pending resources") || !strings.Contains(msg, "backup") {
2086+
t.Errorf("message = %q, want to include 'Pending resources' and 'backup'", msg)
2087+
}
2088+
if !strings.Contains(msg, "Waiting on dependencies") || !strings.Contains(msg, "app") {
2089+
t.Errorf("message = %q, want to include 'Waiting on dependencies' and 'app'", msg)
2090+
}
2091+
}
2092+
2093+
func TestApplyCompositeReady_OverrideStillWinsOverDefers(t *testing.T) {
2094+
cc := NewConditionCollector()
2095+
c := NewCollector(cc, "test.star", nil, nil)
2096+
2097+
c.AddGatingDefers([]GatingDefer{{Name: "app", Reason: "waiting"}})
2098+
2099+
thread := new(starlark.Thread)
2100+
_, err := starlark.Call(thread, c.SetCompositeReadyBuiltin(), starlark.Tuple{
2101+
starlark.True,
2102+
}, nil)
2103+
if err != nil {
2104+
t.Fatalf("set_composite_ready() error: %v", err)
2105+
}
2106+
2107+
rsp := &fnv1.RunFunctionResponse{}
2108+
ApplyCompositeReady(rsp, c, cc)
2109+
2110+
if got := rsp.Desired.Composite.Ready; got != fnv1.Ready_READY_TRUE {
2111+
t.Errorf("Composite.Ready = %v, want READY_TRUE (override wins over defers)", got)
2112+
}
2113+
if got := cc.Conditions(); len(got) != 0 {
2114+
t.Errorf("expected no conditions (override True with no reason), got %+v", got)
2115+
}
2116+
}

0 commit comments

Comments
 (0)