Skip to content

Commit ba0b627

Browse files
committed
e2e: add TestTagResolution for tag-based image refs
Create a pool with a tag ref, verify the controller resolves it to the correct digest, then retag to an update image and verify re-resolution triggers a rollout. The test patches the controller deployment to use a short tag-resolution-interval (10s) and restores the original args on cleanup. Assisted-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Alice Frosi <afrosi@redhat.com>
1 parent cb61ebd commit ba0b627

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

test/e2e/bootcnode_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"testing"
1111
"time"
1212

13+
"github.com/google/go-containerregistry/pkg/name"
14+
"github.com/google/go-containerregistry/pkg/v1/remote"
1315
. "github.com/onsi/gomega"
1416
corev1 "k8s.io/api/core/v1"
1517
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -188,3 +190,172 @@ func TestUpdateReboot(t *testing.T) {
188190

189191
t.Logf("Verified update-marker exists on host via daemon pod")
190192
}
193+
194+
// retagImage reads the image at srcRef from the localhost registry and
195+
// tags it as dstTag. Both refs use localhost:5000 (host-side registry).
196+
func retagImage(t *testing.T, srcRef, dstTag string) {
197+
t.Helper()
198+
199+
src, err := name.ParseReference(srcRef, name.Insecure)
200+
if err != nil {
201+
t.Fatalf("parsing src ref %q: %v", srcRef, err)
202+
}
203+
desc, err := remote.Get(src)
204+
if err != nil {
205+
t.Fatalf("fetching %q: %v", srcRef, err)
206+
}
207+
img, err := desc.Image()
208+
if err != nil {
209+
t.Fatalf("getting image from descriptor: %v", err)
210+
}
211+
212+
dst, err := name.ParseReference(dstTag, name.Insecure)
213+
if err != nil {
214+
t.Fatalf("parsing dst ref %q: %v", dstTag, err)
215+
}
216+
if err := remote.Write(dst, img); err != nil {
217+
t.Fatalf("writing %q: %v", dstTag, err)
218+
}
219+
}
220+
221+
const (
222+
controllerNamespace = "bootc-operator"
223+
controllerDeployment = "bootc-operator-controller-manager"
224+
)
225+
226+
// setTagResolutionInterval patches the controller deployment to use
227+
// the given interval and waits for the rollout to complete. The
228+
// original args are restored in t.Cleanup.
229+
func setTagResolutionInterval(t *testing.T, interval string) {
230+
t.Helper()
231+
232+
kubeconfigPath := os.Getenv("KUBECONFIG")
233+
flag := "--tag-resolution-interval=" + interval
234+
235+
// Read current args to restore later.
236+
out, err := exec.Command("kubectl", "--kubeconfig", kubeconfigPath,
237+
"-n", controllerNamespace, "get", "deploy", controllerDeployment,
238+
"-o", "jsonpath={.spec.template.spec.containers[0].args}").CombinedOutput()
239+
if err != nil {
240+
t.Fatalf("reading deployment args: %s: %v", string(out), err)
241+
}
242+
originalArgs := string(out)
243+
244+
// Patch args to include the interval flag.
245+
patch := fmt.Sprintf(`{"spec":{"template":{"spec":{"containers":[{"name":"manager","args":["--leader-elect","--health-probe-bind-address=:8081","%s"]}]}}}}`, flag)
246+
if out, err := exec.Command("kubectl", "--kubeconfig", kubeconfigPath,
247+
"-n", controllerNamespace, "patch", "deploy", controllerDeployment,
248+
"--type=strategic", "-p", patch).CombinedOutput(); err != nil {
249+
t.Fatalf("patching deployment: %s: %v", string(out), err)
250+
}
251+
252+
// Wait for rollout.
253+
if out, err := exec.Command("kubectl", "--kubeconfig", kubeconfigPath,
254+
"-n", controllerNamespace, "rollout", "status", "deploy/"+controllerDeployment,
255+
"--timeout=2m").CombinedOutput(); err != nil {
256+
t.Fatalf("waiting for rollout: %s: %v", string(out), err)
257+
}
258+
259+
t.Logf("Set --tag-resolution-interval=%s (was %s)", interval, originalArgs)
260+
261+
t.Cleanup(func() {
262+
// Restore original args.
263+
patch := fmt.Sprintf(`{"spec":{"template":{"spec":{"containers":[{"name":"manager","args":["--leader-elect","--health-probe-bind-address=:8081"]}]}}}}`)
264+
if out, err := exec.Command("kubectl", "--kubeconfig", kubeconfigPath,
265+
"-n", controllerNamespace, "patch", "deploy", controllerDeployment,
266+
"--type=strategic", "-p", patch).CombinedOutput(); err != nil {
267+
t.Logf("WARNING: restoring deployment args: %s: %v", string(out), err)
268+
return
269+
}
270+
if out, err := exec.Command("kubectl", "--kubeconfig", kubeconfigPath,
271+
"-n", controllerNamespace, "rollout", "status", "deploy/"+controllerDeployment,
272+
"--timeout=2m").CombinedOutput(); err != nil {
273+
t.Logf("WARNING: waiting for rollout after restore: %s: %v", string(out), err)
274+
}
275+
})
276+
}
277+
278+
// TestTagResolution creates a pool with a tag-based image ref, verifies
279+
// the controller resolves the tag to a digest, then retags the image
280+
// and verifies re-resolution triggers a rollout.
281+
func TestTagResolution(t *testing.T) {
282+
g := NewWithT(t)
283+
g.SetDefaultEventuallyTimeout(pollTimeout)
284+
g.SetDefaultEventuallyPollingInterval(pollInterval)
285+
286+
env := e2eutil.New(t)
287+
288+
ctx := context.Background()
289+
290+
// Shorten the tag resolution interval so re-resolution happens quickly.
291+
setTagResolutionInterval(t, "10s")
292+
293+
nodeName := env.AddNode(t)
294+
295+
// The seed step already pushed node:latest with the original image.
296+
// Create a pool using the tag ref.
297+
pool := env.NewPool("tag", env.NodeImageTagRef())
298+
g.Expect(env.Client.Create(ctx, pool)).To(Succeed())
299+
300+
// Verify targetDigest is resolved to the original image digest.
301+
g.Eventually(func(g Gomega) string {
302+
var p bootcv1alpha1.BootcNodePool
303+
g.Expect(env.Client.Get(ctx, client.ObjectKeyFromObject(pool), &p)).To(Succeed())
304+
return p.Status.TargetDigest
305+
}).WithTimeout(1 * time.Minute).Should(Equal(env.NodeImageDigest()))
306+
307+
t.Logf("Tag resolved to original digest %s", env.NodeImageDigest())
308+
309+
// Wait for node to reach Idle with the original image.
310+
g.Eventually(func(g Gomega) bootcv1alpha1.BootcNodeStatus {
311+
var bn bootcv1alpha1.BootcNode
312+
g.Expect(env.Client.Get(ctx, client.ObjectKey{Name: nodeName}, &bn)).To(Succeed())
313+
return bn.Status
314+
}).WithTimeout(3 * time.Minute).Should(And(
315+
HaveField("Booted", And(
316+
Not(BeNil()),
317+
HaveField("ImageDigest", Equal(env.NodeImageDigest())),
318+
)),
319+
HaveField("Conditions", ContainElement(And(
320+
HaveField("Type", bootcv1alpha1.NodeIdle),
321+
HaveField("Status", metav1.ConditionTrue),
322+
))),
323+
))
324+
325+
t.Logf("Node %q is Idle with original image", nodeName)
326+
327+
// Retag node:latest to point at the update image.
328+
retagImage(t,
329+
"localhost:5000/node@"+env.NodeImageUpdateDigest(),
330+
"localhost:5000/node:latest",
331+
)
332+
333+
t.Logf("Retagged node:latest to update digest %s", env.NodeImageUpdateDigest())
334+
335+
// Wait for the controller to re-resolve and pick up the new digest.
336+
g.Eventually(func(g Gomega) string {
337+
var p bootcv1alpha1.BootcNodePool
338+
g.Expect(env.Client.Get(ctx, client.ObjectKeyFromObject(pool), &p)).To(Succeed())
339+
return p.Status.TargetDigest
340+
}).WithTimeout(1 * time.Minute).Should(Equal(env.NodeImageUpdateDigest()))
341+
342+
t.Logf("Tag re-resolved to update digest %s", env.NodeImageUpdateDigest())
343+
344+
// Wait for node to reach Idle with the update image.
345+
g.Eventually(func(g Gomega) bootcv1alpha1.BootcNodeStatus {
346+
var bn bootcv1alpha1.BootcNode
347+
g.Expect(env.Client.Get(ctx, client.ObjectKey{Name: nodeName}, &bn)).To(Succeed())
348+
return bn.Status
349+
}).WithTimeout(5 * time.Minute).Should(And(
350+
HaveField("Booted", And(
351+
Not(BeNil()),
352+
HaveField("ImageDigest", Equal(env.NodeImageUpdateDigest())),
353+
)),
354+
HaveField("Conditions", ContainElement(And(
355+
HaveField("Type", bootcv1alpha1.NodeIdle),
356+
HaveField("Status", metav1.ConditionTrue),
357+
))),
358+
))
359+
360+
t.Logf("Node %q is Idle with update image", nodeName)
361+
}

0 commit comments

Comments
 (0)