From c7b88cf2237c911a511452959f7e98d5360b3b17 Mon Sep 17 00:00:00 2001 From: wind57 Date: Tue, 5 May 2026 12:58:49 +0300 Subject: [PATCH 1/7] first run on github actions Signed-off-by: wind57 --- .../composites/pre-test-actions/action.yaml | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/workflows/composites/pre-test-actions/action.yaml b/.github/workflows/composites/pre-test-actions/action.yaml index 962c29c79a..51b4866e49 100644 --- a/.github/workflows/composites/pre-test-actions/action.yaml +++ b/.github/workflows/composites/pre-test-actions/action.yaml @@ -29,7 +29,9 @@ runs: with: jdk-version: ${{ inputs.jdk-version }} - - name: restore common images + # restore or download common docker test images, like: busybox, wiremock. + # these will be placed in /tmp/docker/images + - name: restore common images to /tmp/docker/images uses: ./.github/workflows/composites/restore-docker-images - name: build project @@ -37,18 +39,47 @@ runs: run: | ./mvnw clean install -Dspring-boot.build-image.skip=true -DskipITs -DskipTests -T1C -U -B -q + # we could cache these images also, but the vast majority of time, we will + # be running against a SNAPSHOT version. - name: build controllers project uses: ./.github/workflows/composites/build-controllers-project + # we could cache these images also, but the vast majority of time, we will + # be running against a SNAPSHOT version. - name: build integration tests project uses: ./.github/workflows/composites/build-integration-tests-project + - name: put controllers and integration tests tars to /tmp/docker/images + shell: bash + run: | + + # find the version of our project from file: spring-cloud-kubernetes-integration-tests/pom.xml + PROJECT_VERSION="$(./mvnw -q -f spring-cloud-kubernetes-integration-tests/pom.xml help:evaluate -Dexpression=project.version -DforceStdout)" + + # get the existing docker images ( controllers and integration tests ) into the 'images' array + mapfile -t images < <( + docker image ls --format '{{.Repository}}:{{.Tag}}' \ + | grep "^springcloud/.*:${PROJECT_VERSION}$" \ + | sort -u + ) + + # print what we currently have + for i in "${!images[@]}"; do + printf 'images[%s]=%s\n' "$i" "${images[$i]}" + done + + # look at variable 'var', if it starts with 'springcloud/', remove it, return the rest. + for image in "${images[@]}"; do + tar_name="${image#springcloud/}.tar" + docker save -o "/tmp/docker/images/${tar_name}" "$image" + done + + - name: display /tmp/docker/images + shell: bash + run: ls -l /tmp/docker/images + - name: download tests uses: actions/download-artifact@v4 with: name: tests-${{ inputs.branch }}-jdk${{ inputs.jdk-version }}.txt path: /tmp - - - - From 809fec6c1aaf5646393821ef7eb681d857e47b1b Mon Sep 17 00:00:00 2001 From: wind57 Date: Tue, 5 May 2026 14:20:09 +0300 Subject: [PATCH 2/7] first run on github actions Signed-off-by: wind57 --- .../integration/tests/commons/Commons.java | 51 +++++++++++-------- .../integration/tests/commons/Constants.java | 12 +++-- .../tests/commons/FixedPortsK3sContainer.java | 8 +-- .../FabricClientIntegrationTestExtension.java | 6 +-- .../NativeClientIntegrationTestExtension.java | 6 +-- 5 files changed, 47 insertions(+), 36 deletions(-) diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java index 476aca831a..2577a587e5 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java @@ -49,9 +49,9 @@ import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.IMAGE_TARS_TEMP_DIR; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.K3S_IMAGE_TARS_DIR; import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.KUBERNETES_VERSION_FILE; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.TEMP_FOLDER; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.TMP_IMAGES; import static org.springframework.cloud.kubernetes.integration.tests.commons.FixedPortsK3sContainer.CONTAINER; /** @@ -78,12 +78,16 @@ public static K3sContainer container() { return CONTAINER; } - public static void loadSpringCloudKubernetesImage(String project, K3sContainer container) { - try { - loadImage("springcloud/" + project, pomVersion(), project, container); + public static void loadSpringCloudKubernetesImage(String imageName, K3sContainer container) { + File dockerImagesRootDir = Paths.get(K3S_IMAGE_TARS_DIR).toFile(); + String tarName = imageName + ":" + pomVersion(); + + if (dockerImagesRootDir.exists() && dockerImagesRootDir.isDirectory()) { + // we are in github actions ( not locally ) + loadImageFromPath(tarName, container); } - catch (Exception e) { - throw new RuntimeException(e); + else { + loadImage(imageName, pomVersion(), tarName, container); } } @@ -100,20 +104,21 @@ public static void loadImage(String image, String tag, String tarName, K3sContai try (SaveImageCmd saveImageCmd = container.getDockerClient().saveImageCmd(image)) { InputStream imageStream = saveImageCmd.withTag(tag).exec(); - Path imagePath = Paths.get(TEMP_FOLDER + "/" + tarName + ".tar"); + Path imagePath = Paths.get(IMAGE_TARS_TEMP_DIR + "/" + tarName + ".tar"); try { Files.copy(imageStream, imagePath, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new UncheckedIOException(e); } - // import image with ctr. this works because TEMP_FOLDER is mounted in the + // import image with ctr. this works because IMAGE_TARS_TEMP_DIR is mounted in + // the // container Awaitilities.awaitUntil(120, 1000, () -> { Container.ExecResult result; try { result = container.execInContainer("ctr", "i", "import", - Constants.TEMP_FOLDER + "/" + tarName + ".tar"); + Constants.IMAGE_TARS_TEMP_DIR + "/" + tarName + ".tar"); } catch (Exception e) { throw new RuntimeException(e); @@ -129,21 +134,20 @@ public static void loadImage(String image, String tag, String tarName, K3sContai } /** - * Ensures a common external test image is available inside K3s/containerd. - * It first checks whether the image is already present in K3s. - * If not, it tries to load it as a tar under '/tmp/docker/images'. - * If no matching tar is found, it pulls the image directly inside K3s - * using 'ctr images pull'. - * This is meant for shared test images such as busybox, wiremock, kafka, etc. + * Ensures a common external test image is available inside K3s/containerd. It first + * checks whether the image is already present in K3s. If not, it tries to load it as + * a tar under '/tmp/docker/images'. If no matching tar is found, it pulls the image + * directly inside K3s using 'ctr images pull'. This is meant for shared test images + * such as busybox, wiremock, kafka, etc. */ - public static void loadOrPullCommonTestImages(K3sContainer container, String tarName, - String imageNameForDownload, String imageVersion) { + public static void loadOrPullCommonTestImages(K3sContainer container, String tarName, String imageNameForDownload, + String imageVersion) { if (imageAlreadyInK3s(container, tarName)) { return; } - File dockerImagesRootDir = Paths.get(TMP_IMAGES).toFile(); + File dockerImagesRootDir = Paths.get(K3S_IMAGE_TARS_DIR).toFile(); if (dockerImagesRootDir.exists() && dockerImagesRootDir.isDirectory()) { File[] tars = dockerImagesRootDir.listFiles(); if (tars != null && tars.length > 0) { @@ -152,7 +156,8 @@ public static void loadOrPullCommonTestImages(K3sContainer container, String tar .filter(x -> x.contains(tarName)) .findFirst(); if (found.isPresent()) { - LOG.info("running in github actions, will load from : " + TMP_IMAGES + " tar : " + found.get()); + LOG.info("running in github actions, will load from : " + K3S_IMAGE_TARS_DIR + " tar : " + + found.get()); loadImageFromPath(found.get(), container); return; } @@ -225,7 +230,9 @@ private static String fullImageReference(String imageName, String imageVersion) } /** - * validates that the provided image does exist in the local docker registry. + * validates that the provided image does exist in the local docker registry. This is + * only needed when running tests locally, in github actions images are already built + * by the pipeline. */ public static void validateImage(String image, K3sContainer container) { try (ListImagesCmd listImagesCmd = container.getDockerClient().listImagesCmd()) { @@ -311,7 +318,7 @@ private static void loadImageFromPath(String tarName, K3sContainer container) { Awaitilities.awaitUntil(120, 1000, () -> { Container.ExecResult result; try { - result = container.execInContainer("ctr", "i", "import", Constants.TMP_IMAGES + "/" + tarName); + result = container.execInContainer("ctr", "i", "import", Constants.K3S_IMAGE_TARS_DIR + "/" + tarName); } catch (Exception e) { throw new RuntimeException(e); diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java index 811299300b..73ab2ef80b 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java @@ -28,14 +28,18 @@ private Constants() { } /** - * this path is generated by the pipeline of github actions. + * Path populated by the GitHub Actions pipeline. It contains tar files for: + *
  • common test images (busybox, wiremock, etc.)
  • + *
  • controller images (configuration watcher, discovery server, config server)
  • + *
  • application images built from the integration-tests project
  • */ - static final String TMP_IMAGES = "/tmp/docker/images"; + static final String K3S_IMAGE_TARS_DIR = "/tmp/docker/images"; /** - * Temporary folder where to load images. + * Used by local integration-test runs to stage image tar files so they can later be + * imported into K3s via `ctr`. */ - static final String TEMP_FOLDER = new File(System.getProperty("java.io.tmpdir")).getAbsolutePath(); + static final String IMAGE_TARS_TEMP_DIR = new File(System.getProperty("java.io.tmpdir")).getAbsolutePath(); /** * where is the version situated. diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/FixedPortsK3sContainer.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/FixedPortsK3sContainer.java index d935f3c28e..1c78eb4d1e 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/FixedPortsK3sContainer.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/FixedPortsK3sContainer.java @@ -23,8 +23,8 @@ import org.testcontainers.k3s.K3sContainer; import org.testcontainers.utility.DockerImageName; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.TEMP_FOLDER; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.TMP_IMAGES; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.IMAGE_TARS_TEMP_DIR; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.K3S_IMAGE_TARS_DIR; /** * A K3sContainer, but with fixed port mappings. This is needed because of the nature of @@ -70,8 +70,8 @@ FixedPortsK3sContainer configureFixedPorts() { FixedPortsK3sContainer addBinds() { super.withCreateContainerCmdModifier(cmd -> { HostConfig hostConfig = Objects.requireNonNull(cmd.getHostConfig()); - hostConfig.withBinds(Bind.parse(TEMP_FOLDER + ":" + TEMP_FOLDER), - Bind.parse(TMP_IMAGES + ":" + TMP_IMAGES)); + hostConfig.withBinds(Bind.parse(IMAGE_TARS_TEMP_DIR + ":" + IMAGE_TARS_TEMP_DIR), + Bind.parse(K3S_IMAGE_TARS_DIR + ":" + K3S_IMAGE_TARS_DIR)); }); return this; diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java index 6c805a193a..24cb760460 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java @@ -64,9 +64,9 @@ public void beforeAll(ExtensionContext context) { } // 2. external image presence - for (String image : scenario.withImages()) { - Commons.validateImage(image, container); - Commons.loadSpringCloudKubernetesImage(image, container); + for (String imageName : scenario.withImages()) { + Commons.validateImage(imageName, container); + Commons.loadSpringCloudKubernetesImage(imageName, container); } // 3. deploy istio diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java index 5af4da9ee4..67bdfc5e42 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java @@ -58,9 +58,9 @@ public void beforeAll(ExtensionContext context) throws Exception { } // 2. external image presence - for (String image : scenario.withImages()) { - Commons.validateImage(image, container); - Commons.loadSpringCloudKubernetesImage(image, container); + for (String imageName : scenario.withImages()) { + Commons.validateImage(imageName, container); + Commons.loadSpringCloudKubernetesImage(imageName, container); } // 3. set-up RBAC. From ed1aec4c98034c95d51e226e7e0a854d062ce1d7 Mon Sep 17 00:00:00 2001 From: wind57 Date: Tue, 5 May 2026 21:01:58 +0300 Subject: [PATCH 3/7] first run on github actions Signed-off-by: wind57 --- .../integration/tests/commons/Commons.java | 31 ++++++++++--------- .../Fabric8ClientKubernetesFixture.java | 12 ++++--- .../FabricClientIntegrationTestExtension.java | 6 ++-- .../NativeClientIntegrationTestExtension.java | 6 ++-- .../NativeClientKubernetesFixture.java | 2 +- 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java index 2577a587e5..aa0b8dc588 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java @@ -78,30 +78,33 @@ public static K3sContainer container() { return CONTAINER; } - public static void loadSpringCloudKubernetesImage(String imageName, K3sContainer container) { + public static void loadSpringCloudKubernetesImage(String imageNameWithoutTag, K3sContainer container) { File dockerImagesRootDir = Paths.get(K3S_IMAGE_TARS_DIR).toFile(); - String tarName = imageName + ":" + pomVersion(); + String tarName = imageNameWithoutTag + ":" + pomVersion() + ".tar"; if (dockerImagesRootDir.exists() && dockerImagesRootDir.isDirectory()) { - // we are in github actions ( not locally ) - loadImageFromPath(tarName, container); - } - else { - loadImage(imageName, pomVersion(), tarName, container); + File[] tars = dockerImagesRootDir.listFiles(); + if (tars != null && tars.length > 0) { + // we are in github actions ( not locally ) + loadImageFromPath(tarName, container); + return; + } } + + loadImage("springcloud/" + imageNameWithoutTag, pomVersion(), tarName, container); } /** * create a tar, copy it in the running k3s and load this tar as an image. */ - public static void loadImage(String image, String tag, String tarName, K3sContainer container) { + public static void loadImage(String imageNameWithoutTag, String tag, String tarName, K3sContainer container) { if (imageAlreadyInK3s(container, tarName)) { return; } // save image - try (SaveImageCmd saveImageCmd = container.getDockerClient().saveImageCmd(image)) { + try (SaveImageCmd saveImageCmd = container.getDockerClient().saveImageCmd(imageNameWithoutTag)) { InputStream imageStream = saveImageCmd.withTag(tag).exec(); Path imagePath = Paths.get(IMAGE_TARS_TEMP_DIR + "/" + tarName + ".tar"); @@ -246,15 +249,15 @@ public static void validateImage(String image, K3sContainer container) { } } - public static void pullImage(String image, String tag, String tarName, K3sContainer container) + public static void pullImage(String imageWithTag, String tarName, K3sContainer container) throws InterruptedException { if (imageAlreadyInK3s(container, tarName)) { return; } - try (PullImageCmd pullImageCmd = container.getDockerClient().pullImageCmd(image)) { - pullImageCmd.withTag(tag).start().awaitCompletion(); + try (PullImageCmd pullImageCmd = container.getDockerClient().pullImageCmd(imageWithTag)) { + pullImageCmd.start().awaitCompletion(); } } @@ -342,11 +345,11 @@ private static boolean imageAlreadyInK3s(K3sContainer container, String tarName) .getStdout() .contains(tarName); if (present) { - System.out.println("image : " + tarName + " already in k3s, skipping"); + LOG.info("image : " + tarName + " already in k3s, skipping"); return true; } else { - System.out.println("image : " + tarName + " not in k3s"); + LOG.info("image : " + tarName + " not in k3s"); return false; } } diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Fabric8ClientKubernetesFixture.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Fabric8ClientKubernetesFixture.java index ab5859a25a..73bdb81a8b 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Fabric8ClientKubernetesFixture.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Fabric8ClientKubernetesFixture.java @@ -70,10 +70,11 @@ public Fabric8ClientKubernetesFixture(K3sContainer container) { * service. It creates the given resources as-well as waits for them to be created. * The delay check is intentionally not taken as an argument, so that it stays as * tight as possible, providing reasonable defaults. - * + * @param appendTag is false for common test images like busybox, wiremock. it is true + * for our images that we build from integration tests. */ public void createAndWait(String namespace, String name, @Nullable Deployment deployment, @Nullable Service service, - boolean changeVersion) { + boolean appendTag) { try { if (deployment != null) { @@ -83,7 +84,8 @@ public void createAndWait(String namespace, String name, @Nullable Deployment de .getContainers() .get(0) .getImage(); - if (changeVersion) { + + if (appendTag) { deployment.getSpec() .getTemplate() .getSpec() @@ -92,8 +94,10 @@ public void createAndWait(String namespace, String name, @Nullable Deployment de .setImage(imageFromDeployment + ":" + pomVersion()); } else { + // pullImage is only needed when we run some test locally. + // Inside github actions, this will be a NOOP. + pullImage(imageFromDeployment, name, container); String[] image = imageFromDeployment.split(":", 2); - pullImage(image[0], image[1], name, container); loadImage(image[0], image[1], name, container); } diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java index 24cb760460..5e2c0bb4fb 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java @@ -64,9 +64,9 @@ public void beforeAll(ExtensionContext context) { } // 2. external image presence - for (String imageName : scenario.withImages()) { - Commons.validateImage(imageName, container); - Commons.loadSpringCloudKubernetesImage(imageName, container); + for (String imageNameWithoutTag : scenario.withImages()) { + Commons.validateImage(imageNameWithoutTag, container); + Commons.loadSpringCloudKubernetesImage(imageNameWithoutTag, container); } // 3. deploy istio diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java index 67bdfc5e42..9afbde98d4 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java @@ -58,9 +58,9 @@ public void beforeAll(ExtensionContext context) throws Exception { } // 2. external image presence - for (String imageName : scenario.withImages()) { - Commons.validateImage(imageName, container); - Commons.loadSpringCloudKubernetesImage(imageName, container); + for (String imageNameWithoutTag : scenario.withImages()) { + Commons.validateImage(imageNameWithoutTag, container); + Commons.loadSpringCloudKubernetesImage(imageNameWithoutTag, container); } // 3. set-up RBAC. diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java index 4b567c8f1d..005e59b1fd 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java @@ -128,8 +128,8 @@ public void createAndWait(String namespace, String name, V1Deployment deployment .setImage(imageFromDeployment + ":" + pomVersion()); } else { + pullImage(imageFromDeployment, name, container); String[] image = imageFromDeployment.split(":", 2); - pullImage(image[0], image[1], name, container); loadImage(image[0], image[1], name, container); } From 4aa092a305e60caae5887a8296cb02276595c667 Mon Sep 17 00:00:00 2001 From: wind57 Date: Wed, 6 May 2026 12:42:06 +0300 Subject: [PATCH 4/7] first run on github actions Signed-off-by: wind57 --- .../watcher/SecretWatcherWithLabelsTest.java | 3 +- .../fabric8/client/reload/TestAssertions.java | 2 +- .../integration/tests/commons/Commons.java | 249 ---------------- .../integration/tests/commons/Constants.java | 11 +- .../tests/commons/FixedPortsK3sContainer.java | 8 +- .../integration/tests/commons/Images.java | 14 +- .../tests/commons/K3sImageLoader.java | 275 ++++++++++++++++++ .../Fabric8ClientKubernetesFixture.java | 42 ++- .../FabricClientIntegrationTestExtension.java | 7 +- .../NativeClientIntegrationTestExtension.java | 5 +- .../NativeClientKubernetesFixture.java | 32 +- 11 files changed, 331 insertions(+), 317 deletions(-) create mode 100644 spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretWatcherWithLabelsTest.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretWatcherWithLabelsTest.java index 95c1e9d6b5..2118b096a9 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretWatcherWithLabelsTest.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretWatcherWithLabelsTest.java @@ -66,8 +66,7 @@ "spring.cloud.kubernetes.reload.mode=EVENT", "spring.cloud.kubernetes.reload.monitoring-config-maps=false", "spring.cloud.kubernetes.reload.monitoring-secrets=true", - "spring.cloud.kubernetes.secrets.enabled=true", - "spring.cloud.kubernetes.secrets.enabled=true", + "spring.cloud.kubernetes.secrets.enabled=true", "spring.cloud.kubernetes.secrets.enabled=true", "spring.cloud.kubernetes.reload.secrets-labels[spring.cloud.kubernetes.secret]=true", "spring.cloud.kubernetes.configuration.watcher.refresh-delay=1ms" }) class SecretWatcherWithLabelsTest { diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/TestAssertions.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/TestAssertions.java index 2280049998..5ec746884c 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/TestAssertions.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/TestAssertions.java @@ -96,7 +96,7 @@ static void manifests(Phase phase, Fabric8ClientKubernetesFixture fabric8Kuberne if (phase.equals(Phase.CREATE)) { fabric8KubernetesFixture.createAndWait(namespace, configMap, null); - fabric8KubernetesFixture.createAndWait(namespace, null, deployment, service, true); + fabric8KubernetesFixture.createAndWait(namespace, deployment, service, true); } else { fabric8KubernetesFixture.deleteAndWait(namespace, configMap, null); diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java index aa0b8dc588..1b3ff46beb 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java @@ -16,28 +16,11 @@ package org.springframework.cloud.kubernetes.integration.tests.commons; -import java.io.File; -import java.io.IOException; import java.io.InputStream; -import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.time.Duration; -import java.util.Arrays; -import java.util.List; import java.util.Objects; -import java.util.Optional; -import com.github.dockerjava.api.command.ListImagesCmd; -import com.github.dockerjava.api.command.PullImageCmd; -import com.github.dockerjava.api.command.SaveImageCmd; -import com.github.dockerjava.api.model.Image; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.testcontainers.containers.Container; import org.testcontainers.k3s.K3sContainer; import reactor.netty.http.client.HttpClient; import reactor.util.retry.Retry; @@ -49,8 +32,6 @@ import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.IMAGE_TARS_TEMP_DIR; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.K3S_IMAGE_TARS_DIR; import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.KUBERNETES_VERSION_FILE; import static org.springframework.cloud.kubernetes.integration.tests.commons.FixedPortsK3sContainer.CONTAINER; @@ -64,12 +45,6 @@ public final class Commons { private static String POM_VERSION; - private static final Log LOG = LogFactory.getLog(Commons.class); - - private static final String DOCKER_IO = "docker.io/"; - - private static final String DOCKER_IO_LIBRARY = DOCKER_IO + "library/"; - private Commons() { throw new AssertionError("No instance provided"); } @@ -78,189 +53,6 @@ public static K3sContainer container() { return CONTAINER; } - public static void loadSpringCloudKubernetesImage(String imageNameWithoutTag, K3sContainer container) { - File dockerImagesRootDir = Paths.get(K3S_IMAGE_TARS_DIR).toFile(); - String tarName = imageNameWithoutTag + ":" + pomVersion() + ".tar"; - - if (dockerImagesRootDir.exists() && dockerImagesRootDir.isDirectory()) { - File[] tars = dockerImagesRootDir.listFiles(); - if (tars != null && tars.length > 0) { - // we are in github actions ( not locally ) - loadImageFromPath(tarName, container); - return; - } - } - - loadImage("springcloud/" + imageNameWithoutTag, pomVersion(), tarName, container); - } - - /** - * create a tar, copy it in the running k3s and load this tar as an image. - */ - public static void loadImage(String imageNameWithoutTag, String tag, String tarName, K3sContainer container) { - - if (imageAlreadyInK3s(container, tarName)) { - return; - } - - // save image - try (SaveImageCmd saveImageCmd = container.getDockerClient().saveImageCmd(imageNameWithoutTag)) { - InputStream imageStream = saveImageCmd.withTag(tag).exec(); - - Path imagePath = Paths.get(IMAGE_TARS_TEMP_DIR + "/" + tarName + ".tar"); - try { - Files.copy(imageStream, imagePath, StandardCopyOption.REPLACE_EXISTING); - } - catch (IOException e) { - throw new UncheckedIOException(e); - } - // import image with ctr. this works because IMAGE_TARS_TEMP_DIR is mounted in - // the - // container - Awaitilities.awaitUntil(120, 1000, () -> { - Container.ExecResult result; - try { - result = container.execInContainer("ctr", "i", "import", - Constants.IMAGE_TARS_TEMP_DIR + "/" + tarName + ".tar"); - } - catch (Exception e) { - throw new RuntimeException(e); - } - boolean noErrors = result.getStderr() == null || result.getStderr().isEmpty(); - if (!noErrors) { - LOG.info("error is : " + result.getStderr()); - } - return noErrors; - }); - } - - } - - /** - * Ensures a common external test image is available inside K3s/containerd. It first - * checks whether the image is already present in K3s. If not, it tries to load it as - * a tar under '/tmp/docker/images'. If no matching tar is found, it pulls the image - * directly inside K3s using 'ctr images pull'. This is meant for shared test images - * such as busybox, wiremock, kafka, etc. - */ - public static void loadOrPullCommonTestImages(K3sContainer container, String tarName, String imageNameForDownload, - String imageVersion) { - - if (imageAlreadyInK3s(container, tarName)) { - return; - } - - File dockerImagesRootDir = Paths.get(K3S_IMAGE_TARS_DIR).toFile(); - if (dockerImagesRootDir.exists() && dockerImagesRootDir.isDirectory()) { - File[] tars = dockerImagesRootDir.listFiles(); - if (tars != null && tars.length > 0) { - Optional found = Arrays.stream(tars) - .map(File::getName) - .filter(x -> x.contains(tarName)) - .findFirst(); - if (found.isPresent()) { - LOG.info("running in github actions, will load from : " + K3S_IMAGE_TARS_DIR + " tar : " - + found.get()); - loadImageFromPath(found.get(), container); - return; - } - else { - LOG.info(tarName + " not found, resorting to pulling the image"); - } - } - else { - LOG.info("no tars found, will resort to pulling the image"); - } - } - else { - LOG.info("running outside github actions"); - } - - try { - LOG.info("pulling image inside k3s to avoid Docker save/ctr import compatibility issues"); - LOG.info("using : " + imageVersion + " for : " + imageNameForDownload); - pullImageInsideK3s(container, imageNameForDownload, imageVersion); - } - catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Pull image directly inside the K3s container via ctr (containerd). This avoids - * Docker save + ctr import, which can fail with "content digest not found" for - * multi-platform or OCI images. If DOCKER_HUB_USERNAME and DOCKER_HUB_PASSWORD are - * set, they are passed as {@code --user username:password} for registry auth. - */ - private static void pullImageInsideK3s(K3sContainer container, String imageNameForDownload, String imageVersion) { - String fullImageRef = fullImageReference(imageNameForDownload, imageVersion); - - final String[] ctrArgs = buildCtrPullArgs(fullImageRef); - Awaitilities.awaitUntil(120, 1, () -> { - try { - Container.ExecResult result = container.execInContainer(ctrArgs); - boolean noErrors = result.getStderr() == null || result.getStderr().isEmpty(); - if (!noErrors) { - LOG.info("ctr pull stderr: " + result.getStderr()); - } - return noErrors; - } - catch (Exception e) { - throw new RuntimeException(e); - } - - }); - } - - private static String[] buildCtrPullArgs(String fullImageRef) { - String username = System.getenv("DOCKER_HUB_USERNAME"); - String password = System.getenv("DOCKER_HUB_PASSWORD"); - if (username != null && !username.isBlank() && password != null && !password.isBlank()) { - LOG.info("pulling inside k3s with Docker Hub credentials: " + fullImageRef); - return new String[] { "ctr", "-n", "k8s.io", "images", "pull", "--user", username + ":" + password, - fullImageRef }; - } - LOG.info("pulling inside k3s: " + fullImageRef); - return new String[] { "ctr", "-n", "k8s.io", "images", "pull", fullImageRef }; - } - - private static String fullImageReference(String imageName, String imageVersion) { - String imageNameAndVersion = imageName + ":" + imageVersion; - if (imageName.contains("/")) { - return DOCKER_IO + imageNameAndVersion; - } - return DOCKER_IO_LIBRARY + imageNameAndVersion; - } - - /** - * validates that the provided image does exist in the local docker registry. This is - * only needed when running tests locally, in github actions images are already built - * by the pipeline. - */ - public static void validateImage(String image, K3sContainer container) { - try (ListImagesCmd listImagesCmd = container.getDockerClient().listImagesCmd()) { - List images = listImagesCmd.exec(); - images.stream() - .filter(x -> Arrays.stream(x.getRepoTags() == null ? new String[] {} : x.getRepoTags()) - .anyMatch(y -> y.contains(image))) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Image : " + image + " not build locally. " - + "You need to build it first, and then run the test")); - } - } - - public static void pullImage(String imageWithTag, String tarName, K3sContainer container) - throws InterruptedException { - - if (imageAlreadyInK3s(container, tarName)) { - return; - } - - try (PullImageCmd pullImageCmd = container.getDockerClient().pullImageCmd(imageWithTag)) { - pullImageCmd.start().awaitCompletion(); - } - } - public static String pomVersion() { if (POM_VERSION == null) { try (InputStream in = new ClassPathResource(KUBERNETES_VERSION_FILE).getInputStream()) { @@ -317,45 +109,4 @@ public static RetryBackoffSpec retrySpec() { return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); } - private static void loadImageFromPath(String tarName, K3sContainer container) { - Awaitilities.awaitUntil(120, 1000, () -> { - Container.ExecResult result; - try { - result = container.execInContainer("ctr", "i", "import", Constants.K3S_IMAGE_TARS_DIR + "/" + tarName); - } - catch (Exception e) { - throw new RuntimeException(e); - } - boolean noErrors = result.getStderr() == null || result.getStderr().isEmpty(); - if (!noErrors) { - LOG.info("error is : " + result.getStderr()); - } - return noErrors; - }); - } - - private static boolean imageAlreadyInK3s(K3sContainer container, String tarName) { - - if (tarName == null) { - return false; - } - - try { - boolean present = container.execInContainer("sh", "-c", "ctr images list | grep " + tarName) - .getStdout() - .contains(tarName); - if (present) { - LOG.info("image : " + tarName + " already in k3s, skipping"); - return true; - } - else { - LOG.info("image : " + tarName + " not in k3s"); - return false; - } - } - catch (Exception e) { - throw new RuntimeException(e); - } - } - } diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java index 73ab2ef80b..4b5ad0c3bd 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java @@ -28,18 +28,19 @@ private Constants() { } /** - * Path populated by the GitHub Actions pipeline. It contains tar files for: + * Directory populated by the CI pipeline with prebuilt image tar files. It contains + * tar files for: *
  • common test images (busybox, wiremock, etc.)
  • *
  • controller images (configuration watcher, discovery server, config server)
  • *
  • application images built from the integration-tests project
  • */ - static final String K3S_IMAGE_TARS_DIR = "/tmp/docker/images"; + static final String CI_IMAGE_TARS_DIR = "/tmp/docker/images"; /** - * Used by local integration-test runs to stage image tar files so they can later be - * imported into K3s via `ctr`. + * Directory used during local runs to stage image tar files created from the local + * Docker cache before importing them into K3s with 'ctr i import'. */ - static final String IMAGE_TARS_TEMP_DIR = new File(System.getProperty("java.io.tmpdir")).getAbsolutePath(); + static final String LOCAL_IMAGE_TARS_DIR = new File(System.getProperty("java.io.tmpdir")).getAbsolutePath(); /** * where is the version situated. diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/FixedPortsK3sContainer.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/FixedPortsK3sContainer.java index 1c78eb4d1e..283ae9a609 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/FixedPortsK3sContainer.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/FixedPortsK3sContainer.java @@ -23,8 +23,8 @@ import org.testcontainers.k3s.K3sContainer; import org.testcontainers.utility.DockerImageName; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.IMAGE_TARS_TEMP_DIR; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.K3S_IMAGE_TARS_DIR; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.CI_IMAGE_TARS_DIR; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.LOCAL_IMAGE_TARS_DIR; /** * A K3sContainer, but with fixed port mappings. This is needed because of the nature of @@ -70,8 +70,8 @@ FixedPortsK3sContainer configureFixedPorts() { FixedPortsK3sContainer addBinds() { super.withCreateContainerCmdModifier(cmd -> { HostConfig hostConfig = Objects.requireNonNull(cmd.getHostConfig()); - hostConfig.withBinds(Bind.parse(IMAGE_TARS_TEMP_DIR + ":" + IMAGE_TARS_TEMP_DIR), - Bind.parse(K3S_IMAGE_TARS_DIR + ":" + K3S_IMAGE_TARS_DIR)); + hostConfig.withBinds(Bind.parse(LOCAL_IMAGE_TARS_DIR + ":" + LOCAL_IMAGE_TARS_DIR), + Bind.parse(CI_IMAGE_TARS_DIR + ":" + CI_IMAGE_TARS_DIR)); }); return this; diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java index ef76b8009f..ee9a85922b 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java @@ -80,34 +80,34 @@ public static String wiremockVersion() { public static void loadBusybox(K3sContainer container) { if (!imageAlreadyInK3s(container, BUSYBOX_TAR)) { - Commons.loadOrPullCommonTestImages(container, BUSYBOX_TAR, BUSYBOX, busyboxVersion()); + K3sImageLoader.loadOrPullCommonTestImages(container, BUSYBOX_TAR, BUSYBOX, busyboxVersion()); } } public static void loadWiremock(K3sContainer container) { if (!imageAlreadyInK3s(container, WIREMOCK_TAR)) { - Commons.loadOrPullCommonTestImages(container, WIREMOCK_TAR, WIREMOCK, wiremockVersion()); + K3sImageLoader.loadOrPullCommonTestImages(container, WIREMOCK_TAR, WIREMOCK, wiremockVersion()); } } public static void loadIstioCtl(K3sContainer container) { - Commons.loadOrPullCommonTestImages(container, ISTIOCTL_TAR, ISTIOCTL, istioVersion()); + K3sImageLoader.loadOrPullCommonTestImages(container, ISTIOCTL_TAR, ISTIOCTL, istioVersion()); } public static void loadIstioProxyV2(K3sContainer container) { - Commons.loadOrPullCommonTestImages(container, ISTIO_PROXY_V2_TAR, ISTIO_PROXY_V2, istioVersion()); + K3sImageLoader.loadOrPullCommonTestImages(container, ISTIO_PROXY_V2_TAR, ISTIO_PROXY_V2, istioVersion()); } public static void loadIstioPilot(K3sContainer container) { - Commons.loadOrPullCommonTestImages(container, ISTIO_PILOT_TAR, ISTIO_PILOT, istioVersion()); + K3sImageLoader.loadOrPullCommonTestImages(container, ISTIO_PILOT_TAR, ISTIO_PILOT, istioVersion()); } public static void loadKafka(K3sContainer container) { - Commons.loadOrPullCommonTestImages(container, KAFKA_TAR, KAFKA, kafkaVersion()); + K3sImageLoader.loadOrPullCommonTestImages(container, KAFKA_TAR, KAFKA, kafkaVersion()); } public static void loadRabbitmq(K3sContainer container) { - Commons.loadOrPullCommonTestImages(container, RABBITMQ_TAR, RABBITMQ, rabbitMqVersion()); + K3sImageLoader.loadOrPullCommonTestImages(container, RABBITMQ_TAR, RABBITMQ, rabbitMqVersion()); } private static boolean imageAlreadyInK3s(K3sContainer container, String tarName) { diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java new file mode 100644 index 0000000000..a67f66855a --- /dev/null +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java @@ -0,0 +1,275 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.integration.tests.commons; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import com.github.dockerjava.api.command.ListImagesCmd; +import com.github.dockerjava.api.command.PullImageCmd; +import com.github.dockerjava.api.command.SaveImageCmd; +import com.github.dockerjava.api.model.Image; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.testcontainers.containers.Container; +import org.testcontainers.k3s.K3sContainer; + +import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.pomVersion; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.CI_IMAGE_TARS_DIR; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Constants.LOCAL_IMAGE_TARS_DIR; + +/** + * @author wind57 + */ +public final class K3sImageLoader { + + private static final Log LOG = LogFactory.getLog(K3sImageLoader.class); + + private static final String DOCKER_IO = "docker.io/"; + + private static final String DOCKER_IO_LIBRARY = DOCKER_IO + "library/"; + + private K3sImageLoader() { + + } + + public static void loadSpringCloudKubernetesImage(String imageNameWithoutTag, K3sContainer container) { + String tag = pomVersion(); + String imageNameWithTag = imageNameWithoutTag + ":" + tag; + String tarFileName = imageNameWithTag + ".tar"; + Path ciTarPath = Paths.get(CI_IMAGE_TARS_DIR, tarFileName); + + // we are in github ci pipeline + if (Files.exists(ciTarPath)) { + loadCiImageTar(tarFileName, container); + return; + } + + createAndLoadLocalImageTar("springcloud/" + imageNameWithoutTag, tag, tarFileName, container); + } + + /** + * Ensures a common external test image is available inside K3s/containerd. It first + * checks whether the image is already present in K3s. If not, it tries to load it as + * a tar under '/tmp/docker/images'. If no matching tar is found, it pulls the image + * directly inside K3s using 'ctr images pull'. This is meant for shared test images + * such as busybox, wiremock, kafka, etc. + */ + public static void loadOrPullCommonTestImages(K3sContainer container, String tarName, String imageNameForDownload, + String imageVersion) { + + if (imageAlreadyInK3s(container, tarName)) { + return; + } + + File dockerImagesRootDir = Paths.get(CI_IMAGE_TARS_DIR).toFile(); + if (dockerImagesRootDir.exists() && dockerImagesRootDir.isDirectory()) { + File[] tars = dockerImagesRootDir.listFiles(); + if (tars != null && tars.length > 0) { + Optional found = Arrays.stream(tars) + .map(File::getName) + .filter(x -> x.contains(tarName)) + .findFirst(); + if (found.isPresent()) { + LOG.info("running in github actions, will load from : " + CI_IMAGE_TARS_DIR + " tar : " + + found.get()); + loadCiImageTar(found.get(), container); + return; + } + else { + LOG.info(tarName + " not found, resorting to pulling the image"); + } + } + else { + LOG.info("no tars found, will resort to pulling the image"); + } + } + else { + LOG.info("running outside github actions"); + } + + try { + LOG.info("pulling image inside k3s to avoid Docker save/ctr import compatibility issues"); + LOG.info("using : " + imageVersion + " for : " + imageNameForDownload); + pullImageInsideK3s(container, imageNameForDownload, imageVersion); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * validates that the provided image does exist in the local docker registry. This is + * only needed when running tests locally, in github actions images are already built + * by the pipeline. + */ + public static void validateImage(String image, K3sContainer container) { + try (ListImagesCmd listImagesCmd = container.getDockerClient().listImagesCmd()) { + List images = listImagesCmd.exec(); + images.stream() + .filter(x -> Arrays.stream(x.getRepoTags() == null ? new String[] {} : x.getRepoTags()) + .anyMatch(y -> y.contains(image))) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Image : " + image + " not build locally. " + + "You need to build it first, and then run the test")); + } + } + + public static void pullImage(String imageWithTag, String tarName, K3sContainer container) + throws InterruptedException { + + if (imageAlreadyInK3s(container, tarName)) { + return; + } + + try (PullImageCmd pullImageCmd = container.getDockerClient().pullImageCmd(imageWithTag)) { + pullImageCmd.start().awaitCompletion(); + } + } + + private static void loadCiImageTar(String tarFileName, K3sContainer container) { + loadImageTar(CI_IMAGE_TARS_DIR, tarFileName, container); + } + + private static void loadLocalImageTar(String tarFileName, K3sContainer container) { + loadImageTar(LOCAL_IMAGE_TARS_DIR, tarFileName, container); + } + + private static void loadImageTar(String imageTarsDir, String tarFileName, K3sContainer container) { + Awaitilities.awaitUntil(120, 1000, () -> { + Container.ExecResult result; + try { + result = container.execInContainer("ctr", "i", "import", imageTarsDir + "/" + tarFileName); + } + catch (Exception e) { + throw new RuntimeException(e); + } + boolean noErrors = result.getStderr() == null || result.getStderr().isEmpty(); + if (!noErrors) { + LOG.info("error is : " + result.getStderr()); + } + return noErrors; + }); + } + + /** + * Pull image directly inside the K3s container via ctr (containerd). This avoids + * Docker save + ctr import, which can fail with "content digest not found" for + * multi-platform or OCI images. If DOCKER_HUB_USERNAME and DOCKER_HUB_PASSWORD are + * set, they are passed as {@code --user username:password} for registry auth. + */ + private static void pullImageInsideK3s(K3sContainer container, String imageNameForDownload, String imageVersion) { + String fullImageRef = fullImageReference(imageNameForDownload, imageVersion); + + final String[] ctrArgs = buildCtrPullArgs(fullImageRef); + Awaitilities.awaitUntil(120, 1, () -> { + try { + Container.ExecResult result = container.execInContainer(ctrArgs); + boolean noErrors = result.getStderr() == null || result.getStderr().isEmpty(); + if (!noErrors) { + LOG.info("ctr pull stderr: " + result.getStderr()); + } + return noErrors; + } + catch (Exception e) { + throw new RuntimeException(e); + } + + }); + } + + /** + * Local fallback for Spring Cloud images: create a tar from the local Docker image if + * needed, then import it into K3s. + */ + private static void createAndLoadLocalImageTar(String imageNameWithoutTag, String tag, String tarFileName, + K3sContainer container) { + + String imageRef = imageNameWithoutTag + ":" + tag; + if (imageAlreadyInK3s(container, imageRef)) { + return; + } + + Path tarPath = Paths.get(LOCAL_IMAGE_TARS_DIR, tarFileName); + + if (Files.notExists(tarPath)) { + try (SaveImageCmd saveImageCmd = container.getDockerClient().saveImageCmd(imageNameWithoutTag); + InputStream imageStream = saveImageCmd.withTag(tag).exec()) { + Files.copy(imageStream, tarPath, StandardCopyOption.REPLACE_EXISTING); + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + loadLocalImageTar(tarFileName, container); + } + + private static boolean imageAlreadyInK3s(K3sContainer container, String tarName) { + + if (tarName == null) { + return false; + } + + try { + boolean present = container.execInContainer("sh", "-c", "ctr images list | grep " + tarName) + .getStdout() + .contains(tarName); + if (present) { + LOG.info("image : " + tarName + " already in k3s, skipping"); + return true; + } + else { + LOG.info("image : " + tarName + " not in k3s"); + return false; + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String[] buildCtrPullArgs(String fullImageRef) { + String username = System.getenv("DOCKER_HUB_USERNAME"); + String password = System.getenv("DOCKER_HUB_PASSWORD"); + if (username != null && !username.isBlank() && password != null && !password.isBlank()) { + LOG.info("pulling inside k3s with Docker Hub credentials: " + fullImageRef); + return new String[] { "ctr", "-n", "k8s.io", "images", "pull", "--user", username + ":" + password, + fullImageRef }; + } + LOG.info("pulling inside k3s: " + fullImageRef); + return new String[] { "ctr", "-n", "k8s.io", "images", "pull", fullImageRef }; + } + + private static String fullImageReference(String imageName, String imageVersion) { + String imageNameAndVersion = imageName + ":" + imageVersion; + if (imageName.contains("/")) { + return DOCKER_IO + imageNameAndVersion; + } + return DOCKER_IO_LIBRARY + imageNameAndVersion; + } + +} diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Fabric8ClientKubernetesFixture.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Fabric8ClientKubernetesFixture.java index 73bdb81a8b..6d8e432aaf 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Fabric8ClientKubernetesFixture.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Fabric8ClientKubernetesFixture.java @@ -41,13 +41,11 @@ import org.testcontainers.k3s.K3sContainer; import org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.cloud.kubernetes.integration.tests.commons.Images; +import org.springframework.cloud.kubernetes.integration.tests.commons.K3sImageLoader; import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.loadImage; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.pomVersion; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.pullImage; - /** * @author wind57 */ @@ -73,32 +71,26 @@ public Fabric8ClientKubernetesFixture(K3sContainer container) { * @param appendTag is false for common test images like busybox, wiremock. it is true * for our images that we build from integration tests. */ - public void createAndWait(String namespace, String name, @Nullable Deployment deployment, @Nullable Service service, + public void createAndWait(String namespace, @Nullable Deployment deployment, @Nullable Service service, boolean appendTag) { try { if (deployment != null) { - String imageFromDeployment = deployment.getSpec() - .getTemplate() - .getSpec() - .getContainers() - .get(0) - .getImage(); if (appendTag) { - deployment.getSpec() + String imageFromDeployment = deployment.getSpec() .getTemplate() .getSpec() .getContainers() .get(0) - .setImage(imageFromDeployment + ":" + pomVersion()); - } - else { - // pullImage is only needed when we run some test locally. - // Inside github actions, this will be a NOOP. - pullImage(imageFromDeployment, name, container); - String[] image = imageFromDeployment.split(":", 2); - loadImage(image[0], image[1], name, container); + .getImage(); + + String imageNameWithoutTag = imageFromDeployment.replace("docker.io/springcloud/", ""); + K3sImageLoader.loadSpringCloudKubernetesImage(imageNameWithoutTag, container); + + String imageWithVersion = imageFromDeployment + ":" + Commons.pomVersion(); + + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(imageWithVersion); } client.apps().deployments().inNamespace(namespace).resource(deployment).create(); @@ -127,7 +119,7 @@ public void busybox(String namespace, Phase phase) { Service service = Serialization.unmarshal(serviceStream, Service.class); if (phase.equals(Phase.CREATE)) { - createAndWait(namespace, "busybox", deployment, service, false); + createAndWait(namespace, deployment, service, false); } else if (phase.equals(Phase.DELETE)) { deleteAndWait(namespace, deployment, service); @@ -138,7 +130,7 @@ public void externalName(Phase phase) { InputStream serviceStream = inputStream("external-name-service/external-name-service.yaml"); Service service = Serialization.unmarshal(serviceStream, Service.class); if (Phase.CREATE.equals(phase)) { - createAndWait("default", null, null, service, false); + createAndWait("default", null, service, false); } else { deleteAndWait("default", null, service); @@ -292,7 +284,7 @@ public void istioCtl(String namespace, Phase phase) { istioctlDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(imageWithVersion); if (phase.equals(Phase.CREATE)) { - createAndWait(namespace, null, istioctlDeployment, null, false); + createAndWait(namespace, istioctlDeployment, null, false); } else { deleteAndWait(namespace, istioctlDeployment, null); @@ -355,7 +347,7 @@ public void wiremock(String namespace, Phase phase, boolean withNodePort) { if (phase.equals(Phase.CREATE)) { deployment.getMetadata().setNamespace(namespace); service.getMetadata().setNamespace(namespace); - createAndWait(namespace, "wiremock", deployment, service, false); + createAndWait(namespace, deployment, service, false); } else { deleteAndWait(namespace, deployment, service); @@ -372,7 +364,7 @@ public void configWatcher(Phase phase) { Service service = client.services().load(serviceStream).item(); if (phase.equals(Phase.CREATE)) { - createAndWait("default", deployment.getMetadata().getName(), deployment, service, true); + createAndWait("default", deployment, service, true); } else if (phase.equals(Phase.DELETE)) { deleteAndWait("default", deployment, service); diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java index 5e2c0bb4fb..47e79acc65 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/FabricClientIntegrationTestExtension.java @@ -32,6 +32,7 @@ import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.cloud.kubernetes.integration.tests.commons.Images; +import org.springframework.cloud.kubernetes.integration.tests.commons.K3sImageLoader; import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Fabric8ClientKubernetesFixture; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -65,8 +66,8 @@ public void beforeAll(ExtensionContext context) { // 2. external image presence for (String imageNameWithoutTag : scenario.withImages()) { - Commons.validateImage(imageNameWithoutTag, container); - Commons.loadSpringCloudKubernetesImage(imageNameWithoutTag, container); + K3sImageLoader.validateImage(imageNameWithoutTag, container); + K3sImageLoader.loadSpringCloudKubernetesImage(imageNameWithoutTag, container); } // 3. deploy istio @@ -230,7 +231,7 @@ private void istioSetup(K3sContainer container, Fabric8ClientKubernetesFixture f Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); Service service = Serialization.unmarshal(serviceStream, Service.class); - fabric8KubernetesFixture.createAndWait("istio-test", null, deployment, service, true); + fabric8KubernetesFixture.createAndWait("istio-test", deployment, service, true); } catch (Exception e) { diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java index 9afbde98d4..0598d64057 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java @@ -27,6 +27,7 @@ import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.cloud.kubernetes.integration.tests.commons.Images; +import org.springframework.cloud.kubernetes.integration.tests.commons.K3sImageLoader; import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.NativeClientKubernetesFixture; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -59,8 +60,8 @@ public void beforeAll(ExtensionContext context) throws Exception { // 2. external image presence for (String imageNameWithoutTag : scenario.withImages()) { - Commons.validateImage(imageNameWithoutTag, container); - Commons.loadSpringCloudKubernetesImage(imageNameWithoutTag, container); + K3sImageLoader.validateImage(imageNameWithoutTag, container); + K3sImageLoader.loadSpringCloudKubernetesImage(imageNameWithoutTag, container); } // 3. set-up RBAC. diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java index 005e59b1fd..f1bb0e182a 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java @@ -58,13 +58,11 @@ import org.testcontainers.k3s.K3sContainer; import org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.cloud.kubernetes.integration.tests.commons.Images; +import org.springframework.cloud.kubernetes.integration.tests.commons.K3sImageLoader; import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.loadImage; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.pomVersion; -import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.pullImage; - /** * @author wind57 */ @@ -107,30 +105,26 @@ public NativeClientKubernetesFixture(K3sContainer container) { * */ public void createAndWait(String namespace, String name, V1Deployment deployment, V1Service service, - boolean changeVersion) { + boolean imageWithoutTag) { try { coreV1Api.createNamespacedService(namespace, service).execute(); if (deployment != null) { - String imageFromDeployment = deployment.getSpec() - .getTemplate() - .getSpec() - .getContainers() - .get(0) - .getImage(); - if (changeVersion) { - deployment.getSpec() + + if (imageWithoutTag) { + String imageFromDeployment = deployment.getSpec() .getTemplate() .getSpec() .getContainers() .get(0) - .setImage(imageFromDeployment + ":" + pomVersion()); - } - else { - pullImage(imageFromDeployment, name, container); - String[] image = imageFromDeployment.split(":", 2); - loadImage(image[0], image[1], name, container); + .getImage(); + + String imageNameWithoutTag = imageFromDeployment.substring("docker.io/springcloud/".length()); + K3sImageLoader.loadSpringCloudKubernetesImage(imageNameWithoutTag, container); + + String imageWithVersion = imageFromDeployment + ":" + Commons.pomVersion(); + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(imageWithVersion); } appsV1Api.createNamespacedDeployment(namespace, deployment).execute(); From 85390cc83a94436b5b3046eacd4ff56777284012 Mon Sep 17 00:00:00 2001 From: wind57 Date: Wed, 6 May 2026 13:55:44 +0300 Subject: [PATCH 5/7] first run on github actions Signed-off-by: wind57 --- .../apps/ConfigurationWatcherBusKafkaIT.java | 2 +- .../apps/ConfigurationWatcherBusAmqpIT.java | 2 +- .../it/K8sClientConfigMapConfigTreeIT.java | 4 +-- .../client/reload/it/K8sClientReloadBase.java | 7 +++--- .../integration/tests/commons/Images.java | 8 ++++-- .../tests/commons/K3sImageLoader.java | 4 --- .../NativeClientIntegrationTestExtension.java | 2 ++ .../NativeClientKubernetesFixture.java | 25 +++++++++---------- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload/kafka-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherBusKafkaIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload/kafka-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherBusKafkaIT.java index b39b3973e9..95e5ec883c 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload/kafka-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherBusKafkaIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload/kafka-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherBusKafkaIT.java @@ -53,7 +53,7 @@ class ConfigurationWatcherBusKafkaIT { void setup(NativeClientKubernetesFixture fixture) { V1Deployment deployment = fixture.yaml("app/app-deployment.yaml", V1Deployment.class); V1Service service = fixture.yaml("app/app-service.yaml", V1Service.class); - fixture.createAndWait("default", null, deployment, service, true); + fixture.createAndWait("default", deployment, service, true); } @AfterEach diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload/rabbitmq-secret-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherBusAmqpIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload/rabbitmq-secret-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherBusAmqpIT.java index e38e551deb..400fceeddd 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload/rabbitmq-secret-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherBusAmqpIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload/rabbitmq-secret-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherBusAmqpIT.java @@ -51,7 +51,7 @@ class ConfigurationWatcherBusAmqpIT { void setup(NativeClientKubernetesFixture fixture) { V1Deployment deployment = fixture.yaml("app/app-deployment.yaml", V1Deployment.class); V1Service service = fixture.yaml("app/app-service.yaml", V1Service.class); - fixture.createAndWait("default", null, deployment, service, true); + fixture.createAndWait("default", deployment, service, true); } @AfterEach diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapConfigTreeIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapConfigTreeIT.java index e13f9e5bd3..fab94e7bfe 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapConfigTreeIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapConfigTreeIT.java @@ -50,12 +50,12 @@ class K8sClientConfigMapConfigTreeIT extends K8sClientReloadBase { @BeforeAll static void beforeAllLocal(NativeClientKubernetesFixture fixture) { - manifests(Phase.CREATE, fixture, "default", "spring-cloud-kubernetes-k8s-client-reload"); + manifests(Phase.CREATE, fixture, "default"); } @AfterAll static void afterAll(NativeClientKubernetesFixture fixture) { - manifests(Phase.DELETE, fixture, "default", "spring-cloud-kubernetes-k8s-client-reload"); + manifests(Phase.DELETE, fixture, "default"); } /** diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java index 0267ebc86b..dc593166f6 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java @@ -88,8 +88,7 @@ protected static void replaceConfigMap(CoreV1Api api, V1ConfigMap configMap) { } } - protected static void manifests(Phase phase, NativeClientKubernetesFixture fixture, String namespace, - String imageName) { + protected static void manifests(Phase phase, NativeClientKubernetesFixture fixture, String namespace) { V1Deployment deployment = fixture.yaml("mount/deployment.yaml", V1Deployment.class); V1Service service = fixture.yaml("mount/service.yaml", V1Service.class); @@ -97,7 +96,7 @@ protected static void manifests(Phase phase, NativeClientKubernetesFixture fixtu if (phase.equals(Phase.CREATE)) { fixture.createAndWait(namespace, configMap, null); - fixture.createAndWait(namespace, imageName, deployment, service, true); + fixture.createAndWait(namespace, deployment, service, true); } else { fixture.deleteAndWait(namespace, configMap, null); @@ -114,7 +113,7 @@ protected static void manifestsSecret(Phase phase, NativeClientKubernetesFixture if (phase.equals(Phase.CREATE)) { fixture.createAndWait("default", null, secret); - fixture.createAndWait("default", "spring-cloud-kubernetes-k8s-client-reload", deployment, service, true); + fixture.createAndWait("default", deployment, service, true); } else { fixture.deleteAndWait("default", null, secret); diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java index ee9a85922b..e6d3f13d76 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java @@ -19,6 +19,8 @@ import java.io.BufferedReader; import java.io.InputStreamReader; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.testcontainers.k3s.K3sContainer; /** @@ -26,6 +28,8 @@ */ public final class Images { + private static final Log LOG = LogFactory.getLog(Images.class); + private static final String BUSYBOX = "busybox"; private static final String BUSYBOX_TAR = BUSYBOX + ":" + busyboxVersion(); @@ -116,11 +120,11 @@ private static boolean imageAlreadyInK3s(K3sContainer container, String tarName) .getStdout() .contains(tarName); if (present) { - System.out.println("image : " + tarName + " already in k3s, skipping"); + LOG.info("image : " + tarName + " already in k3s, skipping"); return true; } else { - System.out.println("image : " + tarName + " not in k3s"); + LOG.info("image : " + tarName + " not in k3s"); return false; } } diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java index a67f66855a..b2697ed0a9 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java @@ -81,10 +81,6 @@ public static void loadSpringCloudKubernetesImage(String imageNameWithoutTag, K3 public static void loadOrPullCommonTestImages(K3sContainer container, String tarName, String imageNameForDownload, String imageVersion) { - if (imageAlreadyInK3s(container, tarName)) { - return; - } - File dockerImagesRootDir = Paths.get(CI_IMAGE_TARS_DIR).toFile(); if (dockerImagesRootDir.exists() && dockerImagesRootDir.isDirectory()) { File[] tars = dockerImagesRootDir.listFiles(); diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java index 0598d64057..d5258e3bcb 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/k3s/NativeClientIntegrationTestExtension.java @@ -111,11 +111,13 @@ public void beforeAll(ExtensionContext context) throws Exception { // 10. deploy kafka if (scenario.deployKafka()) { + Images.loadKafka(container); nativeClientKubernetesFixture.kafka(Phase.CREATE); } // 11. deploy rabbitMq if (scenario.deployRabbitMq()) { + Images.loadRabbitmq(container); nativeClientKubernetesFixture.rabbitMq(Phase.CREATE); } } diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java index f1bb0e182a..bdcf122c9a 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/NativeClientKubernetesFixture.java @@ -104,15 +104,14 @@ public NativeClientKubernetesFixture(K3sContainer container) { * tight as possible, providing reasonable defaults. * */ - public void createAndWait(String namespace, String name, V1Deployment deployment, V1Service service, - boolean imageWithoutTag) { + public void createAndWait(String namespace, V1Deployment deployment, V1Service service, boolean appendTag) { try { coreV1Api.createNamespacedService(namespace, service).execute(); if (deployment != null) { - if (imageWithoutTag) { + if (appendTag) { String imageFromDeployment = deployment.getSpec() .getTemplate() .getSpec() @@ -262,13 +261,13 @@ public void deleteAndWait(String namespace, V1Deployment deployment, V1Service s public void busybox(String namespace, Phase phase) { V1Deployment deployment = yaml("busybox/deployment.yaml", V1Deployment.class); - String imageWithoutVersion = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage(); - String imageWithVersion = imageWithoutVersion + ":" + Images.busyboxVersion(); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(imageWithVersion); + String imageWithoutTag = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage(); + String imageWithTag = imageWithoutTag + ":" + Images.busyboxVersion(); + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(imageWithTag); V1Service service = yaml("busybox/service.yaml", V1Service.class); if (phase.equals(Phase.CREATE)) { - createAndWait(namespace, "busybox", deployment, service, false); + createAndWait(namespace, deployment, service, false); } else if (phase.equals(Phase.DELETE)) { deleteAndWait(namespace, deployment, service); @@ -285,7 +284,7 @@ public void kafka(Phase phase) { V1Service service = yaml("kafka/kafka-service.yaml", V1Service.class); if (phase.equals(Phase.CREATE)) { - createAndWait("default", "kafka", deployment, service, false); + createAndWait("default", deployment, service, false); } else if (phase.equals(Phase.DELETE)) { deleteAndWait("default", deployment, service); @@ -302,7 +301,7 @@ public void rabbitMq(Phase phase) { V1Service service = yaml("rabbitmq/rabbitmq-service.yaml", V1Service.class); if (phase.equals(Phase.CREATE)) { - createAndWait("default", "rabbitmq", deployment, service, false); + createAndWait("default", deployment, service, false); } else if (phase.equals(Phase.DELETE)) { deleteAndWait("default", deployment, service); @@ -489,7 +488,7 @@ public void wiremock(String namespace, Phase phase, boolean withNodePort) { if (phase.equals(Phase.CREATE)) { deployment.getMetadata().setNamespace(namespace); service.getMetadata().setNamespace(namespace); - createAndWait(namespace, "wiremock", deployment, service, false); + createAndWait(namespace, deployment, service, false); } else { deleteAndWait(namespace, deployment, service); @@ -500,7 +499,7 @@ public void wiremock(String namespace, Phase phase, boolean withNodePort) { public void externalName(Phase phase) { V1Service service = yaml("external-name-service/external-name-service.yaml", V1Service.class); if (Phase.CREATE.equals(phase)) { - createAndWait("default", null, null, service, false); + createAndWait("default", null, service, false); } else { deleteAndWait("default", null, service); @@ -549,7 +548,7 @@ public void configWatcher(Phase phase, String refreshDelay, boolean reloadEnable deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); if (phase.equals(Phase.CREATE)) { - createAndWait("default", deployment.getMetadata().getName(), deployment, service, true); + createAndWait("default", deployment, service, true); } else if (phase.equals(Phase.DELETE)) { deleteAndWait("default", deployment, service); @@ -561,7 +560,7 @@ public void discoveryServer(Phase phase) { V1Service service = yaml("discovery-server/discoveryserver-service.yaml", V1Service.class); if (phase == Phase.CREATE) { - createAndWait("default", null, deployment, service, true); + createAndWait("default", deployment, service, true); } else { deleteAndWait("default", null, service); From 7ff31b51c59fb326ae4aed049474799ef702954b Mon Sep 17 00:00:00 2001 From: wind57 Date: Wed, 6 May 2026 15:02:08 +0300 Subject: [PATCH 6/7] copilot review comments Signed-off-by: wind57 --- .../watcher/SecretWatcherWithLabelsTest.java | 2 +- .../kubernetes/integration/tests/commons/Constants.java | 2 ++ .../kubernetes/integration/tests/commons/Images.java | 4 ++-- .../integration/tests/commons/K3sImageLoader.java | 9 +++++++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretWatcherWithLabelsTest.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretWatcherWithLabelsTest.java index 2118b096a9..43eb55bbfd 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretWatcherWithLabelsTest.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretWatcherWithLabelsTest.java @@ -66,7 +66,7 @@ "spring.cloud.kubernetes.reload.mode=EVENT", "spring.cloud.kubernetes.reload.monitoring-config-maps=false", "spring.cloud.kubernetes.reload.monitoring-secrets=true", - "spring.cloud.kubernetes.secrets.enabled=true", "spring.cloud.kubernetes.secrets.enabled=true", + "spring.cloud.kubernetes.secrets.enabled=true", "spring.cloud.kubernetes.reload.secrets-labels[spring.cloud.kubernetes.secret]=true", "spring.cloud.kubernetes.configuration.watcher.refresh-delay=1ms" }) class SecretWatcherWithLabelsTest { diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java index 4b5ad0c3bd..80b2d87250 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Constants.java @@ -30,9 +30,11 @@ private Constants() { /** * Directory populated by the CI pipeline with prebuilt image tar files. It contains * tar files for: + *
      *
    • common test images (busybox, wiremock, etc.)
    • *
    • controller images (configuration watcher, discovery server, config server)
    • *
    • application images built from the integration-tests project
    • + *
    */ static final String CI_IMAGE_TARS_DIR = "/tmp/docker/images"; diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java index e6d3f13d76..8a775723d6 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Images.java @@ -52,11 +52,11 @@ public final class Images { private static final String KAFKA = "confluentinc/confluent-local"; - private static final String KAFKA_TAR = KAFKA.replace('/', '-') + kafkaVersion(); + private static final String KAFKA_TAR = KAFKA.replace('/', '-') + ":" + kafkaVersion(); private static final String RABBITMQ = "rabbitmq"; - private static final String RABBITMQ_TAR = "rabbitmq"; + private static final String RABBITMQ_TAR = "rabbitmq" + ":" + rabbitMqVersion(); private Images() { diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java index b2697ed0a9..09fa9f955c 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K3sImageLoader.java @@ -59,6 +59,11 @@ private K3sImageLoader() { public static void loadSpringCloudKubernetesImage(String imageNameWithoutTag, K3sContainer container) { String tag = pomVersion(); String imageNameWithTag = imageNameWithoutTag + ":" + tag; + + if (imageAlreadyInK3s(container, imageNameWithTag)) { + return; + } + String tarFileName = imageNameWithTag + ".tar"; Path ciTarPath = Paths.get(CI_IMAGE_TARS_DIR, tarFileName); @@ -81,6 +86,10 @@ public static void loadSpringCloudKubernetesImage(String imageNameWithoutTag, K3 public static void loadOrPullCommonTestImages(K3sContainer container, String tarName, String imageNameForDownload, String imageVersion) { + if (imageAlreadyInK3s(container, tarName)) { + return; + } + File dockerImagesRootDir = Paths.get(CI_IMAGE_TARS_DIR).toFile(); if (dockerImagesRootDir.exists() && dockerImagesRootDir.isDirectory()) { File[] tars = dockerImagesRootDir.listFiles(); From f31434b660cb059a0d588b79aca545342aa1f992 Mon Sep 17 00:00:00 2001 From: wind57 Date: Wed, 6 May 2026 16:23:45 +0300 Subject: [PATCH 7/7] trigger Signed-off-by: wind57