From 5dbf3e557557f719335ad17c817ac3c1cd8c5531 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Mon, 8 Jun 2026 14:19:28 +0000 Subject: [PATCH 1/4] ci: bump bink to fdfdc863 (--target-imgref support) Assisted-by: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0dde973..a12e012 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ on: branches: [main] env: - BINK_COMMIT: 2b1bbdca74fc36d319fb1ea6d0523d9a732fcb1f + BINK_COMMIT: fdfdc863d9f4e9caf83e56c06bf7f940cfb9f315 permissions: {} From adba1a08d3e121a25db1dbfad8e82c1660efe1b3 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 9 Jun 2026 09:00:17 +0000 Subject: [PATCH 2/4] e2e: seed bink registry with real node image Seed the bink registry with the bootc image from the node disk image so e2e tests can reference real images. Add --target-imgref support to make nodes track the registry image. Assisted-by: Claude Opus 4.6 (1M context) --- Makefile | 23 ++++++++++++++--- test/e2e/bootcnode_test.go | 6 ++++- test/e2e/e2eutil/env.go | 52 +++++++++++++++++++++++++++++++++----- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index c76131b..ccb404f 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,8 @@ CONTAINER_TOOL ?= podman BINK_CLUSTER_NAME ?= e2e KUBECONFIG_BINK ?= ./kubeconfig-$(BINK_CLUSTER_NAME) ARTIFACTS ?= $(abspath _output/logs) +BINK_NODE_DISK_IMAGE ?= ghcr.io/alicefr/bink/node:v1.35-fedora-44-disk +BINK_LOCAL_REGISTRY_NODE_IMAGE ?= registry.cluster.local:5000/node # YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header. YEAR ?= $(shell date +%Y) @@ -62,7 +64,10 @@ e2e: ## Run e2e tests (requires: make deploy-bink). V=1 for verbose. RUN= rm -rf $(ARTIFACTS) cd test/e2e && KUBECONFIG=$(abspath $(KUBECONFIG_BINK)) BINK_CLUSTER_NAME=$(BINK_CLUSTER_NAME) \ $(if $(BINK_NODE_IMAGE),BINK_NODE_IMAGE=$(BINK_NODE_IMAGE)) \ + BINK_NODE_DISK_IMAGE=$(BINK_NODE_DISK_IMAGE) \ + BINK_LOCAL_REGISTRY_NODE_IMAGE=$(BINK_LOCAL_REGISTRY_NODE_IMAGE) \ ARTIFACTS=$(ARTIFACTS) \ + BINK_NODE_IMAGE_DIGEST=$$(skopeo inspect --tls-verify=false --format '{{.Digest}}' docker://localhost:5000/node:latest) \ go test -timeout 10m -count=1 $(if $(V),-v) $(if $(RUN),-run $(RUN)) . ##@ Build @@ -111,12 +116,22 @@ undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/. # Note the :latest tag here: this makes the pull policy be Always. IMG_BINK ?= registry.cluster.local:5000/bootc-operator-e2e:latest -.PHONY: start-bink -start-bink: ## Start a bink cluster (idempotent). +.PHONY: seed-node-image +seed-node-image: ## Pull the bootc node image by digest and push to the bink registry. bink registry start - bink cluster list 2>&1 | grep -qw $(BINK_CLUSTER_NAME) || \ + podman pull $(BINK_NODE_DISK_IMAGE) + bootc_img=$$(podman inspect --format '{{index .Config.Labels "bink.bootc-image"}}' $(BINK_NODE_DISK_IMAGE)) && \ + bootc_digest=$$(podman inspect --format '{{index .Config.Labels "bink.bootc-image-digest"}}' $(BINK_NODE_DISK_IMAGE)) && \ + podman pull "$$bootc_img@$$bootc_digest" && \ + podman tag "$$bootc_img@$$bootc_digest" localhost:5000/node:latest + podman push --tls-verify=false localhost:5000/node:latest + +.PHONY: start-bink +start-bink: seed-node-image ## Start a bink cluster (idempotent). + bink cluster list 2>&1 | grep -qw $(BINK_CLUSTER_NAME) || { \ + node_digest=$$(skopeo inspect --tls-verify=false --format '{{.Digest}}' docker://localhost:5000/node:latest) && \ bink cluster start --cluster-name $(BINK_CLUSTER_NAME) --node-name controller --api-port 0 --expose $(KUBECONFIG_BINK) \ - $(if $(BINK_NODE_IMAGE),--node-image $(BINK_NODE_IMAGE)) + --node-image $(BINK_NODE_DISK_IMAGE) --target-imgref $(BINK_LOCAL_REGISTRY_NODE_IMAGE)@$$node_digest; } kubectl --kubeconfig $(KUBECONFIG_BINK) wait --for=condition=Ready node/controller --timeout=5m .PHONY: deploy-bink diff --git a/test/e2e/bootcnode_test.go b/test/e2e/bootcnode_test.go index 1e7850c..d4fd5b1 100644 --- a/test/e2e/bootcnode_test.go +++ b/test/e2e/bootcnode_test.go @@ -35,8 +35,12 @@ func TestControllerMembership(t *testing.T) { ctx := context.Background() - // Create a pool selecting this test's nodes. + // Use real registry image when seeded; fall back to fake ref for + // local dev runs without deploy-bink. imageRef := testutil.ImageDigestRefA + if ref := env.NodeImageDigestedPullSpec(); ref != "" { + imageRef = ref + } pool := env.NewPool("workers", imageRef) g.Expect(env.Client.Create(ctx, pool)).To(Succeed()) diff --git a/test/e2e/e2eutil/env.go b/test/e2e/e2eutil/env.go index 0472851..a0d59fc 100644 --- a/test/e2e/e2eutil/env.go +++ b/test/e2e/e2eutil/env.go @@ -52,6 +52,14 @@ type Env struct { // nodes tracks node names added via AddNode for cleanup. nodes []string + + // nodeImageDigest is the manifest digest of the bootc image seeded + // into the bink registry (e.g. "sha256:abc123..."). Empty when not seeded. + nodeImageDigest string + + // nodeImageRegistry is the in-cluster registry path for the seeded node image + // (e.g. "registry.cluster.local:5000/node"). Empty when not seeded. + nodeImageRegistry string } // New connects to an existing bink cluster and returns an Env ready @@ -70,12 +78,17 @@ func New(t *testing.T) *Env { t.Fatal("BINK_CLUSTER_NAME must be set") } + nodeImageDigest := os.Getenv("BINK_NODE_IMAGE_DIGEST") + nodeImageRegistry := os.Getenv("BINK_LOCAL_REGISTRY_NODE_IMAGE") + k8sClient := buildClient(t, kubeconfigPath) env := &Env{ - Client: k8sClient, - clusterName: clusterName, - testID: sanitizeTestName(t.Name()), + Client: k8sClient, + clusterName: clusterName, + testID: sanitizeTestName(t.Name()), + nodeImageDigest: nodeImageDigest, + nodeImageRegistry: nodeImageRegistry, } t.Cleanup(func() { @@ -89,8 +102,9 @@ func New(t *testing.T) *Env { type NodeOption func(*nodeConfig) type nodeConfig struct { - memory int - labels map[string]string + memory int + labels map[string]string + targetImgRef string } // WithMemory sets the VM memory in MB for the node. @@ -111,6 +125,15 @@ func WithLabel(key, value string) NodeOption { } } +// WithTargetImgRef sets the target image reference for the node, +// passed as --target-imgref to bink node add. Overrides the automatic +// default that AddNode applies when registry metadata is available. +func WithTargetImgRef(ref string) NodeOption { + return func(c *nodeConfig) { + c.targetImgRef = ref + } +} + // AddNode provisions a worker node via bink, waits for it to be Ready, // and returns the node name. The node is labeled with LabelE2ETest // (and any extra labels from WithLabel). @@ -122,6 +145,13 @@ func (e *Env) AddNode(t *testing.T, opts ...NodeOption) string { o(cfg) } + if cfg.targetImgRef == "" { + if e.nodeImageRegistry == "" { + t.Fatal("BINK_LOCAL_REGISTRY_NODE_IMAGE must be set (or use WithTargetImgRef)") + } + cfg.targetImgRef = e.nodeImageRegistry + ":latest" + } + nodeName := e.generateNodeName(t) // Provision the node with labels applied at join time. @@ -133,9 +163,10 @@ func (e *Env) AddNode(t *testing.T, opts ...NodeOption) string { if cfg.memory > 0 { args = append(args, "--memory", fmt.Sprintf("%d", cfg.memory)) } - if img := os.Getenv("BINK_NODE_IMAGE"); img != "" { + if img := os.Getenv("BINK_NODE_DISK_IMAGE"); img != "" { args = append(args, "--node-image", img) } + args = append(args, "--target-imgref", cfg.targetImgRef) t.Logf("Adding node %q...", nodeName) if err := runBink(t, args...); err != nil { t.Fatalf("adding node %q: %v", nodeName, err) @@ -169,6 +200,15 @@ func (e *Env) TestLabels() map[string]string { return map[string]string{LabelE2ETest: e.testID} } +// NodeImageDigestedPullSpec returns the digest-qualified reference for the +// seeded node image (e.g. "registry.cluster.local:5000/node@sha256:abc123"). +func (e *Env) NodeImageDigestedPullSpec() string { + if e.nodeImageRegistry == "" || e.nodeImageDigest == "" { + return "" + } + return e.nodeImageRegistry + "@" + e.nodeImageDigest +} + // cleanup gathers diagnostic logs, then deletes test-scoped resources // and bink nodes. func (e *Env) cleanup(t *testing.T) { From 034d5c48a1bc0bf2166d0bbdbc289718f00d32c7 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 9 Jun 2026 09:00:34 +0000 Subject: [PATCH 3/4] e2e: assert booted image matches seeded registry image Verify that the BootcNode booted image and digest match the seeded registry image when available. Assisted-by: Claude Opus 4.6 (1M context) --- test/e2e/bootcnode_test.go | 17 ++++++----------- test/e2e/e2eutil/env.go | 11 ++++++++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/test/e2e/bootcnode_test.go b/test/e2e/bootcnode_test.go index d4fd5b1..221f113 100644 --- a/test/e2e/bootcnode_test.go +++ b/test/e2e/bootcnode_test.go @@ -14,7 +14,6 @@ import ( bootcv1alpha1 "github.com/jlebon/bootc-operator/api/v1alpha1" "github.com/jlebon/bootc-operator/test/e2e/e2eutil" - testutil "github.com/jlebon/bootc-operator/test/util" ) const ( @@ -35,13 +34,7 @@ func TestControllerMembership(t *testing.T) { ctx := context.Background() - // Use real registry image when seeded; fall back to fake ref for - // local dev runs without deploy-bink. - imageRef := testutil.ImageDigestRefA - if ref := env.NodeImageDigestedPullSpec(); ref != "" { - imageRef = ref - } - pool := env.NewPool("workers", imageRef) + pool := env.NewPool("workers", env.NodeImageDigestedPullSpec()) g.Expect(env.Client.Create(ctx, pool)).To(Succeed()) // Wait for BootcNode to appear for the worker. @@ -56,7 +49,7 @@ func TestControllerMembership(t *testing.T) { g.Expect(owner.Name).To(Equal(pool.Name)) // Verify desiredImage. - g.Expect(bn.Spec.DesiredImage).To(Equal(imageRef)) + g.Expect(bn.Spec.DesiredImage).To(Equal(env.NodeImageDigestedPullSpec())) // Verify the worker has the managed label. var node corev1.Node @@ -83,8 +76,10 @@ func TestControllerMembership(t *testing.T) { g.Eventually(func(g Gomega) { g.Expect(env.Client.Get(ctx, client.ObjectKey{Name: nodeName}, &bn)).To(Succeed()) g.Expect(bn.Status.Booted).NotTo(BeNil(), "expected booted status to be populated") - g.Expect(bn.Status.Booted.Image).NotTo(BeEmpty(), "expected booted image to be non-empty") - g.Expect(bn.Status.Booted.ImageDigest).NotTo(BeEmpty(), "expected booted imageDigest to be non-empty") + g.Expect(bn.Status.Booted.Image).To(Equal(env.NodeImageDigestedPullSpec()), + "booted image should match seeded registry image") + g.Expect(bn.Status.Booted.ImageDigest).To(Equal(env.NodeImageDigest()), + "booted image digest should match seeded registry image") g.Expect(bn.Status.Conditions).To(ContainElement(And( HaveField("Type", bootcv1alpha1.NodeIdle), HaveField("Status", metav1.ConditionTrue), diff --git a/test/e2e/e2eutil/env.go b/test/e2e/e2eutil/env.go index a0d59fc..423ca19 100644 --- a/test/e2e/e2eutil/env.go +++ b/test/e2e/e2eutil/env.go @@ -146,10 +146,10 @@ func (e *Env) AddNode(t *testing.T, opts ...NodeOption) string { } if cfg.targetImgRef == "" { - if e.nodeImageRegistry == "" { - t.Fatal("BINK_LOCAL_REGISTRY_NODE_IMAGE must be set (or use WithTargetImgRef)") + if e.nodeImageRegistry == "" || e.nodeImageDigest == "" { + t.Fatal("BINK_LOCAL_REGISTRY_NODE_IMAGE and NODE_IMAGE_DIGEST must be set (or use WithTargetImgRef)") } - cfg.targetImgRef = e.nodeImageRegistry + ":latest" + cfg.targetImgRef = e.nodeImageRegistry + "@" + e.nodeImageDigest } nodeName := e.generateNodeName(t) @@ -209,6 +209,11 @@ func (e *Env) NodeImageDigestedPullSpec() string { return e.nodeImageRegistry + "@" + e.nodeImageDigest } +// NodeImageDigest returns the manifest digest of the seeded node image. +func (e *Env) NodeImageDigest() string { + return e.nodeImageDigest +} + // cleanup gathers diagnostic logs, then deletes test-scoped resources // and bink nodes. func (e *Env) cleanup(t *testing.T) { From 45ada58b49e873e3721324aa3f3941b165e68424 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 9 Jun 2026 09:00:45 +0000 Subject: [PATCH 4/4] Makefile: add build-update-image target for update testing Move the update image build logic out of e2e test code and into a Makefile target. Build the update image as part of deploy-bink so it is available for both manual testing and e2e tests. Assisted-by: Claude Opus 4.6 (1M context) --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ccb404f..4245b3c 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,12 @@ build-daemon: ## Build daemon binary. buildimg: ## Build container image. $(CONTAINER_TOOL) build -t $(IMG) . +.PHONY: build-update-image +build-update-image: ## Build a derived node image for update testing and push to bink registry. + @printf 'FROM localhost:5000/node:latest\nRUN touch /usr/share/update-marker\n' | \ + podman build -t localhost:5000/node:update -f - . + podman push --tls-verify=false localhost:5000/node:update + ##@ Deployment ifndef ignore-not-found @@ -135,7 +141,7 @@ start-bink: seed-node-image ## Start a bink cluster (idempotent). kubectl --kubeconfig $(KUBECONFIG_BINK) wait --for=condition=Ready node/controller --timeout=5m .PHONY: deploy-bink -deploy-bink: start-bink kustomize ## Deploy to a bink cluster (requires: buildimg). +deploy-bink: start-bink build-update-image kustomize ## Deploy to a bink cluster (requires: buildimg). podman push --tls-verify=false $(IMG) localhost:5000/bootc-operator-e2e:latest # On re-deploy, restart the rollout to force a re-pull of the :latest tag. # On fresh deploy, skip the restart -- the pod is already pulling the correct image.