From 8d91e42a9f5e69afaa6a76398e7d8f4eebb62616 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 26 Jun 2026 13:17:12 +0000 Subject: [PATCH] cmd/hi: share the test-container prefix matcher and check the k3s image Factor the hs-/ts-/derp-/k3s- prefix set into one helper used by cleanup and docker; add a doctor check for the ghcr k3s image. --- cmd/hi/cleanup.go | 21 ++++++++++++++++++--- cmd/hi/docker.go | 2 +- cmd/hi/doctor.go | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/cmd/hi/cleanup.go b/cmd/hi/cleanup.go index 5ad624ce..d58ab779 100644 --- a/cmd/hi/cleanup.go +++ b/cmd/hi/cleanup.go @@ -187,14 +187,29 @@ func removeContainerWithRetry(ctx context.Context, cli *client.Client, container return err == nil } +// testContainerNamePrefixes are the name prefixes used by containers that the +// integration test harness creates (headscale, tailscale, DERP, and k3s). +var testContainerNamePrefixes = []string{"hs-", "ts-", "derp-", "k3s-"} + +// matchesTestContainerPrefix reports whether name belongs to an integration +// test container, ignoring any leading "/" that Docker prefixes names with. +func matchesTestContainerPrefix(name string) bool { + name = strings.TrimPrefix(name, "/") + for _, prefix := range testContainerNamePrefixes { + if strings.HasPrefix(name, prefix) { + return true + } + } + + return false +} + // isTestContainerName reports whether any of the container names belong to an // integration test container. func isTestContainerName(names []string) bool { for _, name := range names { if strings.Contains(name, "headscale-test-suite") || - strings.Contains(name, "hs-") || - strings.Contains(name, "ts-") || - strings.Contains(name, "derp-") { + matchesTestContainerPrefix(name) { return true } } diff --git a/cmd/hi/docker.go b/cmd/hi/docker.go index cdfa775c..102ee526 100644 --- a/cmd/hi/docker.go +++ b/cmd/hi/docker.go @@ -735,7 +735,7 @@ func getCurrentTestContainers(containers []container.Summary, testContainerID st for _, cont := range containers { for _, name := range cont.Names { containerName := strings.TrimPrefix(name, "/") - if strings.HasPrefix(containerName, "hs-") || strings.HasPrefix(containerName, "ts-") { + if matchesTestContainerPrefix(containerName) { // Check if container has matching run ID label if cont.Labels != nil && cont.Labels["hi.run-id"] == runID { testRunContainers = append(testRunContainers, testContainer{ diff --git a/cmd/hi/doctor.go b/cmd/hi/doctor.go index 593e2f37..7bd42fb9 100644 --- a/cmd/hi/doctor.go +++ b/cmd/hi/doctor.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/juanfont/headscale/integration/dockertestutil" + "github.com/juanfont/headscale/integration/k3sic" ) const ( @@ -21,6 +22,7 @@ const ( nameDockerContext = "Docker Context" nameDockerSocket = "Docker Socket" nameGolangImage = "Golang Image" + nameK3sImage = "K3s Image" nameGoInstall = "Go Installation" ) @@ -66,6 +68,7 @@ func runDoctorCheck(ctx context.Context) error { results = append(results, checkDockerSocket(ctx)) results = append(results, checkDockerHubCredentials()) results = append(results, checkGolangImage(ctx)) + results = append(results, checkK3sImage(ctx)) } // Check 3: Go installation @@ -242,6 +245,44 @@ func checkGolangImage(ctx context.Context) DoctorResult { return pass(nameGolangImage, fmt.Sprintf("Golang image %s is now available", imageName)) } +// checkK3sImage verifies the ghcr k3s image used by TestK8sOperator is available +// locally or can be pulled. The image is pinned (see [k3sic.K3sImage]). +func checkK3sImage(ctx context.Context) DoctorResult { + cli, err := createDockerClient(ctx) + if err != nil { + return fail(nameK3sImage, "Cannot create Docker client for image check") + } + defer cli.Close() + + imageName := k3sic.K3sImage + + available, err := checkImageAvailableLocally(ctx, cli, imageName) + if err != nil { + return fail( + nameK3sImage, + fmt.Sprintf("Cannot check k3s image %s: %v", imageName, err), + "Check Docker daemon status", + "Try: docker images | grep k3s", + ) + } + + if available { + return pass(nameK3sImage, fmt.Sprintf("K3s image %s is available locally", imageName)) + } + + err = ensureImageAvailable(ctx, cli, imageName, false) + if err != nil { + return warn( + nameK3sImage, + fmt.Sprintf("K3s image %s not available locally and could not pull: %v", imageName, err), + "Only TestK8sOperator needs this image; other tests are unaffected", + "Try: docker pull "+imageName, + ) + } + + return pass(nameK3sImage, fmt.Sprintf("K3s image %s is now available", imageName)) +} + // checkGoInstallation verifies Go is installed and working. func checkGoInstallation(ctx context.Context) DoctorResult { _, err := exec.LookPath("go")