Skip to content

SetStarted/SetFinished status patches are no-ops due to snapshot taken after mutation #1232

Description

@KrishnaParihar1

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

  1. 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:

  2. 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)
  1. 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.

  2. 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.

  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions