Skip to content
Open
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
21 changes: 21 additions & 0 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1841,6 +1841,27 @@ func (p *DockerProvider) PullImage(ctx context.Context, img string) error {
return p.attemptToPullImage(ctx, img, image.PullOptions{})
}

// PullImage pulls image from registry, passing options to the provider
func (p *DockerProvider) PullImageWithOpts(ctx context.Context, img string, opts ...PullImageOption) error {
pullOpts := pullImageOptions{}

for _, opt := range opts {
if err := opt(&pullOpts); err != nil {
return fmt.Errorf("applying pull image option: %w", err)
}
}

return p.attemptToPullImage(ctx, img, pullOpts.dockerPullOpts)
}
Comment on lines +1844 to +1855
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix documentation comment.

Line 1844 comment says "PullImage" but the method name is "PullImageWithOpts".

Apply this diff:

-// PullImage pulls image from registry, passing options to the provider
+// PullImageWithOpts pulls image from registry, passing options to the provider
 func (p *DockerProvider) PullImageWithOpts(ctx context.Context, img string, opts ...PullImageOption) error {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// PullImage pulls image from registry, passing options to the provider
func (p *DockerProvider) PullImageWithOpts(ctx context.Context, img string, opts ...PullImageOption) error {
pullOpts := pullImageOptions{}
for _, opt := range opts {
if err := opt(&pullOpts); err != nil {
return fmt.Errorf("applying pull image option: %w", err)
}
}
return p.attemptToPullImage(ctx, img, pullOpts.dockerPullOpts)
}
// PullImageWithOpts pulls image from registry, passing options to the provider
func (p *DockerProvider) PullImageWithOpts(ctx context.Context, img string, opts ...PullImageOption) error {
pullOpts := pullImageOptions{}
for _, opt := range opts {
if err := opt(&pullOpts); err != nil {
return fmt.Errorf("applying pull image option: %w", err)
}
}
return p.attemptToPullImage(ctx, img, pullOpts.dockerPullOpts)
}
🤖 Prompt for AI Agents
In docker.go around lines 1844 to 1855, the top-of-function comment incorrectly
reads "PullImage" while the function is named PullImageWithOpts; update the
documentation comment to match the function name (e.g., "PullImageWithOpts pulls
an image from the registry, passing options to the provider") and ensure the
comment uses the Go doc convention (starts with the exact function name).


func PullDockerImageWithPlatform(platform specs.Platform) PullImageOption {
return func(opts *pullImageOptions) error {
opts.dockerPullOpts.Platform = platforms.Format(platform)

return nil
}
}

var permanentClientErrors = []func(error) bool{
errdefs.IsNotFound,
errdefs.IsInvalidArgument,
Expand Down
7 changes: 7 additions & 0 deletions image.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package testcontainers
import (
"context"

"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
)

Expand All @@ -18,6 +19,12 @@ type saveImageOptions struct {

type SaveImageOption func(*saveImageOptions) error

type pullImageOptions struct {
dockerPullOpts image.PullOptions
}

type PullImageOption func(*pullImageOptions) error

// ImageProvider allows manipulating images
type ImageProvider interface {
ListImages(context.Context) ([]ImageInfo, error)
Expand Down
4 changes: 2 additions & 2 deletions modules/k3s/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ go 1.24.0
toolchain go1.24.7

require (
github.com/containerd/platforms v0.2.1
github.com/docker/docker v28.3.3+incompatible
github.com/docker/go-connections v0.6.0
github.com/opencontainers/image-spec v1.1.1
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.39.0
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -23,7 +25,6 @@ require (
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
Expand Down Expand Up @@ -61,7 +62,6 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
Expand Down
64 changes: 62 additions & 2 deletions modules/k3s/k3s.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"os"
"path/filepath"

"github.com/containerd/platforms"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/go-connections/nat"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"gopkg.in/yaml.v3"

"github.com/testcontainers/testcontainers-go"
Expand Down Expand Up @@ -179,10 +181,12 @@ func unmarshal(bytes []byte) (*KubeConfigValue, error) {
return &kubeConfig, nil
}

// LoadImages imports local images into the cluster using containerd
func (c *K3sContainer) LoadImages(ctx context.Context, images ...string) error {
return c.LoadImagesWithOpts(ctx, images)
return c.LoadImagesWithPlatform(ctx, images, nil)
}

// Deprecated: use LoadImagesWithPlatform instead
func (c *K3sContainer) LoadImagesWithOpts(ctx context.Context, images []string, opts ...testcontainers.SaveImageOption) error {
provider, err := testcontainers.ProviderDocker.GetProvider()
if err != nil {
Expand All @@ -209,10 +213,66 @@ func (c *K3sContainer) LoadImagesWithOpts(ctx context.Context, images []string,
return fmt.Errorf("copying image to container %w", err)
}

_, _, err = c.Exec(ctx, []string{"ctr", "-n=k8s.io", "images", "import", "--all-platforms", containerPath})
exit, reader, err := c.Exec(ctx, []string{"ctr", "-n=k8s.io", "images", "import", "--all-platforms", containerPath})
if err != nil {
return fmt.Errorf("importing image %w", err)
}
if exit != 0 {
b, _ := io.ReadAll(reader)
return fmt.Errorf("importing image %s", string(b))
}

return nil
}

// LoadImagesWithPlatform imports local images into the cluster using containerd for a specific platform
func (c *K3sContainer) LoadImagesWithPlatform(ctx context.Context, images []string, platform *v1.Platform) error {
provider, err := testcontainers.ProviderDocker.GetProvider()
if err != nil {
return fmt.Errorf("getting docker provider %w", err)
}

opts := []testcontainers.SaveImageOption{}
if platform != nil {
opts = append(opts, testcontainers.SaveDockerImageWithPlatforms(*platform))
}

// save image
imagesTar, err := os.CreateTemp(os.TempDir(), "images*.tar")
if err != nil {
return fmt.Errorf("creating temporary images file %w", err)
}
defer func() {
_ = os.Remove(imagesTar.Name())
}()

err = provider.SaveImagesWithOpts(context.Background(), imagesTar.Name(), images, opts...)
if err != nil {
return fmt.Errorf("saving images %w", err)
}

containerPath := "/tmp/" + filepath.Base(imagesTar.Name())
err = c.CopyFileToContainer(ctx, imagesTar.Name(), containerPath, 0x644)
if err != nil {
return fmt.Errorf("copying image to container %w", err)
}

cmd := []string{"ctr", "-n=k8s.io", "images", "import"}

if platform != nil {
cmd = append(cmd, "--platform", platforms.Format(*platform))
}

cmd = append(cmd, containerPath)

exit, reader, err := c.Exec(ctx, cmd)
if err != nil {
return fmt.Errorf("importing image %w", err)
}
if exit != 0 {
b, _ := io.ReadAll(reader)
return fmt.Errorf("importing image %s", string(b))
}

return nil
}
122 changes: 120 additions & 2 deletions modules/k3s/k3s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

"github.com/containerd/platforms"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -39,17 +40,134 @@ func Test_LoadImages(t *testing.T) {
provider, err := testcontainers.ProviderDocker.GetProvider()
require.NoError(t, err)

dockerProvider, _ := provider.(*testcontainers.DockerProvider)

// This function only works for single architecture images
// Forces the test to use a single-arch version of the image
arch := platforms.DefaultSpec().Architecture
if platforms.DefaultSpec().Variant != "" {
arch += platforms.DefaultSpec().Variant
}
nginxImg := arch + "/nginx"

// ensure nginx image is available locally
err = provider.PullImage(ctx, "nginx")
err = provider.PullImage(ctx, nginxImg)
require.NoError(t, err)

t.Run("Test load image not available", func(t *testing.T) {
err := k3sContainer.LoadImages(ctx, "fake.registry/fake:non-existing")
require.Error(t, err)
})

t.Run("Test load image with wrong architecture", func(t *testing.T) {
p, _ := platforms.Parse("linux/s390x")
img := "nginx:mainline"
err = dockerProvider.PullImageWithOpts(ctx, img, testcontainers.PullDockerImageWithPlatform(p))
require.NoError(t, err)

err := k3sContainer.LoadImages(ctx, img)
require.Error(t, err)
require.Regexp(t, "content digest .* not found", err)
})

t.Run("Test load image in cluster", func(t *testing.T) {
err := k3sContainer.LoadImages(ctx, nginxImg)
require.NoError(t, err)

pod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: nginxImg,
ImagePullPolicy: corev1.PullNever, // use image only if already present
},
},
},
}

_, err = k8s.CoreV1().Pods("default").Create(ctx, pod, metav1.CreateOptions{})
require.NoError(t, err)

err = kwait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) {
state, err := getTestPodState(ctx, k8s)
if err != nil {
return false, err
}
if state.Terminated != nil {
return false, fmt.Errorf("pod terminated: %v", state.Terminated)
}
return state.Running != nil, nil
})
require.NoError(t, err)

state, err := getTestPodState(ctx, k8s)
require.NoError(t, err)
require.NotNil(t, state.Running)
})
}

func Test_LoadImagesWithPlatform(t *testing.T) {
// Give up to three minutes to run this test
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Minute))
defer cancel()

k3sContainer, err := k3s.Run(ctx, "rancher/k3s:v1.27.1-k3s1")
testcontainers.CleanupContainer(t, k3sContainer)
require.NoError(t, err)

kubeConfigYaml, err := k3sContainer.GetKubeConfig(ctx)
require.NoError(t, err)

restcfg, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
require.NoError(t, err)

k8s, err := kubernetes.NewForConfig(restcfg)
require.NoError(t, err)

provider, err := testcontainers.ProviderDocker.GetProvider()
require.NoError(t, err)

dockerProvider, _ := provider.(*testcontainers.DockerProvider)

// ensure nginx image is available locally
err = provider.PullImage(ctx, "nginx")
require.NoError(t, err)

t.Run("Test load image not available", func(t *testing.T) {
p, _ := platforms.Parse("linux/amd64")
err := k3sContainer.LoadImagesWithPlatform(ctx, []string{"fake.registry/fake:non-existing"}, &p)
require.Error(t, err)
})

t.Run("Test load image with wrong architecture", func(t *testing.T) {
pullPlatform, _ := platforms.Parse("linux/s390x")
img := "nginx:mainline"
err = dockerProvider.PullImageWithOpts(ctx, img, testcontainers.PullDockerImageWithPlatform(pullPlatform))
require.NoError(t, err)

loadPlatform, _ := platforms.Parse("linux/amd64")
err := k3sContainer.LoadImagesWithPlatform(ctx, []string{img}, &loadPlatform)
require.Error(t, err)
expected := fmt.Sprintf(
"image with reference %s was found but does not provide the specified platform (%s)",
img,
platforms.Format(loadPlatform),
)
require.Contains(t, err.Error(), expected)
})

t.Run("Test load image in cluster", func(t *testing.T) {
err := k3sContainer.LoadImages(ctx, "nginx")
p := platforms.DefaultSpec()
p.OS = "linux"
err := k3sContainer.LoadImagesWithPlatform(ctx, []string{"nginx"}, &p)
require.NoError(t, err)

pod := &corev1.Pod{
Expand Down
Loading