Skip to content

BootcNode.spec.desiredImage does not reflect the ref form its BootcNodePool specifies #146

Description

@zarcen

Summary

BootcNodePool.spec.image.ref accepts a tag or a digest, but the BootcNode
children always show a digest. A pool that specifies a tag produces children
whose spec disagrees with the parent's spec.

In Kubernetes, spec is the desired state a controller moves toward. A child
resource's spec silently disagreeing with its owner's is surprising: if I ask a
pool to move its nodes to :v1.2, I expect each BootcNode to say :v1.2. This
is an API-consistency request, independent of any downstream behaviour.

Current behaviour

internal/controller/bootcnodepool_controller.go:

func desiredImageFromPool(pool *bootcv1alpha1.BootcNodePool) string {
	ref, _ := parseImageRef(pool.Spec.Image.Ref)
	return reference.TrimNamed(ref).String() + "@" + pool.Status.TargetDigest
}

reference.TrimNamed discards the tag, so the digest form is built
unconditionally. Example — pool:

spec:
  image:
    ref: registry.example.com/org/bootc-node:v1.2

child after reconcile:

$ kubectl get bootcnode <node> -o jsonpath='{.spec.desiredImage}'
registry.example.com/org/bootc-node@sha256:66508381...

syncBootcNodeSpec also keeps it that way, and because the values differ it
resets desiredImageState from Booted back to Staged — so hand-patching the
field is not just ineffective but disruptive mid-rollout (it revokes a pending
reboot approval).

Requested behaviour

The child mirrors the parent: a pool specifying a digest yields digests on its
BootcNodes; a pool specifying a tag yields that tag.

Resolution stays exactly where it is today — in status. status.targetDigest
already holds the resolved digest, and re-resolution already drives rollouts:

digest, err := r.TagResolver.Resolve(ctx, pool.Spec.Image.Ref)
...
if pool.Status.TargetDigest != digest {
	log.Info("Resolved tag to new digest", "ref", pool.Spec.Image.Ref, "digest", digest)
}
pool.Status.TargetDigest = digest

So a tag that starts pointing at new content still triggers a rollout, unchanged.
Whether a tag is stable is the user's decision, and pinning a digest remains
available for anyone who wants that guarantee — by specifying a digest, which the
proposal preserves faithfully.

What would need to change

Only the comparison. classifyNode currently requires a digested
desiredImage (internal/controller/rollout.go):

ref, err := parseImageRef(bn.Spec.DesiredImage)
...
digested, ok := ref.(reference.Digested)
if !ok {
	return 0, fmt.Errorf("non-digested desiredImage %q", bn.Spec.DesiredImage)
}

if digested.Digest().String() == bn.Status.Booted.ImageDigest {
	return nodeStateUpToDate, nil
}

The existing comment right above it already suggests the fix:

// We could pass in the pool here and use targetDigest instead to avoid
// parsing. Though I do also like how this function takes just a BootcNode.

Comparing pool.Status.TargetDigest against bn.Status.Booted.ImageDigest
decouples the classification from the ref form, after which desiredImage can
carry whatever the pool specified. That also removes a parse and an error path
that becomes unreachable.

Daemon side: it passes desiredImage to bootc switch, which accepts either
form, so a tag needs no daemon change.

Trade-off to be explicit about

With a tag reaching bootc switch, the tag could move between the controller
resolving targetDigest and the daemon running the switch, so a node could land
on content one resolution newer than the controller intended. The next reconcile
converges it. If that is unacceptable for a deployment, specifying a digest gives
the strict guarantee — which is the point: the choice becomes the user's, expressed
in spec, rather than being made implicitly for them.

Happy to send a PR if this shape looks right, including whether you'd prefer it
unconditional or behind a field.

Environment

  • bootc-operator at commit 2e6cce1
  • bootc 1.16.6, CentOS Stream 10 (kernel 6.12), aarch64
  • Kubernetes v1.36.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions