Description
SetStarted and SetFinished in operator/job/status.go use client.MergeFrom to build a JSON merge patch for the job's status, but the "before" snapshot is captured after the status has already been mutated on the same object. As a result, the generated patch is empty and the Started/Finished flags and their associated Ready/Progressing conditions are never actually sent to the API server through these two code paths.
Root cause
func (c *Config) SetStarted(ctx context.Context, message string, args ...interface{}) {
log := controllerruntime.LoggerFrom(ctx)
status := c.Obj.GetStatus()
status.SetStarted(fmt.Sprintf(message, args...))
c.Obj.SetStatus(status) // (1) mutate first
patch := client.MergeFrom(c.Obj.DeepCopyObject().(client.Object)) // (2) snapshot AFTER mutation
err := c.Client.Status().Patch(ctx, c.Obj, patch) // (3) diff base vs current -> identical
...
}
Additional Context
No response
Logs
Expected Behavior
When SetStarted/SetFinished (operator/job/status.go) are called, the resulting Status().Patch() call should send a non-empty merge patch that actually updates .status.started/.status.finished and the corresponding Ready/Progressing conditions on the Backup/Restore/Prune/Check/Archive object in the API server.
Steps To Reproduce
-
This is a logic bug identified through code review of operator/job/status.go, not a field-observed issue, so it's reproducible by inspection/unit test rather than a live-cluster repro:
-
Open operator/job/status.go, SetStarted (lines 63–78) and SetFinished (lines 81–96).
Note the order of operations:
status := c.Obj.GetStatus()
status.SetStarted(...) // mutates c.Obj in place
c.Obj.SetStatus(status)
patch := client.MergeFrom(c.Obj.DeepCopyObject().(client.Object)) // snapshot taken AFTER mutation
err := c.Client.Status().Patch(ctx, c.Obj, patch)
-
Since SetStatus mutates c.Obj in place (pointer receiver, e.g. func (b *Backup) SetStatus(status Status) { b.Status = status }), the DeepCopyObject() snapshot used as the MergeFrom base is taken after the new status is already applied.
-
client.MergeFrom(base) diffs base against the object passed to Patch() later. Because both are now identical, the generated JSON merge patch is {} — no fields change server-side.
-
To confirm in a test: call SetStarted against a Config backed by a fake client, then re-Get the object from the fake client and assert .Status.Started == true — it will fail because the patch sent was empty.
Version of K8up
k8up-4.1.0 through current main (k8up-4.9.0)
Version of Kubernetes
client-side bug, not Kubernetes-version specific
Distribution of Kubernetes
N/A — not distribution-specific
Description
SetStarted and SetFinished in operator/job/status.go use client.MergeFrom to build a JSON merge patch for the job's status, but the "before" snapshot is captured after the status has already been mutated on the same object. As a result, the generated patch is empty and the Started/Finished flags and their associated Ready/Progressing conditions are never actually sent to the API server through these two code paths.
Root cause
Additional Context
No response
Logs
Expected Behavior
When
SetStarted/SetFinished(operator/job/status.go) are called, theresulting Status().Patch()call should send a non-empty merge patch that actually updates.status.started/.status.finished and the correspondingReady/Progressingconditions on theBackup/Restore/Prune/Check/Archiveobject in the API server.Steps To Reproduce
This is a logic bug identified through code review of operator/job/status.go, not a field-observed issue, so it's reproducible by inspection/unit test rather than a live-cluster repro:
Open operator/job/status.go, SetStarted (lines 63–78) and SetFinished (lines 81–96).
Note the order of operations:
Since SetStatus mutates c.Obj in place (pointer receiver, e.g. func (b *Backup) SetStatus(status Status) { b.Status = status }), the DeepCopyObject() snapshot used as the MergeFrom base is taken after the new status is already applied.
client.MergeFrom(base) diffs base against the object passed to Patch() later. Because both are now identical, the generated JSON merge patch is {} — no fields change server-side.
To confirm in a test: call SetStarted against a Config backed by a fake client, then re-Get the object from the fake client and assert .Status.Started == true — it will fail because the patch sent was empty.
Version of K8up
k8up-4.1.0 through current main (k8up-4.9.0)
Version of Kubernetes
client-side bug, not Kubernetes-version specific
Distribution of Kubernetes
N/A — not distribution-specific