Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions pkg/k3d/k3d.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,17 @@ func (c *Cluster) importImages(images ...string) error {
return nil
}

// localhost/ images only exist via local builds; nothing can pull them.
var pullable []string
for _, img := range needed {
if !strings.HasPrefix(normalizeImageRef(img), "localhost/") {
pullable = append(pullable, img)
}
}
if err := pullMissingFromHost(pullable); err != nil {
return err
}

args := append([]string{"image", "import", fmt.Sprintf("--cluster=%s", c.Name)}, needed...)
if out, err := exec.Command("k3d", args...).CombinedOutput(); err != nil {
return fmt.Errorf("%w: %s", err, out)
Expand All @@ -563,6 +574,47 @@ func (c *Cluster) importImages(images ...string) error {
return nil
}

// pullMissingFromHost pulls any of the given images that the host Docker
// daemon doesn't list. k3d only imports images the daemon lists, and an image
// pre-pulled by CI isn't guaranteed to still be there when a test imports it.
func pullMissingFromHost(images []string) error {
out, err := exec.Command("docker", "image", "ls", "--format", "{{.Repository}}:{{.Tag}}").Output()
if err != nil {
return errors.Wrap(err, "listing host images")
}
present := make(map[string]bool)
for _, ref := range strings.Fields(string(out)) {
present[normalizeImageRef(ref)] = true
}
for _, img := range images {
if present[normalizeImageRef(img)] {
continue
}
if out, err := exec.Command("docker", "pull", "-q", img).CombinedOutput(); err != nil {
return errors.Wrapf(err, "pulling %q: %s", img, out)
}
}
return nil
}

// normalizeImageRef mirrors containerd's docker-reference normalization so
// differently spelled refs to the same image compare equal: bare names gain
// docker.io/library/, hub-style names gain docker.io/, and refs without a
// tag get :latest. localhost/ and dotted-registry refs stay as given.
func normalizeImageRef(image string) string {
if strings.LastIndex(image, ":") < strings.LastIndex(image, "/") || !strings.Contains(image, ":") {
image += ":latest"
}
first, _, found := strings.Cut(image, "/")
switch {
case !found:
return "docker.io/library/" + image
case first != "localhost" && !strings.ContainsAny(first, ".:"):
return "docker.io/" + image
}
return image
}

func (c *Cluster) DeleteNode(name string) error {
c.mu.Lock()
defer c.mu.Unlock()
Expand Down
34 changes: 34 additions & 0 deletions pkg/k3d/k3d_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package k3d

import (
"fmt"
"os/exec"
"sync"
"testing"

Expand Down Expand Up @@ -59,6 +60,23 @@ func TestIntegrationMultiInstance(t *testing.T) {
assert.NoError(t, errors.Join(errs...))
}

func TestIntegrationImportRepullsMissingImage(t *testing.T) {
testutil.SkipIfNotIntegration(t)

// Unused by any other test, so removing it from the host can't break a
// concurrent import.
const image = "busybox:1.37.0"

name := "import-repull"
cluster, err := GetOrCreate(name, WithAgents(0))
t.Cleanup(func() { forceCleanup(name) })
require.NoError(t, err)

// k3d alone fails this with "no valid images specified".
_ = exec.Command("docker", "image", "rm", "--force", image).Run()
require.NoError(t, cluster.ImportImage(image))
}

func TestClusterCreateArgs(t *testing.T) {
fastDetectionArgs := []string{
`--kube-controller-manager-arg=node-monitor-grace-period=10s@server:*`,
Expand All @@ -82,3 +100,19 @@ func TestClusterCreateArgs(t *testing.T) {
}
})
}

func TestNormalizeImageRef(t *testing.T) {
for given, want := range map[string]string{
"localhost/redpanda-operator:dev": "localhost/redpanda-operator:dev",
"redpandadata/redpanda:v25.2.1": "docker.io/redpandadata/redpanda:v25.2.1",
"rancher/mirrored-library-busybox:1.36.1": "docker.io/rancher/mirrored-library-busybox:1.36.1",
"busybox:1.36.1": "docker.io/library/busybox:1.36.1",
"busybox": "docker.io/library/busybox:latest",
"redpandadata/redpanda": "docker.io/redpandadata/redpanda:latest",
"quay.io/jetstack/cert-manager-controller:v1.17": "quay.io/jetstack/cert-manager-controller:v1.17",
"ghcr.io/loft-sh/vcluster-pro:4.4.0": "ghcr.io/loft-sh/vcluster-pro:4.4.0",
"registry:5000/img": "registry:5000/img:latest",
} {
require.Equal(t, want, normalizeImageRef(given), "input %q", given)
}
}
Loading