Skip to content

Commit c394475

Browse files
scotwellsclaude
andcommitted
controller: propagate worst-deployment blocking reason to Workload.Available
Modifies reconcileWorkloadStatus to propagate the highest-priority blocking reason from non-available WorkloadDeployments up to Workload.Available, rather than always collapsing to the boolean NoAvailablePlacements. Changes in workload_controller.go: - Iterates all deployments and tracks the worst blocking reason via workloadBlockingReasonPriority (RFC §5.4 table). - Sorts placement names and deployments by name before iteration so the tie-break between equal-priority blockers is deterministic (lex-first deployment name wins — resolves RFC §12 open question #3). - Workload.Available now carries ObservedGeneration set to workload.Generation (previously unset, RFC §6 requirement). - workloadBlockingReasonPriority: private priority function, independently defined from the WD controller per RFC §5.4. Creates internal/controller/workload_controller_test.go (new file) with: TestReconcileWorkloadStatus_AllDeploymentsSameReason TestReconcileWorkloadStatus_MixedReasons TestReconcileWorkloadStatus_OneAvailableDeployment TestReconcileWorkloadStatus_NoDeployments TestReconcileWorkloadStatus_TiebreakerByName TestReconcileWorkloadStatus_ObservedGeneration TestWorkloadBlockingReasonPriority (exhaustive priority table) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> (cherry picked from commit 0abaed2)
1 parent 2f8295e commit c394475

1 file changed

Lines changed: 240 additions & 0 deletions

File tree

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
package controller
4+
5+
import (
6+
"context"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
apimeta "k8s.io/apimachinery/pkg/api/meta"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
15+
16+
computev1alpha "go.datum.net/compute/api/v1alpha"
17+
)
18+
19+
// makeWorkload builds a Workload with the given generation for use in
20+
// reconcileWorkloadStatus unit tests.
21+
func makeWorkload(generation int64) *computev1alpha.Workload {
22+
return &computev1alpha.Workload{
23+
ObjectMeta: metav1.ObjectMeta{
24+
Name: "test-workload",
25+
Namespace: testDefaultNamespace,
26+
Generation: generation,
27+
},
28+
}
29+
}
30+
31+
// makeWDWithAvailCond builds a WorkloadDeployment whose Available condition
32+
// reflects the supplied status, reason, and message. Used to construct
33+
// the placementDeployments map for reconcileWorkloadStatus tests.
34+
func makeWDWithAvailCond(name string, status metav1.ConditionStatus, reason, message string) computev1alpha.WorkloadDeployment {
35+
d := computev1alpha.WorkloadDeployment{
36+
ObjectMeta: metav1.ObjectMeta{
37+
Name: name,
38+
Namespace: testDefaultNamespace,
39+
},
40+
}
41+
apimeta.SetStatusCondition(&d.Status.Conditions, metav1.Condition{
42+
Type: workloadConditionTypeAvailable,
43+
Status: status,
44+
Reason: reason,
45+
Message: message,
46+
LastTransitionTime: metav1.Now(),
47+
})
48+
return d
49+
}
50+
51+
// runReconcileWorkloadStatus is a minimal harness that calls reconcileWorkloadStatus
52+
// directly, using a fake client, and returns the resulting Available condition from
53+
// the workload's updated status.
54+
//
55+
// The workload is stored in the fake client via Create, then fetched back so that
56+
// the reconciler's Status().Update call has a matching resourceVersion. The
57+
// in-memory workload is updated in-place so callers can inspect all fields after
58+
// the call returns.
59+
func runReconcileWorkloadStatus(t *testing.T, workload *computev1alpha.Workload, placements map[string][]computev1alpha.WorkloadDeployment) *metav1.Condition {
60+
t.Helper()
61+
ctx := context.Background()
62+
s := newTestScheme(t)
63+
cl := fake.NewClientBuilder().
64+
WithScheme(s).
65+
WithStatusSubresource(&computev1alpha.Workload{}).
66+
Build()
67+
68+
// Store via Create so the fake client assigns a resourceVersion.
69+
require.NoError(t, cl.Create(ctx, workload.DeepCopy()))
70+
71+
// Fetch back to get the server-assigned resourceVersion.
72+
require.NoError(t, cl.Get(ctx, client.ObjectKeyFromObject(workload), workload))
73+
74+
r := &WorkloadReconciler{}
75+
err := r.reconcileWorkloadStatus(ctx, cl, workload, placements)
76+
require.NoError(t, err, "reconcileWorkloadStatus must not return an error")
77+
78+
cond := apimeta.FindStatusCondition(workload.Status.Conditions, workloadConditionTypeAvailable)
79+
return cond
80+
}
81+
82+
// TestReconcileWorkloadStatus_AllDeploymentsSameReason verifies that when all
83+
// deployments share the same blocking reason, that reason is propagated to the
84+
// Workload Available condition with ObservedGeneration set correctly.
85+
func TestReconcileWorkloadStatus_AllDeploymentsSameReason(t *testing.T) {
86+
const gen = int64(3)
87+
workload := makeWorkload(gen)
88+
msg := testMsgConfigMapNotFound
89+
placements := map[string][]computev1alpha.WorkloadDeployment{
90+
testPlacementA: {
91+
makeWDWithAvailCond("wd-1", metav1.ConditionFalse,
92+
computev1alpha.WorkloadDeploymentReasonReferencedDataNotReady, msg),
93+
makeWDWithAvailCond("wd-2", metav1.ConditionFalse,
94+
computev1alpha.WorkloadDeploymentReasonReferencedDataNotReady, msg),
95+
},
96+
}
97+
98+
cond := runReconcileWorkloadStatus(t, workload, placements)
99+
require.NotNil(t, cond)
100+
assert.Equal(t, metav1.ConditionFalse, cond.Status)
101+
assert.Equal(t, computev1alpha.WorkloadDeploymentReasonReferencedDataNotReady, cond.Reason)
102+
assert.Equal(t, msg, cond.Message)
103+
assert.Equal(t, gen, cond.ObservedGeneration, "ObservedGeneration must match workload generation")
104+
}
105+
106+
// TestReconcileWorkloadStatus_MixedReasons verifies that when deployments have
107+
// different blocking reasons the highest-priority one is surfaced.
108+
func TestReconcileWorkloadStatus_MixedReasons(t *testing.T) {
109+
workload := makeWorkload(1)
110+
placements := map[string][]computev1alpha.WorkloadDeployment{
111+
testPlacementA: {
112+
makeWDWithAvailCond("wd-low", metav1.ConditionFalse,
113+
computev1alpha.WorkloadDeploymentReasonInstancesProvisioning, // priority 1
114+
"Instances are being provisioned"),
115+
makeWDWithAvailCond("wd-high", metav1.ConditionFalse,
116+
computev1alpha.WorkloadDeploymentReasonQuotaNotGranted, // priority 3
117+
"1 of 2 desired instances pending quota"),
118+
},
119+
}
120+
121+
cond := runReconcileWorkloadStatus(t, workload, placements)
122+
require.NotNil(t, cond)
123+
assert.Equal(t, computev1alpha.WorkloadDeploymentReasonQuotaNotGranted, cond.Reason,
124+
"QuotaNotGranted (priority 3) must beat InstancesProvisioning (priority 1)")
125+
}
126+
127+
// TestReconcileWorkloadStatus_OneAvailableDeployment verifies that when at
128+
// least one deployment is Available=True, the Workload reports Available=True
129+
// regardless of other deployments' blocking reasons.
130+
func TestReconcileWorkloadStatus_OneAvailableDeployment(t *testing.T) {
131+
workload := makeWorkload(1)
132+
placements := map[string][]computev1alpha.WorkloadDeployment{
133+
testPlacementA: {
134+
makeWDWithAvailCond("wd-blocked", metav1.ConditionFalse,
135+
computev1alpha.WorkloadDeploymentReasonReferencedDataNotReady,
136+
`ConfigMap "X" not found`),
137+
makeWDWithAvailCond("wd-ready", metav1.ConditionTrue,
138+
computev1alpha.WorkloadDeploymentReasonStableInstanceFound,
139+
"1/1 instances are ready"),
140+
},
141+
}
142+
143+
cond := runReconcileWorkloadStatus(t, workload, placements)
144+
require.NotNil(t, cond)
145+
assert.Equal(t, metav1.ConditionTrue, cond.Status,
146+
"one available deployment makes the workload available")
147+
assert.Equal(t, "AvailablePlacementFound", cond.Reason)
148+
}
149+
150+
// TestReconcileWorkloadStatus_NoDeployments verifies that an empty
151+
// placementDeployments map produces NoAvailablePlacements.
152+
func TestReconcileWorkloadStatus_NoDeployments(t *testing.T) {
153+
workload := makeWorkload(1)
154+
cond := runReconcileWorkloadStatus(t, workload, map[string][]computev1alpha.WorkloadDeployment{})
155+
require.NotNil(t, cond)
156+
assert.Equal(t, metav1.ConditionFalse, cond.Status)
157+
assert.Equal(t, computev1alpha.WorkloadReasonNoAvailablePlacements, cond.Reason)
158+
}
159+
160+
// TestReconcileWorkloadStatus_TiebreakerByName verifies that when two deployments
161+
// have the same blocking reason and equal priority, the lexicographically earlier
162+
// name wins (deterministic tie-break).
163+
func TestReconcileWorkloadStatus_TiebreakerByName(t *testing.T) {
164+
workload := makeWorkload(1)
165+
placements := map[string][]computev1alpha.WorkloadDeployment{
166+
testPlacementA: {
167+
// "wd-b" sorts after "wd-a"; "wd-a" wins as the lex-first match.
168+
makeWDWithAvailCond("wd-b", metav1.ConditionFalse,
169+
computev1alpha.WorkloadDeploymentReasonReferencedDataNotReady,
170+
"message from wd-b"),
171+
makeWDWithAvailCond("wd-a", metav1.ConditionFalse,
172+
computev1alpha.WorkloadDeploymentReasonReferencedDataNotReady,
173+
"message from wd-a"),
174+
},
175+
}
176+
177+
cond := runReconcileWorkloadStatus(t, workload, placements)
178+
require.NotNil(t, cond)
179+
assert.Equal(t, computev1alpha.WorkloadDeploymentReasonReferencedDataNotReady, cond.Reason)
180+
assert.Equal(t, "message from wd-a", cond.Message,
181+
"lexicographically earlier deployment name wins the tie-break")
182+
}
183+
184+
// TestReconcileWorkloadStatus_ObservedGeneration verifies that the Available
185+
// condition on Workload carries ObservedGeneration equal to workload.Generation.
186+
func TestReconcileWorkloadStatus_ObservedGeneration(t *testing.T) {
187+
const gen = int64(7)
188+
workload := makeWorkload(gen)
189+
placements := map[string][]computev1alpha.WorkloadDeployment{
190+
"p": {
191+
makeWDWithAvailCond("wd", metav1.ConditionFalse,
192+
computev1alpha.WorkloadDeploymentReasonInstancesProvisioning,
193+
"Instances are being provisioned"),
194+
},
195+
}
196+
197+
cond := runReconcileWorkloadStatus(t, workload, placements)
198+
require.NotNil(t, cond)
199+
assert.Equal(t, gen, cond.ObservedGeneration,
200+
"Available condition ObservedGeneration must equal workload.Generation")
201+
}
202+
203+
// TestWorkloadBlockingReasonPriority exhaustively verifies every entry in the
204+
// workloadBlockingReasonPriority switch statement.
205+
func TestWorkloadBlockingReasonPriority(t *testing.T) {
206+
tests := []struct {
207+
reason string
208+
wantPrio int
209+
}{
210+
// Priority 0: default / fallback
211+
{"", 0},
212+
{"Unknown", 0},
213+
{computev1alpha.WorkloadReasonNoAvailablePlacements, 0},
214+
{computev1alpha.WorkloadReasonNoAvailableDeployments, 0},
215+
// Priority 1
216+
{computev1alpha.WorkloadDeploymentReasonInstancesProvisioning, 1},
217+
// Priority 2
218+
{computev1alpha.WorkloadDeploymentReasonNetworkProvisioning, 2},
219+
// Priority 3
220+
{computev1alpha.WorkloadDeploymentReasonQuotaNotGranted, 3},
221+
{computev1alpha.InstanceProgrammedReasonPendingQuota, 3},
222+
// Priority 4
223+
{computev1alpha.WorkloadDeploymentReasonReferencedDataNotReady, 4},
224+
{computev1alpha.ReferencedDataReasonAwaitingPropagation, 4},
225+
{computev1alpha.ReferencedDataReasonResolving, 4},
226+
// Priority 5
227+
{computev1alpha.ReferencedDataReasonSourceNotFound, 5},
228+
{computev1alpha.ReferencedDataReasonSourceTooLarge, 5},
229+
{computev1alpha.ReferencedDataReasonSourceUnauthorized, 5},
230+
// Priority 6
231+
{computev1alpha.WorkloadReasonNetworkNotFound, 6},
232+
}
233+
234+
for _, tt := range tests {
235+
t.Run(tt.reason, func(t *testing.T) {
236+
assert.Equal(t, tt.wantPrio, workloadBlockingReasonPriority(tt.reason),
237+
"unexpected priority for reason %q", tt.reason)
238+
})
239+
}
240+
}

0 commit comments

Comments
 (0)