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
Summary
BootcNodePool.spec.image.refaccepts a tag or a digest, but theBootcNodechildren always show a digest. A pool that specifies a tag produces children
whose
specdisagrees with the parent'sspec.In Kubernetes,
specis the desired state a controller moves toward. A childresource's
specsilently disagreeing with its owner's is surprising: if I ask apool to move its nodes to
:v1.2, I expect eachBootcNodeto say:v1.2. Thisis an API-consistency request, independent of any downstream behaviour.
Current behaviour
internal/controller/bootcnodepool_controller.go:reference.TrimNameddiscards the tag, so the digest form is builtunconditionally. Example — pool:
child after reconcile:
syncBootcNodeSpecalso keeps it that way, and because the values differ itresets
desiredImageStatefromBootedback toStaged— so hand-patching thefield 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.targetDigestalready holds the resolved digest, and re-resolution already drives rollouts:
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.
classifyNodecurrently requires a digesteddesiredImage(internal/controller/rollout.go):The existing comment right above it already suggests the fix:
Comparing
pool.Status.TargetDigestagainstbn.Status.Booted.ImageDigestdecouples the classification from the ref form, after which
desiredImagecancarry whatever the pool specified. That also removes a parse and an error path
that becomes unreachable.
Daemon side: it passes
desiredImagetobootc switch, which accepts eitherform, 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 controllerresolving
targetDigestand the daemon running the switch, so a node could landon 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
2e6cce1