Skip to content

Commit 15e7e07

Browse files
fix(k3s): don't attempt to load all platforms when using LoadImages
deprecate LoadImagesWithOpts with a new LoadImagesWithPlatform
1 parent 89d09dc commit 15e7e07

File tree

5 files changed

+205
-5
lines changed

5 files changed

+205
-5
lines changed

docker.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1841,6 +1841,27 @@ func (p *DockerProvider) PullImage(ctx context.Context, img string) error {
18411841
return p.attemptToPullImage(ctx, img, image.PullOptions{})
18421842
}
18431843

1844+
// PullImage pulls image from registry, passing options to the provider
1845+
func (p *DockerProvider) PullImageWithOpts(ctx context.Context, img string, opts ...PullImageOption) error {
1846+
pullOpts := pullImageOptions{}
1847+
1848+
for _, opt := range opts {
1849+
if err := opt(&pullOpts); err != nil {
1850+
return fmt.Errorf("applying save image option: %w", err)
1851+
}
1852+
}
1853+
1854+
return p.attemptToPullImage(ctx, img, pullOpts.dockerPullOpts)
1855+
}
1856+
1857+
func PullDockerImageWithPlatform(platform specs.Platform) PullImageOption {
1858+
return func(opts *pullImageOptions) error {
1859+
opts.dockerPullOpts.Platform = platforms.Format(platform)
1860+
1861+
return nil
1862+
}
1863+
}
1864+
18441865
var permanentClientErrors = []func(error) bool{
18451866
errdefs.IsNotFound,
18461867
errdefs.IsInvalidArgument,

image.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package testcontainers
33
import (
44
"context"
55

6+
"github.com/docker/docker/api/types/image"
67
"github.com/docker/docker/client"
78
)
89

@@ -18,10 +19,17 @@ type saveImageOptions struct {
1819

1920
type SaveImageOption func(*saveImageOptions) error
2021

22+
type pullImageOptions struct {
23+
dockerPullOpts image.PullOptions
24+
}
25+
26+
type PullImageOption func(*pullImageOptions) error
27+
2128
// ImageProvider allows manipulating images
2229
type ImageProvider interface {
2330
ListImages(context.Context) ([]ImageInfo, error)
2431
SaveImages(context.Context, string, ...string) error
2532
SaveImagesWithOpts(context.Context, string, []string, ...SaveImageOption) error
2633
PullImage(context.Context, string) error
34+
PullImageWithOpts(context.Context, string, ...PullImageOption) error
2735
}

modules/k3s/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ go 1.24.0
55
toolchain go1.24.7
66

77
require (
8+
github.com/containerd/platforms v0.2.1
89
github.com/docker/docker v28.3.3+incompatible
910
github.com/docker/go-connections v0.6.0
11+
github.com/opencontainers/image-spec v1.1.1
1012
github.com/stretchr/testify v1.11.1
1113
github.com/testcontainers/testcontainers-go v0.39.0
1214
gopkg.in/yaml.v3 v3.0.1
@@ -23,7 +25,6 @@ require (
2325
github.com/containerd/errdefs v1.0.0 // indirect
2426
github.com/containerd/errdefs/pkg v0.3.0 // indirect
2527
github.com/containerd/log v0.1.0 // indirect
26-
github.com/containerd/platforms v0.2.1 // indirect
2728
github.com/cpuguy83/dockercfg v0.3.2 // indirect
2829
github.com/davecgh/go-spew v1.1.1 // indirect
2930
github.com/distribution/reference v0.6.0 // indirect
@@ -61,7 +62,6 @@ require (
6162
github.com/morikuni/aec v1.0.0 // indirect
6263
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
6364
github.com/opencontainers/go-digest v1.0.0 // indirect
64-
github.com/opencontainers/image-spec v1.1.1 // indirect
6565
github.com/pkg/errors v0.9.1 // indirect
6666
github.com/pmezard/go-difflib v1.0.0 // indirect
6767
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect

modules/k3s/k3s.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import (
77
"os"
88
"path/filepath"
99

10+
"github.com/containerd/platforms"
1011
"github.com/docker/docker/api/types/container"
1112
"github.com/docker/docker/api/types/mount"
1213
"github.com/docker/go-connections/nat"
14+
v1 "github.com/opencontainers/image-spec/specs-go/v1"
1315
"gopkg.in/yaml.v3"
1416

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

184+
// LoadImages imports local images into the cluster using containerd
182185
func (c *K3sContainer) LoadImages(ctx context.Context, images ...string) error {
183-
return c.LoadImagesWithOpts(ctx, images)
186+
return c.LoadImagesWithPlatform(ctx, images, nil)
184187
}
185188

189+
// Deprecated: use LoadImagesWithPlatform instead
186190
func (c *K3sContainer) LoadImagesWithOpts(ctx context.Context, images []string, opts ...testcontainers.SaveImageOption) error {
187191
provider, err := testcontainers.ProviderDocker.GetProvider()
188192
if err != nil {
@@ -220,3 +224,55 @@ func (c *K3sContainer) LoadImagesWithOpts(ctx context.Context, images []string,
220224

221225
return nil
222226
}
227+
228+
// LoadImagesWithPlatform imports local images into the cluster using containerd for a specific platform
229+
func (c *K3sContainer) LoadImagesWithPlatform(ctx context.Context, images []string, platform *v1.Platform) error {
230+
provider, err := testcontainers.ProviderDocker.GetProvider()
231+
if err != nil {
232+
return fmt.Errorf("getting docker provider %w", err)
233+
}
234+
235+
opts := []testcontainers.SaveImageOption{}
236+
if platform != nil {
237+
opts = append(opts, testcontainers.SaveDockerImageWithPlatforms(*platform))
238+
}
239+
240+
// save image
241+
imagesTar, err := os.CreateTemp(os.TempDir(), "images*.tar")
242+
if err != nil {
243+
return fmt.Errorf("creating temporary images file %w", err)
244+
}
245+
defer func() {
246+
_ = os.Remove(imagesTar.Name())
247+
}()
248+
249+
err = provider.SaveImagesWithOpts(context.Background(), imagesTar.Name(), images, opts...)
250+
if err != nil {
251+
return fmt.Errorf("saving images %w", err)
252+
}
253+
254+
containerPath := "/tmp/" + filepath.Base(imagesTar.Name())
255+
err = c.CopyFileToContainer(ctx, imagesTar.Name(), containerPath, 0x644)
256+
if err != nil {
257+
return fmt.Errorf("copying image to container %w", err)
258+
}
259+
260+
cmd := []string{"ctr", "-n=k8s.io", "images", "import"}
261+
262+
if platform != nil {
263+
cmd = append(cmd, "--platform", platforms.Format(*platform))
264+
}
265+
266+
cmd = append(cmd, containerPath)
267+
268+
exit, reader, err := c.Exec(ctx, cmd)
269+
if err != nil {
270+
return fmt.Errorf("importing image %w", err)
271+
}
272+
if exit != 0 {
273+
b, _ := io.ReadAll(reader)
274+
return fmt.Errorf("importing image %s", string(b))
275+
}
276+
277+
return nil
278+
}

modules/k3s/k3s_test.go

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package k3s_test
33
import (
44
"context"
55
"fmt"
6+
"regexp"
67
"testing"
78
"time"
89

10+
"github.com/containerd/platforms"
911
"github.com/stretchr/testify/require"
1012
corev1 "k8s.io/api/core/v1"
1113
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -39,17 +41,130 @@ func Test_LoadImages(t *testing.T) {
3941
provider, err := testcontainers.ProviderDocker.GetProvider()
4042
require.NoError(t, err)
4143

44+
// This function only works for single architecture images
45+
// Forces the test to use a single-arch version of the image
46+
arch := platforms.DefaultSpec().Architecture
47+
if platforms.DefaultSpec().Variant != "" {
48+
arch += platforms.DefaultSpec().Variant
49+
}
50+
nginxImg := arch + "/nginx"
51+
4252
// ensure nginx image is available locally
43-
err = provider.PullImage(ctx, "nginx")
53+
err = provider.PullImage(ctx, nginxImg)
4454
require.NoError(t, err)
4555

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

61+
t.Run("Test load image with wrong architecture", func(t *testing.T) {
62+
p, _ := platforms.Parse("linux/s390x")
63+
img := "nginx:mainline"
64+
err = provider.PullImageWithOpts(ctx, img, testcontainers.PullDockerImageWithPlatform(p))
65+
require.NoError(t, err)
66+
67+
err := k3sContainer.LoadImages(ctx, img)
68+
require.Error(t, err)
69+
require.Regexp(t, "content digest .* not found", err)
70+
})
71+
72+
t.Run("Test load image in cluster", func(t *testing.T) {
73+
err := k3sContainer.LoadImages(ctx, nginxImg)
74+
require.NoError(t, err)
75+
76+
pod := &corev1.Pod{
77+
TypeMeta: metav1.TypeMeta{
78+
Kind: "Pod",
79+
APIVersion: "v1",
80+
},
81+
ObjectMeta: metav1.ObjectMeta{
82+
Name: "test-pod",
83+
},
84+
Spec: corev1.PodSpec{
85+
Containers: []corev1.Container{
86+
{
87+
Name: "nginx",
88+
Image: nginxImg,
89+
ImagePullPolicy: corev1.PullNever, // use image only if already present
90+
},
91+
},
92+
},
93+
}
94+
95+
_, err = k8s.CoreV1().Pods("default").Create(ctx, pod, metav1.CreateOptions{})
96+
require.NoError(t, err)
97+
98+
err = kwait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) {
99+
state, err := getTestPodState(ctx, k8s)
100+
if err != nil {
101+
return false, err
102+
}
103+
if state.Terminated != nil {
104+
return false, fmt.Errorf("pod terminated: %v", state.Terminated)
105+
}
106+
return state.Running != nil, nil
107+
})
108+
require.NoError(t, err)
109+
110+
state, err := getTestPodState(ctx, k8s)
111+
require.NoError(t, err)
112+
require.NotNil(t, state.Running)
113+
})
114+
}
115+
116+
func Test_LoadImagesWithPlatform(t *testing.T) {
117+
// Give up to three minutes to run this test
118+
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Minute))
119+
defer cancel()
120+
121+
k3sContainer, err := k3s.Run(ctx, "rancher/k3s:v1.27.1-k3s1")
122+
testcontainers.CleanupContainer(t, k3sContainer)
123+
require.NoError(t, err)
124+
125+
kubeConfigYaml, err := k3sContainer.GetKubeConfig(ctx)
126+
require.NoError(t, err)
127+
128+
restcfg, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
129+
require.NoError(t, err)
130+
131+
k8s, err := kubernetes.NewForConfig(restcfg)
132+
require.NoError(t, err)
133+
134+
provider, err := testcontainers.ProviderDocker.GetProvider()
135+
require.NoError(t, err)
136+
137+
// ensure nginx image is available locally
138+
err = provider.PullImage(ctx, "nginx")
139+
require.NoError(t, err)
140+
141+
t.Run("Test load image not available", func(t *testing.T) {
142+
p, _ := platforms.Parse("linux/amd64")
143+
err := k3sContainer.LoadImagesWithPlatform(ctx, []string{"fake.registry/fake:non-existing"}, &p)
144+
require.Error(t, err)
145+
})
146+
147+
t.Run("Test load image with wrong architecture", func(t *testing.T) {
148+
pullPlatform, _ := platforms.Parse("linux/s390x")
149+
img := "nginx:mainline"
150+
err = provider.PullImageWithOpts(ctx, img, testcontainers.PullDockerImageWithPlatform(pullPlatform))
151+
require.NoError(t, err)
152+
153+
loadPlatform, _ := platforms.Parse("linux/amd64")
154+
err := k3sContainer.LoadImagesWithPlatform(ctx, []string{img}, &loadPlatform)
155+
require.Error(t, err)
156+
expected := fmt.Sprintf(
157+
"image with reference %s was found but does not provide the specified platform (%s)",
158+
img,
159+
platforms.Format(loadPlatform),
160+
)
161+
require.Contains(t, err.Error(), expected)
162+
})
163+
51164
t.Run("Test load image in cluster", func(t *testing.T) {
52-
err := k3sContainer.LoadImages(ctx, "nginx")
165+
p := platforms.DefaultSpec()
166+
p.OS = "linux"
167+
err := k3sContainer.LoadImagesWithPlatform(ctx, []string{"nginx"}, &p)
53168
require.NoError(t, err)
54169

55170
pod := &corev1.Pod{

0 commit comments

Comments
 (0)