diff --git a/.github/workflows/linux-build-release.yml b/.github/workflows/linux-build-release.yml index c41f1466b..acb50b589 100644 --- a/.github/workflows/linux-build-release.yml +++ b/.github/workflows/linux-build-release.yml @@ -38,6 +38,12 @@ jobs: run: ./gradlew functionalTest - name: Documentation tests run: ./gradlew docTest + - name: Upload test reports + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: test-reports + path: build/reports/tests - name: Assemble artifacts run: ./gradlew assemble javadoc asciidoctorAllGuides - name: Upload binaries diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 657e39d05..395e27d66 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -23,5 +23,11 @@ jobs: run: ./gradlew test - name: Integration tests run: ./gradlew integrationTest + - name: Upload test reports + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: test-reports + path: build/reports/tests - name: Assemble artifacts run: ./gradlew assemble javadoc asciidoctorAllGuides diff --git a/src/functTest/groovy/com/bmuschko/gradle/docker/tasks/image/DockerRemoveImageFunctionalTest.groovy b/src/functTest/groovy/com/bmuschko/gradle/docker/tasks/image/DockerRemoveImageFunctionalTest.groovy index 65b4c49de..35cbee250 100644 --- a/src/functTest/groovy/com/bmuschko/gradle/docker/tasks/image/DockerRemoveImageFunctionalTest.groovy +++ b/src/functTest/groovy/com/bmuschko/gradle/docker/tasks/image/DockerRemoveImageFunctionalTest.groovy @@ -21,13 +21,13 @@ class DockerRemoveImageFunctionalTest extends AbstractGroovyDslFunctionalTest { dependsOn dockerfile inputDir = file("build/docker") } - + task removeImage(type: DockerRemoveImage) { dependsOn buildImage force = true targetImageId buildImage.getImageId() } - + task removeImageAndCheckRemoval(type: DockerListImages) { dependsOn removeImage showAll = true @@ -42,6 +42,75 @@ class DockerRemoveImageFunctionalTest extends AbstractGroovyDslFunctionalTest { !result.output.contains("repository") } + def "can remove multiple images"() { + // Given a few Official Images that are small... :-) + String firstImage = "alpine:3" + String secondImage = "busybox:1.36" + String thirdImage = "bash:5.2.21" + + // And a build script that uses them... + buildFile << """ + import com.bmuschko.gradle.docker.tasks.image.Dockerfile + import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage + import com.bmuschko.gradle.docker.tasks.image.DockerListImages + import com.bmuschko.gradle.docker.tasks.image.DockerRemoveImage + + task firstDockerfile(type: Dockerfile) { + from '$firstImage' + label(['maintainer': 'jane.doe@example.com']) + } + task buildFirstImage(type: DockerBuildImage) { + dependsOn firstDockerfile + inputDir = file("build/docker") + } + + task secondDockerfile(type: Dockerfile) { + from '$secondImage' + label(['maintainer': 'jane.doe@example.com']) + } + task buildSecondImage(type: DockerBuildImage) { + dependsOn secondDockerfile + inputDir = file("build/docker") + } + + task thirdDockerfile(type: Dockerfile) { + from '$thirdImage' + label(['maintainer': 'jane.doe@example.com']) + } + task buildThirdImage(type: DockerBuildImage) { + dependsOn thirdDockerfile + inputDir = file("build/docker") + } + + task removeImages(type: DockerRemoveImage) { + dependsOn buildFirstImage + dependsOn buildSecondImage + dependsOn buildThirdImage + + force = true + + images( + buildFirstImage.getImageId(), + buildSecondImage.getImageId(), + buildThirdImage.getImageId() + ) + } + + task removeImageAndCheckRemoval(type: DockerListImages) { + dependsOn removeImages + showAll = true + dangling = true + } + """ + + when: + BuildResult result = build('removeImageAndCheckRemoval') + + then: + !result.output.contains("repository") + } + + def "can remove image tagged in multiple repositories"() { buildFile << """ import com.bmuschko.gradle.docker.tasks.image.Dockerfile @@ -59,21 +128,21 @@ class DockerRemoveImageFunctionalTest extends AbstractGroovyDslFunctionalTest { dependsOn dockerfile inputDir = file("build/docker") } - + task tagImage(type: DockerTagImage) { dependsOn buildImage repository = "repository" tag = "tag2" targetImageId buildImage.getImageId() } - + task tagImageSecondTime(type: DockerTagImage) { dependsOn tagImage repository = "repository" tag = "tag2" targetImageId buildImage.getImageId() } - + task removeImage(type: DockerRemoveImage) { dependsOn tagImageSecondTime force = true diff --git a/src/main/java/com/bmuschko/gradle/docker/internal/DefaultDockerConfigResolver.java b/src/main/java/com/bmuschko/gradle/docker/internal/DefaultDockerConfigResolver.java index 16dd8d647..ad6eee01a 100644 --- a/src/main/java/com/bmuschko/gradle/docker/internal/DefaultDockerConfigResolver.java +++ b/src/main/java/com/bmuschko/gradle/docker/internal/DefaultDockerConfigResolver.java @@ -1,8 +1,5 @@ package com.bmuschko.gradle.docker.internal; -import org.gradle.api.logging.Logger; -import org.gradle.api.logging.Logging; - import javax.annotation.Nullable; import java.io.File; diff --git a/src/main/java/com/bmuschko/gradle/docker/tasks/image/DockerRemoveImage.java b/src/main/java/com/bmuschko/gradle/docker/tasks/image/DockerRemoveImage.java index 14e323c14..e940d2298 100644 --- a/src/main/java/com/bmuschko/gradle/docker/tasks/image/DockerRemoveImage.java +++ b/src/main/java/com/bmuschko/gradle/docker/tasks/image/DockerRemoveImage.java @@ -16,29 +16,151 @@ package com.bmuschko.gradle.docker.tasks.image; import com.github.dockerjava.api.command.RemoveImageCmd; +import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; + public class DockerRemoveImage extends DockerExistingImage { + /** + * Configures `--force` option when removing the images. + */ @Input @Optional public final Property getForce() { return force; } + /** + * Configures `--no-prune` option when removing the images. + */ + @Input + @Optional + public final Property getNoPrune() { + return noPrune; + } + private final Property force = getProject().getObjects().property(Boolean.class); + private final Property noPrune = getProject().getObjects().property(Boolean.class); + + private final ListProperty images = getProject().getObjects().listProperty(String.class); + + /** + * Sets the IDs or names of the images to be removed. + * + * @param images the names or IDs of the images. + * @see #images(ListProperty) + */ + public void images(List images) { + this.images.set(images); + } + + /** + * Sets the IDs or names of the images to be removed. + * + * @param images the names or IDs of the images. + * @see #images(ListProperty) + */ + public void images(String... images) { + this.images.set(Arrays.asList(images)); + } + + /** + * Sets the IDs or names of the images to be removed. + * + * @param images Image ID or name as {@link Callable} + * @see #images(String...) + * @see #images(ListProperty) + */ + public void images(Callable> images) { + // TODO: How to do this, and is it necessary? + // images(providers.provider(images)); + } + + /** + * Sets the IDs or names of the images to be removed. + * + * @param images Image ID or name as {@link Provider} + * @see #images(String...) + */ + public void images(ListProperty images) { + this.images.set(images); + } + + // Overriding methods to provide a warning message. + /** + * Sets the target image ID or name. + * + * @param imageId Image ID or name + * @see #targetImageId(Callable) + * @see #targetImageId(Provider) + */ + @Override + public void targetImageId(String imageId) { + logWarning(); + super.targetImageId(imageId); + } + + /** + * Sets the target image ID or name. + * + * @param imageId Image ID or name as Callable + * @see #targetImageId(String) + * @see #targetImageId(Provider) + */ + @Override + public void targetImageId(Callable imageId) { + logWarning(); + super.targetImageId(imageId); + } + + /** + * Sets the target image ID or name. + * + * @param imageId Image ID or name as Provider + * @see #targetImageId(String) + * @see #targetImageId(Callable) + */ + @Override + public void targetImageId(Provider imageId) { + logWarning(); + super.targetImageId(imageId); + } + + private void logWarning() { + getLogger().warn("Use property 'images' instead of 'targetImageId' when listing images to remove"); + } @Override public void runRemoteCommand() { - getLogger().quiet("Removing image with ID \'" + getImageId().get() + "\'."); - RemoveImageCmd removeImageCmd = getDockerClient().removeImageCmd(getImageId().get()); + java.util.Optional imageId = java.util.Optional.ofNullable(this.getImageId().getOrNull()); + List imagesIds = this.images.get(); - if (Boolean.TRUE.equals(force.getOrNull())) { - removeImageCmd.withForce(force.get()); + if (imageId.isPresent() && !imagesIds.isEmpty()) { + throw new IllegalStateException("Project sets both properties 'images' and 'targetImageId', but only one is allowed. Please use 'images'."); } - removeImageCmd.exec(); + imageId.ifPresent(this::removeImage); + imagesIds.forEach(this::removeImage); + } + + private void removeImage(String imageId) { + getLogger().quiet("Removing image with ID '{}'.", imageId); + try (RemoveImageCmd removeImageCmd = getDockerClient().removeImageCmd(imageId)) { + if (Boolean.TRUE.equals(force.getOrNull())) { + removeImageCmd.withForce(force.get()); + } + + if (Boolean.TRUE.equals(noPrune.getOrNull())) { + removeImageCmd.withNoPrune(noPrune.get()); + } + removeImageCmd.exec(); + } } }