Skip to content

Commit 5d11801

Browse files
scotwellsclaude
andcommitted
feat(controller): surface rollout progress via UpdatedReplicas + ObservedGeneration
A restart/rolling update was invisible from the project plane: there was no status field representing how many instances are on the new template revision. Add UpdatedReplicas (instances whose observed template hash matches the desired template, regardless of readiness) and ObservedGeneration to both WorkloadDeployment and Workload (plus placement) status. UpdatedReplicas is computed on the cell WD reconcile alongside CurrentReplicas (which is now its Programmed subset), aggregated up into the Workload, and rides the existing status sync to the project plane. Repoint the "Up-to-date" printcolumn to .status.updatedReplicas to match `kubectl get deployment` semantics, so a roll is visible as the count dips below Replicas and recovers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 015c017 commit 5d11801

2 files changed

Lines changed: 29 additions & 8 deletions

File tree

internal/controller/workload_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ func (r *WorkloadReconciler) reconcileWorkloadStatus(
220220
newWorkloadStatus := workload.Status.DeepCopy()
221221
totalReplicas := int32(0)
222222
totalCurrentReplicas := int32(0)
223+
totalUpdatedReplicas := int32(0)
223224
totalDesiredReplicas := int32(0)
224225
totalReadyReplicas := int32(0)
225226
totalDeployments := int32(0)
@@ -251,12 +252,14 @@ func (r *WorkloadReconciler) reconcileWorkloadStatus(
251252
foundAvailableDeployment := false
252253
replicas := int32(0)
253254
currentReplicas := int32(0)
255+
updatedReplicas := int32(0)
254256
desiredReplicas := int32(0)
255257
readyReplicas := int32(0)
256258
totalDeployments += int32(len(placementDeployments))
257259
for _, deployment := range placementDeployments {
258260
replicas += deployment.Status.Replicas
259261
currentReplicas += deployment.Status.CurrentReplicas
262+
updatedReplicas += deployment.Status.UpdatedReplicas
260263
desiredReplicas += deployment.Status.DesiredReplicas
261264
readyReplicas += deployment.Status.ReadyReplicas
262265

@@ -266,11 +269,13 @@ func (r *WorkloadReconciler) reconcileWorkloadStatus(
266269
}
267270
totalReplicas += replicas
268271
totalCurrentReplicas += currentReplicas
272+
totalUpdatedReplicas += updatedReplicas
269273
totalDesiredReplicas += desiredReplicas
270274
totalReadyReplicas += readyReplicas
271275

272276
placementStatus.Replicas = replicas
273277
placementStatus.CurrentReplicas = currentReplicas
278+
placementStatus.UpdatedReplicas = updatedReplicas
274279
placementStatus.DesiredReplicas = desiredReplicas
275280
placementStatus.ReadyReplicas = readyReplicas
276281

@@ -304,8 +309,10 @@ func (r *WorkloadReconciler) reconcileWorkloadStatus(
304309
newWorkloadStatus.Deployments = totalDeployments
305310
newWorkloadStatus.Replicas = totalReplicas
306311
newWorkloadStatus.CurrentReplicas = totalCurrentReplicas
312+
newWorkloadStatus.UpdatedReplicas = totalUpdatedReplicas
307313
newWorkloadStatus.DesiredReplicas = totalDesiredReplicas
308314
newWorkloadStatus.ReadyReplicas = totalReadyReplicas
315+
newWorkloadStatus.ObservedGeneration = workload.Generation
309316

310317
if equality.Semantic.DeepEqual(workload.Status, newWorkloadStatus) {
311318
return nil

internal/controller/workloaddeployment_controller.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,17 @@ func (r *WorkloadDeploymentReconciler) Reconcile(ctx context.Context, req mcreco
169169
desiredReplicas = 0
170170
}
171171

172-
currentReplicas, readyReplicas, quotaBlockedReplicas, err := r.reconcileInstanceGates(ctx, cl.GetClient(), &deployment, instances.Items, networkReady)
172+
currentReplicas, updatedReplicas, readyReplicas, quotaBlockedReplicas, err := r.reconcileInstanceGates(ctx, cl.GetClient(), &deployment, instances.Items, networkReady)
173173
if err != nil {
174174
return ctrl.Result{}, err
175175
}
176176

177177
deployment.Status.Replicas = int32(replicas)
178178
deployment.Status.CurrentReplicas = int32(currentReplicas)
179+
deployment.Status.UpdatedReplicas = int32(updatedReplicas)
179180
deployment.Status.DesiredReplicas = desiredReplicas
180181
deployment.Status.ReadyReplicas = int32(readyReplicas)
182+
deployment.Status.ObservedGeneration = deployment.Generation
181183

182184
if quotaBlockedReplicas > 0 {
183185
apimeta.SetStatusCondition(&deployment.Status.Conditions, metav1.Condition{
@@ -233,7 +235,7 @@ func (r *WorkloadDeploymentReconciler) reconcileInstanceGates(
233235
deployment *computev1alpha.WorkloadDeployment,
234236
instances []computev1alpha.Instance,
235237
networkReady bool,
236-
) (currentReplicas, readyReplicas, quotaBlockedReplicas int, err error) {
238+
) (currentReplicas, updatedReplicas, readyReplicas, quotaBlockedReplicas int, err error) {
237239
templateHash := instancecontrol.ComputeHash(deployment.Spec.Template)
238240
for _, instance := range instances {
239241
if apimeta.IsStatusConditionPresentAndEqual(instance.Status.Conditions, computev1alpha.InstanceQuotaGranted, metav1.ConditionFalse) {
@@ -252,22 +254,34 @@ func (r *WorkloadDeploymentReconciler) reconcileInstanceGates(
252254
instance.Spec.Controller.SchedulingGates = newGates
253255
return nil
254256
}); patchErr != nil {
255-
return 0, 0, 0, fmt.Errorf("failed updating instance: %w", patchErr)
257+
return 0, 0, 0, 0, fmt.Errorf("failed updating instance: %w", patchErr)
256258
}
257259
}
258260
}
259261

260-
if apimeta.IsStatusConditionTrue(instance.Status.Conditions, computev1alpha.InstanceProgrammed) {
261-
if instance.Status.Controller.ObservedTemplateHash == templateHash {
262-
currentReplicas++
263-
}
262+
// An instance is "updated" once it has observed the desired template
263+
// revision, regardless of readiness. Counting these (even before they are
264+
// Programmed) makes a rolling update / restart observable: UpdatedReplicas
265+
// dips below Replicas while the recreated instance comes up, then recovers.
266+
// Status.Controller is a pointer the infra provider may not have populated
267+
// yet; guard the deref to avoid a panic that would abort the reconcile.
268+
onLatestRevision := instance.Status.Controller != nil &&
269+
instance.Status.Controller.ObservedTemplateHash == templateHash
270+
if onLatestRevision {
271+
updatedReplicas++
272+
}
273+
274+
// CurrentReplicas is the Programmed subset of UpdatedReplicas — updated
275+
// instances that are ready to serve.
276+
if onLatestRevision && apimeta.IsStatusConditionTrue(instance.Status.Conditions, computev1alpha.InstanceProgrammed) {
277+
currentReplicas++
264278
}
265279

266280
if apimeta.IsStatusConditionTrue(instance.Status.Conditions, computev1alpha.InstanceReady) {
267281
readyReplicas++
268282
}
269283
}
270-
return currentReplicas, readyReplicas, quotaBlockedReplicas, nil
284+
return currentReplicas, updatedReplicas, readyReplicas, quotaBlockedReplicas, nil
271285
}
272286

273287
// reconcileNetworks ensures NetworkBindings and SubnetClaims exist for all

0 commit comments

Comments
 (0)