From 565a142ed301085d76b73706de98bf67838fa923 Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Sun, 17 Sep 2023 18:11:02 +0200 Subject: [PATCH 01/12] Add image helpers Signed-off-by: Pablo Chacin --- docker.go | 51 +++++++++++++++++++++++++++ docker_test.go | 1 + generic.go | 1 + image.go | 18 ++++++++++ image_test.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+) create mode 100644 image.go create mode 100644 image_test.go diff --git a/docker.go b/docker.go index 4eefed529b..6cedda769e 100644 --- a/docker.go +++ b/docker.go @@ -1468,3 +1468,54 @@ func containerFromDockerResponse(ctx context.Context, response types.Container) return &container, nil } + +// ListImages list images from the provider. If an image has multiple Tags, each tag is reported +// individually with the same ID and same labels +func (p *DockerProvider)ListImages(ctx context.Context) ([]ImageInfo, error) { + images := []ImageInfo{} + + imageList, err := p.client.ImageList(ctx, types.ImageListOptions{}) + if err != nil { + return images, fmt.Errorf("listing images %w", err) + } + + for _, img := range imageList { + for _, tag := range img.RepoTags { + images = append(images, ImageInfo{ID: img.ID, Name: tag}) + } + } + + return images, nil +} + + +// SaveImages exports a list of images as an uncompressed tar +func (p *DockerProvider)SaveImages(ctx context.Context, output string, images...string) error { + outputFile, err := os.Create(output) + if err != nil { + return fmt.Errorf("opening output file %w", err) + } + defer func(){ + _ = outputFile.Close() + }() + + imageReader, err := p.client.ImageSave(ctx, images) + if err != nil { + return fmt.Errorf("saving images %w", err) + } + defer func(){ + _ = imageReader.Close() + }() + + _, err = io.Copy(outputFile, imageReader) + if err != nil { + return fmt.Errorf("writing images to output %w", err) + } + + return nil +} + +// PullImage pulls image from registry +func (p *DockerProvider)PullImage(ctx context.Context, image string) error { + return p.attemptToPullImage(ctx, image, types.ImagePullOptions{}) +} diff --git a/docker_test.go b/docker_test.go index 8d1a31e227..d58d24c8b7 100644 --- a/docker_test.go +++ b/docker_test.go @@ -2080,3 +2080,4 @@ func TestDockerProviderFindContainerByName(t *testing.T) { require.NotNil(t, c) assert.Contains(t, c.Names, c1Name) } + diff --git a/generic.go b/generic.go index 85b1542c7d..7d0cd2ad5f 100644 --- a/generic.go +++ b/generic.go @@ -156,4 +156,5 @@ func GenericContainer(ctx context.Context, req GenericContainerRequest) (Contain type GenericProvider interface { ContainerProvider NetworkProvider + ImageProvider } diff --git a/image.go b/image.go new file mode 100644 index 0000000000..7917ee518d --- /dev/null +++ b/image.go @@ -0,0 +1,18 @@ +package testcontainers + +import ( + "context" +) + +// ImageInfo represents a summary information of an image +type ImageInfo struct { + ID string + Name string +} + +// ImageProvider allows manipulating images +type ImageProvider interface { + ListImages(context.Context) ([]ImageInfo, error) + SaveImages(context.Context, string, ...string) error + PullImage(context.Context, string) error +} diff --git a/image_test.go b/image_test.go new file mode 100644 index 0000000000..04ebfa938d --- /dev/null +++ b/image_test.go @@ -0,0 +1,95 @@ +package testcontainers + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/testcontainers/testcontainers-go/internal/testcontainersdocker" +) + +func TestImageList(t *testing.T) { + t.Setenv("DOCKER_HOST", testcontainersdocker.ExtractDockerHost(context.Background())) + + provider, err := ProviderDocker.GetProvider() + if err != nil { + t.Fatalf("failed to get provider %v", err) + } + + defer func() { + _ = provider.Close() + }() + + req := ContainerRequest{ + Image: "redis:latest", + } + + container, err := provider.CreateContainer(context.Background(), req) + if err != nil { + t.Fatalf("creating test container %v", err) + } + + defer func() { + _ = container.Terminate(context.Background()) + }() + + images, err := provider.ListImages(context.Background()) + if err != nil { + t.Fatalf("listing images %v", err) + } + + if len(images) == 0 { + t.Fatal("no images retrieved") + } + + // look if the list contains the container image + for _, img := range images { + if img.Name == req.Image { + return + } + } + + t.Fatalf("expected image not found: %s", req.Image) +} + +func TestSaveImages(t *testing.T) { + t.Setenv("DOCKER_HOST", testcontainersdocker.ExtractDockerHost(context.Background())) + + provider, err := ProviderDocker.GetProvider() + if err != nil { + t.Fatalf("failed to get provider %v", err) + } + + defer func() { + _ = provider.Close() + }() + + req := ContainerRequest{ + Image: "redis:latest", + } + + container, err := provider.CreateContainer(context.Background(), req) + if err != nil { + t.Fatalf("creating test container %v", err) + } + + defer func() { + _ = container.Terminate(context.Background()) + }() + + output := filepath.Join(t.TempDir(), "images.tar") + err = provider.SaveImages(context.Background(), output, req.Image) + if err != nil { + t.Fatalf("saving image %q: %v", req.Image, err) + } + + info, err := os.Stat(output) + if err != nil { + t.Fatal(err) + } + + if info.Size() == 0 { + t.Fatalf("output file is empty") + } +} From 5d76f6a3c353bb8329d71d91d64ba13df5a99ea8 Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Sun, 17 Sep 2023 18:12:20 +0200 Subject: [PATCH 02/12] Add LoadImages function Signed-off-by: Pablo Chacin --- modules/k3s/k3s.go | 19 ++++++ modules/k3s/k3s_image_test.go | 109 ++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 modules/k3s/k3s_image_test.go diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index ad8fa2866c..2fb20fb5e5 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "path" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" @@ -163,3 +164,21 @@ func unmarshal(bytes []byte) (*KubeConfigValue, error) { } return &kubeConfig, nil } + +// LoadImages loads images into the k3s container. +func (c *K3sContainer) LoadImages(ctx context.Context, images string) error { + imageFile := path.Base(images) + containerPath := fmt.Sprintf("/tmp/%s", imageFile) + + err := c.Container.CopyFileToContainer(ctx, images, containerPath, 0x644) + if err != nil { + return fmt.Errorf("copying image to container %w", err) + } + + _, _, err = c.Container.Exec(ctx, []string{"ctr", "-n=k8s.io", "images", "import", containerPath}) + if err != nil { + return fmt.Errorf("importing image %w", err) + } + + return nil +} \ No newline at end of file diff --git a/modules/k3s/k3s_image_test.go b/modules/k3s/k3s_image_test.go new file mode 100644 index 0000000000..2ee21211b3 --- /dev/null +++ b/modules/k3s/k3s_image_test.go @@ -0,0 +1,109 @@ +package k3s + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +func ExampleLoadImages() { + // runK3sContainer { + ctx := context.Background() + + k3sContainer, err := RunContainer(ctx, + testcontainers.WithImage("docker.io/rancher/k3s:v1.27.1-k3s1"), + testcontainers.WithWaitStrategy(wait.ForLog(".*Node controller sync successful.*").AsRegexp()), + ) + if err != nil { + panic(err) + } + + // Clean up the container + defer func() { + if err := k3sContainer.Terminate(ctx); err != nil { + panic(err) + } + }() + // } + + kubeConfigYaml, err := k3sContainer.GetKubeConfig(ctx) + if err != nil { + panic(err) + } + + restcfg, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml) + if err != nil { + panic(err) + } + + k8s, err := kubernetes.NewForConfig(restcfg) + if err != nil { + panic(err) + } + + provider, err := testcontainers.ProviderDocker.GetProvider() + if err != nil { + panic(err) + } + + err = provider.PullImage(context.Background(), "nginx") + if err != nil { + panic(err) + } + + output := filepath.Join(os.TempDir(), "nginx.tar") + err = provider.SaveImages(context.Background(), output, "nginx") + if err != nil { + panic(err) + } + + err = k3sContainer.LoadImages(context.Background(), output) + if err != nil { + panic(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: "nginx", + ImagePullPolicy: corev1.PullNever, // use image only if already present + }, + }, + }, + } + + _, err = k8s.CoreV1().Pods("default").Create(context.Background(), pod, metav1.CreateOptions{}) + if err != nil { + panic(err) + } + + time.Sleep(1 * time.Second) + pod, err = k8s.CoreV1().Pods("default").Get(context.Background(), "test-pod", metav1.GetOptions{}) + if err != nil { + panic(err) + } + + if pod.Status.ContainerStatuses[0].State.Waiting.Reason == "ErrImageNeverPull" { + panic(fmt.Errorf("Image was not loaded")) + } +} From c073dd2bdeb2e12448b60eee93969ec5b02bef91 Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Mon, 18 Sep 2023 14:19:47 +0200 Subject: [PATCH 03/12] Document k3s LoadImages method Signed-off-by: Pablo Chacin --- docs/modules/k3s.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/modules/k3s.md b/docs/modules/k3s.md index 94c73ee4a5..d34edfdc11 100644 --- a/docs/modules/k3s.md +++ b/docs/modules/k3s.md @@ -79,3 +79,19 @@ to the Kubernetes Rest Client API using a Kubernetes client. It'll be returned i [Get KubeConifg](../../modules/k3s/k3s_test.go) inside_block:GetKubeConfig + +#### LoadImages + +The `LoadImages` method loads images from a `.tar` file into the kubernetes cluster and makes them available to pods. This is useful for testing images generated locally without having to push them to a docker registry. + +The `.tar` file can be created, for example, using the [docker save](https://docs.docker.com/engine/reference/commandline/save/) command: + +```sh +docker save -o images.tar ... +``` + +Also, the [DockerProvider](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#DockerProvider) offers methods for pulling and saving images, which can be used from the test code, as shown in the following example: + + +[Load Images](../../modules/k3s/k3s_image_test.go) inside_block:LoadImages + \ No newline at end of file From aa2defbee70219e6c7b57be2d9606d4f4f1bb4cd Mon Sep 17 00:00:00 2001 From: pablochacin Date: Tue, 19 Sep 2023 09:21:27 +0200 Subject: [PATCH 04/12] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel de la Peña --- modules/k3s/k3s.go | 2 +- modules/k3s/k3s_image_test.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index 2fb20fb5e5..03e98412b7 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -167,7 +167,7 @@ func unmarshal(bytes []byte) (*KubeConfigValue, error) { // LoadImages loads images into the k3s container. func (c *K3sContainer) LoadImages(ctx context.Context, images string) error { - imageFile := path.Base(images) + imageFile := filepath.Base(images) containerPath := fmt.Sprintf("/tmp/%s", imageFile) err := c.Container.CopyFileToContainer(ctx, images, containerPath, 0x644) diff --git a/modules/k3s/k3s_image_test.go b/modules/k3s/k3s_image_test.go index 2ee21211b3..82c92a3741 100644 --- a/modules/k3s/k3s_image_test.go +++ b/modules/k3s/k3s_image_test.go @@ -18,7 +18,6 @@ import ( ) func ExampleLoadImages() { - // runK3sContainer { ctx := context.Background() k3sContainer, err := RunContainer(ctx, @@ -35,7 +34,6 @@ func ExampleLoadImages() { panic(err) } }() - // } kubeConfigYaml, err := k3sContainer.GetKubeConfig(ctx) if err != nil { From 13bb4c4e6aaa1422ccbac3e563bb37c0acee141c Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Tue, 19 Sep 2023 13:58:25 +0200 Subject: [PATCH 05/12] Fix filepath import Signed-off-by: Pablo Chacin --- modules/k3s/k3s.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index 03e98412b7..438ee27477 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "io" - "path" + "path/filepath" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" From f0791ac39203b0f076b427afa8031cb50c807451 Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Tue, 19 Sep 2023 13:59:03 +0200 Subject: [PATCH 06/12] Fix runnable example package Signed-off-by: Pablo Chacin --- modules/k3s/k3s.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index 438ee27477..b54242f9cc 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -1,4 +1,4 @@ -package k3s +package k3s_test import ( "context" From 669cbb1fc1c5fd40ebf1d4b685a76efc3aac9812 Mon Sep 17 00:00:00 2001 From: pablochacin Date: Tue, 19 Sep 2023 16:07:07 +0200 Subject: [PATCH 07/12] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel de la Peña --- modules/k3s/k3s.go | 2 +- modules/k3s/k3s_image_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index b54242f9cc..438ee27477 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -1,4 +1,4 @@ -package k3s_test +package k3s import ( "context" diff --git a/modules/k3s/k3s_image_test.go b/modules/k3s/k3s_image_test.go index 82c92a3741..a7639ac0a1 100644 --- a/modules/k3s/k3s_image_test.go +++ b/modules/k3s/k3s_image_test.go @@ -1,4 +1,4 @@ -package k3s +package k3s_test import ( "context" From 2305e798f1c3c8377713079ee2628178a07e7e88 Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Tue, 19 Sep 2023 17:08:04 +0200 Subject: [PATCH 08/12] Fix linter isues Signed-off-by: Pablo Chacin --- docker.go | 11 +++++------ docker_test.go | 1 - image.go | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docker.go b/docker.go index 6cedda769e..4737d37c9d 100644 --- a/docker.go +++ b/docker.go @@ -1471,7 +1471,7 @@ func containerFromDockerResponse(ctx context.Context, response types.Container) // ListImages list images from the provider. If an image has multiple Tags, each tag is reported // individually with the same ID and same labels -func (p *DockerProvider)ListImages(ctx context.Context) ([]ImageInfo, error) { +func (p *DockerProvider) ListImages(ctx context.Context) ([]ImageInfo, error) { images := []ImageInfo{} imageList, err := p.client.ImageList(ctx, types.ImageListOptions{}) @@ -1488,14 +1488,13 @@ func (p *DockerProvider)ListImages(ctx context.Context) ([]ImageInfo, error) { return images, nil } - // SaveImages exports a list of images as an uncompressed tar -func (p *DockerProvider)SaveImages(ctx context.Context, output string, images...string) error { +func (p *DockerProvider) SaveImages(ctx context.Context, output string, images ...string) error { outputFile, err := os.Create(output) if err != nil { return fmt.Errorf("opening output file %w", err) } - defer func(){ + defer func() { _ = outputFile.Close() }() @@ -1503,7 +1502,7 @@ func (p *DockerProvider)SaveImages(ctx context.Context, output string, images... if err != nil { return fmt.Errorf("saving images %w", err) } - defer func(){ + defer func() { _ = imageReader.Close() }() @@ -1516,6 +1515,6 @@ func (p *DockerProvider)SaveImages(ctx context.Context, output string, images... } // PullImage pulls image from registry -func (p *DockerProvider)PullImage(ctx context.Context, image string) error { +func (p *DockerProvider) PullImage(ctx context.Context, image string) error { return p.attemptToPullImage(ctx, image, types.ImagePullOptions{}) } diff --git a/docker_test.go b/docker_test.go index d58d24c8b7..8d1a31e227 100644 --- a/docker_test.go +++ b/docker_test.go @@ -2080,4 +2080,3 @@ func TestDockerProviderFindContainerByName(t *testing.T) { require.NotNil(t, c) assert.Contains(t, c.Names, c1Name) } - diff --git a/image.go b/image.go index 7917ee518d..f7f2bf0260 100644 --- a/image.go +++ b/image.go @@ -6,8 +6,8 @@ import ( // ImageInfo represents a summary information of an image type ImageInfo struct { - ID string - Name string + ID string + Name string } // ImageProvider allows manipulating images From e31271285399ce41c6b7a8c798017112080f139f Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Wed, 20 Sep 2023 09:28:25 +0200 Subject: [PATCH 09/12] Fix k3s import Signed-off-by: Pablo Chacin --- modules/k3s/k3s.go | 2 +- modules/k3s/k3s_image_test.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index 438ee27477..6803a2f42c 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -181,4 +181,4 @@ func (c *K3sContainer) LoadImages(ctx context.Context, images string) error { } return nil -} \ No newline at end of file +} diff --git a/modules/k3s/k3s_image_test.go b/modules/k3s/k3s_image_test.go index a7639ac0a1..eb0230bdc6 100644 --- a/modules/k3s/k3s_image_test.go +++ b/modules/k3s/k3s_image_test.go @@ -14,13 +14,15 @@ import ( "k8s.io/client-go/tools/clientcmd" "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/k3s" "github.com/testcontainers/testcontainers-go/wait" + ) func ExampleLoadImages() { ctx := context.Background() - k3sContainer, err := RunContainer(ctx, + k3sContainer, err := k3s.RunContainer(ctx, testcontainers.WithImage("docker.io/rancher/k3s:v1.27.1-k3s1"), testcontainers.WithWaitStrategy(wait.ForLog(".*Node controller sync successful.*").AsRegexp()), ) From be6534839d85fbb3f32d32c515c97107d5571d94 Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Wed, 20 Sep 2023 15:18:39 +0200 Subject: [PATCH 10/12] Simplify LoadImage API Signed-off-by: Pablo Chacin --- docs/modules/k3s.md | 16 ------ modules/k3s/k3s.go | 26 +++++++-- modules/k3s/k3s_image_test.go | 100 +++++++++++++++++----------------- 3 files changed, 72 insertions(+), 70 deletions(-) diff --git a/docs/modules/k3s.md b/docs/modules/k3s.md index d34edfdc11..94c73ee4a5 100644 --- a/docs/modules/k3s.md +++ b/docs/modules/k3s.md @@ -79,19 +79,3 @@ to the Kubernetes Rest Client API using a Kubernetes client. It'll be returned i [Get KubeConifg](../../modules/k3s/k3s_test.go) inside_block:GetKubeConfig - -#### LoadImages - -The `LoadImages` method loads images from a `.tar` file into the kubernetes cluster and makes them available to pods. This is useful for testing images generated locally without having to push them to a docker registry. - -The `.tar` file can be created, for example, using the [docker save](https://docs.docker.com/engine/reference/commandline/save/) command: - -```sh -docker save -o images.tar ... -``` - -Also, the [DockerProvider](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#DockerProvider) offers methods for pulling and saving images, which can be used from the test code, as shown in the following example: - - -[Load Images](../../modules/k3s/k3s_image_test.go) inside_block:LoadImages - \ No newline at end of file diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index 6803a2f42c..21154b951f 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "os" "path/filepath" "github.com/docker/docker/api/types/container" @@ -166,11 +167,28 @@ func unmarshal(bytes []byte) (*KubeConfigValue, error) { } // LoadImages loads images into the k3s container. -func (c *K3sContainer) LoadImages(ctx context.Context, images string) error { - imageFile := filepath.Base(images) - containerPath := fmt.Sprintf("/tmp/%s", imageFile) +func (c *K3sContainer) LoadImages(ctx context.Context, images ...string) error { + provider, err := testcontainers.ProviderDocker.GetProvider() + if err != nil { + return fmt.Errorf("getting docker provider %w", err) + } + + // 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.SaveImages(context.Background(), imagesTar.Name(), images...) + if err != nil { + return fmt.Errorf("saving images %w", err) + } - err := c.Container.CopyFileToContainer(ctx, images, containerPath, 0x644) + containerPath := fmt.Sprintf("/tmp/%s", filepath.Base(imagesTar.Name())) + err = c.Container.CopyFileToContainer(ctx, imagesTar.Name(), containerPath, 0x644) if err != nil { return fmt.Errorf("copying image to container %w", err) } diff --git a/modules/k3s/k3s_image_test.go b/modules/k3s/k3s_image_test.go index eb0230bdc6..864a8a6d82 100644 --- a/modules/k3s/k3s_image_test.go +++ b/modules/k3s/k3s_image_test.go @@ -2,24 +2,20 @@ package k3s_test import ( "context" - "fmt" - "os" - "path/filepath" + "testing" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/k3s" "github.com/testcontainers/testcontainers-go/wait" - ) -func ExampleLoadImages() { +func Test_LoadImages(t *testing.T) { ctx := context.Background() k3sContainer, err := k3s.RunContainer(ctx, @@ -27,83 +23,87 @@ func ExampleLoadImages() { testcontainers.WithWaitStrategy(wait.ForLog(".*Node controller sync successful.*").AsRegexp()), ) if err != nil { - panic(err) + t.Fatal(err) } // Clean up the container defer func() { if err := k3sContainer.Terminate(ctx); err != nil { - panic(err) + t.Fatal(err) } }() kubeConfigYaml, err := k3sContainer.GetKubeConfig(ctx) if err != nil { - panic(err) + t.Fatal(err) } restcfg, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml) if err != nil { - panic(err) + t.Fatal(err) } k8s, err := kubernetes.NewForConfig(restcfg) if err != nil { - panic(err) + t.Fatal(err) } provider, err := testcontainers.ProviderDocker.GetProvider() if err != nil { - panic(err) + t.Fatal(err) } + // ensure nginx image is available locally err = provider.PullImage(context.Background(), "nginx") if err != nil { - panic(err) + t.Fatal(err) } - output := filepath.Join(os.TempDir(), "nginx.tar") - err = provider.SaveImages(context.Background(), output, "nginx") - if err != nil { - panic(err) - } + t.Run("Test load image not available", func(t *testing.T) { + err = k3sContainer.LoadImages(context.Background(), "nginx", "fake.registry/fake:non-existing") + if err == nil { + t.Fatal("should had failed") + } + }) - err = k3sContainer.LoadImages(context.Background(), output) - if err != nil { - panic(err) - } + t.Run("Test load image in cluster", func(t *testing.T) { + err = k3sContainer.LoadImages(context.Background(), "nginx") + if err != nil { + t.Fatal(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: "nginx", - ImagePullPolicy: corev1.PullNever, // use image only if already present + 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: "nginx", + ImagePullPolicy: corev1.PullNever, // use image only if already present + }, }, }, - }, - } + } - _, err = k8s.CoreV1().Pods("default").Create(context.Background(), pod, metav1.CreateOptions{}) - if err != nil { - panic(err) - } + _, err = k8s.CoreV1().Pods("default").Create(context.Background(), pod, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } - time.Sleep(1 * time.Second) - pod, err = k8s.CoreV1().Pods("default").Get(context.Background(), "test-pod", metav1.GetOptions{}) - if err != nil { - panic(err) - } + time.Sleep(1 * time.Second) + pod, err = k8s.CoreV1().Pods("default").Get(context.Background(), "test-pod", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } - if pod.Status.ContainerStatuses[0].State.Waiting.Reason == "ErrImageNeverPull" { - panic(fmt.Errorf("Image was not loaded")) - } + if pod.Status.ContainerStatuses[0].State.Waiting.Reason == "ErrImageNeverPull" { + t.Fatal("Image was not loaded") + } + }) } From 2b0ed211de61f7ab2984fe868b6d491695baa913 Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Thu, 21 Sep 2023 11:29:48 +0200 Subject: [PATCH 11/12] Fix panin in test Signed-off-by: Pablo Chacin --- modules/k3s/k3s_image_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/k3s/k3s_image_test.go b/modules/k3s/k3s_image_test.go index 864a8a6d82..55ca0a1bd3 100644 --- a/modules/k3s/k3s_image_test.go +++ b/modules/k3s/k3s_image_test.go @@ -60,14 +60,14 @@ func Test_LoadImages(t *testing.T) { } t.Run("Test load image not available", func(t *testing.T) { - err = k3sContainer.LoadImages(context.Background(), "nginx", "fake.registry/fake:non-existing") + err := k3sContainer.LoadImages(context.Background(), "fake.registry/fake:non-existing") if err == nil { t.Fatal("should had failed") } }) t.Run("Test load image in cluster", func(t *testing.T) { - err = k3sContainer.LoadImages(context.Background(), "nginx") + err := k3sContainer.LoadImages(context.Background(), "nginx") if err != nil { t.Fatal(err) } @@ -101,8 +101,8 @@ func Test_LoadImages(t *testing.T) { if err != nil { t.Fatal(err) } - - if pod.Status.ContainerStatuses[0].State.Waiting.Reason == "ErrImageNeverPull" { + waiting := pod.Status.ContainerStatuses[0].State.Waiting + if waiting != nil && waiting.Reason == "ErrImageNeverPull" { t.Fatal("Image was not loaded") } }) From 31f6a9a64a49ce732b538a7869f4e54e4626cc1c Mon Sep 17 00:00:00 2001 From: Pablo Chacin Date: Thu, 21 Sep 2023 11:30:36 +0200 Subject: [PATCH 12/12] Re-introduce LoadImages documentation Signed-off-by: Pablo Chacin --- docs/modules/k3s.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/modules/k3s.md b/docs/modules/k3s.md index 94c73ee4a5..1c770c4e41 100644 --- a/docs/modules/k3s.md +++ b/docs/modules/k3s.md @@ -79,3 +79,11 @@ to the Kubernetes Rest Client API using a Kubernetes client. It'll be returned i [Get KubeConifg](../../modules/k3s/k3s_test.go) inside_block:GetKubeConfig + +#### LoadImages + +The `LoadImages` method loads a list of images into the kubernetes cluster and makes them available to pods. + +This is useful for testing images generated locally without having to push them to a public docker registry or having to configure `k3s` to [use a private registry](https://docs.k3s.io/installation/private-registry). + +The images must be already present in the node running the test. [DockerProvider](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#DockerProvider) offers a method for pulling images, which can be used from the test code to ensure the image is present locally before loading them to the cluster.