Skip to content

Commit 317a455

Browse files
committed
fix: freight soak time verification with unrelated and control-flow stages
Auto-promotion could either incorrectly pass or permanently stall the soak time gate (Stage.spec.requestedFreight[].sources.requiredSoakTime), depending on the upstream Stage topology. Two distinct problems are addressed: 1. Soak accrued in unrelated Stages satisfied the gate. ListFreightFromWarehouse iterated over every Stage in the Freight's status.verifiedIn map when checking whether the required soak time had elapsed. Soak time accumulated in Stages that were NOT named as upstream sources could therefore satisfy the requirement, letting Freight promote before it had actually soaked in the configured upstream Stage(s). Restrict the check to the Stages named in the source's `stages` list (opts.VerifiedIn). 2. A control-flow upstream Stage could never satisfy the gate. Control-flow Stages verify Freight without ever holding it: the Freight is never added to status.currentlyIn and no completed soak is ever recorded. GetLongestSoak only consulted those two fields, so it returned 0 for a control-flow Stage no matter how long ago the Freight was verified there. With problem 1 fixed, a downstream Stage whose upstream source is a control-flow Stage could then never pass its requiredSoakTime, and auto-promotion stalled indefinitely. When neither a current residency nor a completed soak is recorded for a Stage in which the Freight is verified -- which uniquely identifies a control-flow Stage -- measure the soak from the Freight's verifiedAt timestamp instead. Regular Stages always have one of the two records, so their behavior is unchanged. Refs: #4586 Signed-off-by: Justin Chen <mail@justin0u0.com>
1 parent f791a1f commit 317a455

4 files changed

Lines changed: 93 additions & 7 deletions

File tree

api/v1alpha1/freight_types.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,16 +153,26 @@ func (f *Freight) IsApprovedFor(stage string) bool {
153153
// the current soak time is calculated and compared to the longest completed
154154
// soak time on record.
155155
func (f *Freight) GetLongestSoak(stage string) time.Duration {
156-
if _, verified := f.Status.VerifiedIn[stage]; !verified {
156+
record, verified := f.Status.VerifiedIn[stage]
157+
if !verified {
157158
return 0
158159
}
159160
var longestCompleted time.Duration
160-
if record, isVerified := f.Status.VerifiedIn[stage]; isVerified && record.LongestCompletedSoak != nil {
161+
if record.LongestCompletedSoak != nil {
161162
longestCompleted = record.LongestCompletedSoak.Duration
162163
}
163164
var current time.Duration
164-
if record, isCurrent := f.Status.CurrentlyIn[stage]; isCurrent {
165-
current = time.Since(record.Since.Time)
165+
if c, isCurrent := f.Status.CurrentlyIn[stage]; isCurrent && c.Since != nil {
166+
current = time.Since(c.Since.Time)
167+
}
168+
// Control-flow Stages verify Freight without ever holding it: the Freight is
169+
// never added to CurrentlyIn and no soak is ever completed. For such Stages,
170+
// neither a current nor a completed soak is recorded, so measure the soak
171+
// from the time the Freight was verified. A regular Stage always has either
172+
// a current residency or a completed soak, so this branch does not affect
173+
// it.
174+
if longestCompleted == 0 && current == 0 && record.VerifiedAt != nil {
175+
return time.Since(record.VerifiedAt.Time)
166176
}
167177
return time.Duration(max(longestCompleted.Nanoseconds(), current.Nanoseconds()))
168178
}

api/v1alpha1/freight_types_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,21 @@ func TestFreight_GetLongestSoak(t *testing.T) {
316316
require.LessOrEqual(t, longestSoak, 2*time.Hour+time.Second)
317317
},
318318
},
319+
{
320+
// A control-flow Stage verifies Freight without ever holding it, so
321+
// neither a current residency nor a completed soak is recorded. Soak
322+
// must be measured from the time the Freight was verified.
323+
name: "Freight verified in a control-flow Stage; soak measured from verification",
324+
status: FreightStatus{
325+
VerifiedIn: map[string]VerifiedStage{
326+
testStage: {VerifiedAt: &metav1.Time{Time: time.Now().Add(-2 * time.Hour)}},
327+
},
328+
},
329+
assertions: func(t *testing.T, _ FreightStatus, longestSoak time.Duration) {
330+
require.GreaterOrEqual(t, longestSoak, 2*time.Hour)
331+
require.LessOrEqual(t, longestSoak, 2*time.Hour+time.Second)
332+
},
333+
},
319334
}
320335
for _, testCase := range testCases {
321336
t.Run(testCase.name, func(t *testing.T) {

pkg/api/warehouse.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,15 @@ func ListFreightFromWarehouse(
225225
}
226226
}
227227

228-
// Track set of Stages that have passed the verification soak time
229-
// for the Freight.
228+
// Track the set of configured upstream Stages in which the Freight
229+
// has been verified AND has passed the required soak time. Only
230+
// upstream Stages named in opts.VerifiedIn are considered -- soak
231+
// time accrued in unrelated Stages must not satisfy this gate.
230232
verifiedStages := sets.New[string]()
231-
for stage := range f.Status.VerifiedIn {
233+
for _, stage := range opts.VerifiedIn {
234+
if _, verified := f.Status.VerifiedIn[stage]; !verified {
235+
continue
236+
}
232237
if f.HasSoakedIn(stage, opts.RequiredSoakTime) {
233238
verifiedStages.Insert(stage)
234239
}

pkg/api/warehouse_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,62 @@ func TestListFreightFromWarehouse(t *testing.T) {
452452
require.Equal(t, "fake-freight-2", freight[0].Name)
453453
},
454454
},
455+
{
456+
name: "soak in unrelated Stage does not satisfy soak gate",
457+
opts: &ListWarehouseFreightOptions{
458+
VerifiedIn: []string{testUpstreamStage},
459+
RequiredSoakTime: &metav1.Duration{Duration: time.Hour},
460+
},
461+
objects: []client.Object{
462+
&kargoapi.Freight{
463+
// Verified in the configured upstream Stage but has not
464+
// soaked there long enough. It HAS soaked > 1h in an
465+
// unrelated Stage, which must not count.
466+
ObjectMeta: metav1.ObjectMeta{
467+
Namespace: testProject,
468+
Name: "fake-freight-1",
469+
},
470+
Origin: kargoapi.FreightOrigin{
471+
Kind: kargoapi.FreightOriginKindWarehouse,
472+
Name: testWarehouse,
473+
},
474+
Status: kargoapi.FreightStatus{
475+
VerifiedIn: map[string]kargoapi.VerifiedStage{
476+
testUpstreamStage: {
477+
LongestCompletedSoak: &metav1.Duration{Duration: 30 * time.Minute},
478+
},
479+
"unrelated-stage": {
480+
LongestCompletedSoak: &metav1.Duration{Duration: 2 * time.Hour},
481+
},
482+
},
483+
},
484+
},
485+
&kargoapi.Freight{
486+
// Verified in the configured upstream Stage with sufficient
487+
// soak time -- should be returned.
488+
ObjectMeta: metav1.ObjectMeta{
489+
Namespace: testProject,
490+
Name: "fake-freight-2",
491+
},
492+
Origin: kargoapi.FreightOrigin{
493+
Kind: kargoapi.FreightOriginKindWarehouse,
494+
Name: testWarehouse,
495+
},
496+
Status: kargoapi.FreightStatus{
497+
VerifiedIn: map[string]kargoapi.VerifiedStage{
498+
testUpstreamStage: {
499+
LongestCompletedSoak: &metav1.Duration{Duration: 2 * time.Hour},
500+
},
501+
},
502+
},
503+
},
504+
},
505+
assertions: func(t *testing.T, freight []kargoapi.Freight, err error) {
506+
require.NoError(t, err)
507+
require.Len(t, freight, 1)
508+
require.Equal(t, "fake-freight-2", freight[0].Name)
509+
},
510+
},
455511
{
456512
name: "failure with invalid AvailabilityStrategy",
457513
opts: &ListWarehouseFreightOptions{

0 commit comments

Comments
 (0)