From c2432c2301b6a4d0ab383d00c38faacd8eeb0eb2 Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Mon, 19 Feb 2024 15:08:03 -0500 Subject: [PATCH 1/9] Update to platform 0.12 --- .github/workflows/integration-tests.yml | 6 + README.md | 2 +- client/pom.xml | 4 + .../dev/snowdrop/buildpack/BuildConfig.java | 102 +++ .../dev/snowdrop/buildpack/BuilderImage.java | 123 ++++ .../dev/snowdrop/buildpack/Buildpack.java | 239 ------ .../snowdrop/buildpack/BuildpackBuild.java | 94 +++ .../dev/snowdrop/buildpack/TestDriver.java | 59 +- .../buildpack/config/CacheConfig.java | 30 + .../buildpack/config/DockerConfig.java | 63 ++ .../buildpack/config/ImageReference.java | 12 + .../snowdrop/buildpack/config/LogConfig.java | 44 ++ .../buildpack/config/PlatformConfig.java | 50 ++ .../buildpack/docker/BuildContainerUtils.java | 149 ++++ .../buildpack/docker/ContainerEntry.java | 2 +- .../buildpack/docker/ContainerUtils.java | 81 ++- .../buildpack/docker/FileContent.java | 4 + .../buildpack/docker/VolumeUtils.java | 28 +- .../ContainerStatus.java | 2 +- .../lifecycle/LifecycleExecutor.java | 148 ++++ .../{phases => lifecycle}/LifecyclePhase.java | 2 +- .../lifecycle/LifecyclePhaseFactory.java | 268 +++++++ .../snowdrop/buildpack/lifecycle/Version.java | 62 ++ .../buildpack/lifecycle/phases/Analyzer.java | 88 +++ .../buildpack/lifecycle/phases/Builder.java | 61 ++ .../buildpack/lifecycle/phases/Creator.java | 76 ++ .../buildpack/lifecycle/phases/Detector.java | 94 +++ .../buildpack/lifecycle/phases/Exporter.java | 81 +++ .../buildpack/lifecycle/phases/Extender.java | 72 ++ .../buildpack/lifecycle/phases/Restorer.java | 82 +++ .../snowdrop/buildpack/phases/Creator.java | 74 -- .../phases/LifecyclePhaseFactory.java | 176 ----- .../buildpack/utils/BuildpackMetadata.java | 50 +- .../snowdrop/buildpack/utils/JsonUtils.java | 25 +- .../buildpack/utils/LifecycleArgs.java | 58 ++ .../buildpack/utils/LifecycleMetadata.java | 61 ++ .../buildpack/config/CacheConfigTest.java | 29 + .../buildpack/config/DockerConfigTest.java | 80 ++ .../buildpack/config/ImageReferenceTest.java | 18 + .../buildpack/config/LogConfigTest.java | 42 ++ .../buildpack/config/PlatformConfigTest.java | 56 ++ .../buildpack/docker/ContainerUtilsTest.java | 2 +- .../buildpack/docker/ImageUtilsTest.java | 1 - .../buildpack/docker/VolumeUtilsTest.java | 35 +- .../lifecycle/ContainerStatusTest.java | 14 + .../lifecycle/LifecycleExecutorTest.java | 582 +++++++++++++++ .../lifecycle/phases/AnalzyerTest.java | 373 ++++++++++ .../lifecycle/phases/BuilderTest.java | 199 +++++ .../lifecycle/phases/CreatorTest.java | 373 ++++++++++ .../lifecycle/phases/DetectorTest.java | 681 ++++++++++++++++++ .../lifecycle/phases/ExporterTest.java | 534 ++++++++++++++ .../lifecycle/phases/ExtenderTest.java | 284 ++++++++ .../lifecycle/phases/RestorerTest.java | 308 ++++++++ .../utils/BuildpackMetadataTest.java | 71 ++ .../buildpack/utils/JsonUtilsTest.java | 50 ++ .../buildpack/utils/LifecycleArgsTest.java | 29 + pom.xml | 7 +- samples/hello-quarkus/pack.java | 12 +- samples/hello-spring/pack.java | 9 +- 59 files changed, 5788 insertions(+), 573 deletions(-) create mode 100644 client/src/main/java/dev/snowdrop/buildpack/BuildConfig.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/BuilderImage.java delete mode 100644 client/src/main/java/dev/snowdrop/buildpack/Buildpack.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/config/CacheConfig.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/config/DockerConfig.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/config/ImageReference.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/config/LogConfig.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/config/PlatformConfig.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java rename client/src/main/java/dev/snowdrop/buildpack/{phases => lifecycle}/ContainerStatus.java (81%) create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java rename client/src/main/java/dev/snowdrop/buildpack/{phases => lifecycle}/LifecyclePhase.java (78%) create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/Version.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Builder.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Extender.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java delete mode 100644 client/src/main/java/dev/snowdrop/buildpack/phases/Creator.java delete mode 100644 client/src/main/java/dev/snowdrop/buildpack/phases/LifecyclePhaseFactory.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/utils/LifecycleArgs.java create mode 100644 client/src/main/java/dev/snowdrop/buildpack/utils/LifecycleMetadata.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/config/CacheConfigTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/config/DockerConfigTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/config/ImageReferenceTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/config/LogConfigTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/config/PlatformConfigTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/lifecycle/ContainerStatusTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutorTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/AnalzyerTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/BuilderTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/CreatorTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/DetectorTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExporterTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExtenderTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/RestorerTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/utils/BuildpackMetadataTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/utils/JsonUtilsTest.java create mode 100644 client/src/test/java/dev/snowdrop/buildpack/utils/LifecycleArgsTest.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 3c4a75f..36bc8d1 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -27,6 +27,7 @@ on: branches: - main pull_request: + workflow_dispatch: jobs: integration-tests: @@ -39,27 +40,32 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2.3.4 + - name: Cache .m2 registry uses: actions/cache@v2.1.5 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}-${{ github.sha }} restore-keys: ${{ runner.os }}-maven- + - name: Setup Java uses: actions/setup-java@v2 with: java-version: ${{ matrix.java }} distribution: 'adopt' + - name: Setup sdkman run: | curl -s "https://get.sdkman.io" | bash source "$HOME/.sdkman/bin/sdkman-init.sh" sdkman_auto_answer=false sdkman_selfupdate_enable=false + - name: Setup jbang run: | sdk install jbang ${{matrix.jbang}} sdk default jbang ${{matrix.jbang}} + - name: Integration Tests run: | cd samples/hello-spring diff --git a/README.md b/README.md index 91d056e..2c5e6f4 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Buildpack.builder() .build(); ``` -This will use the default builder image from (https://packeto.io) to handle the build +This will use the default builder image from (https://paketo.io) to handle the build of the project in the `/home/user/java-project` folder. The resulting image will be stored in the local docker daemon as `test/testimage:latest`. diff --git a/client/pom.xml b/client/pom.xml index 32bd4e2..23b2d39 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -26,6 +26,10 @@ com.github.docker-java docker-java-transport-httpclient5 + + org.tomlj + tomlj + org.apache.commons commons-compress diff --git a/client/src/main/java/dev/snowdrop/buildpack/BuildConfig.java b/client/src/main/java/dev/snowdrop/buildpack/BuildConfig.java new file mode 100644 index 0000000..e6fe9c6 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/BuildConfig.java @@ -0,0 +1,102 @@ +package dev.snowdrop.buildpack; + +import java.util.ArrayList; +import java.util.List; + +import dev.snowdrop.buildpack.config.CacheConfig; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.config.PlatformConfig; +import dev.snowdrop.buildpack.docker.Content; +import io.sundr.builder.annotations.Buildable; + + +@Buildable(generateBuilderPackage=true, builderPackage="dev.snowdrop.buildpack.builder") +public class BuildConfig { + public static BuildConfigBuilder builder() { + return new BuildConfigBuilder(); + } + + private static final ImageReference DEFAULT_BUILDER_IMAGE = new ImageReference("paketobuildpacks/builder:base"); + + private DockerConfig dockerConfig; + private CacheConfig buildCacheConfig; + private CacheConfig launchCacheConfig; + private CacheConfig kanikoCacheConfig; + private PlatformConfig platformConfig; + private LogConfig logConfig; + private ImageReference builderImage; + private ImageReference runImage; + private ImageReference outputImage; + private List application; + + private final int exitCode; + + public BuildConfig(DockerConfig dockerConfig, + CacheConfig buildCacheConfig, + CacheConfig launchCacheConfig, + CacheConfig kanikoCacheConfig, + PlatformConfig platformConfig, + LogConfig logConfig, + ImageReference builderImage, + ImageReference runImage, + ImageReference outputImage, + List application){ + this.dockerConfig = dockerConfig != null ? dockerConfig : DockerConfig.builder().build(); + this.buildCacheConfig = buildCacheConfig != null ? buildCacheConfig : CacheConfig.builder().build(); + this.launchCacheConfig = launchCacheConfig != null ? launchCacheConfig : CacheConfig.builder().build(); + this.kanikoCacheConfig = kanikoCacheConfig != null ? kanikoCacheConfig : CacheConfig.builder().build(); + this.platformConfig = platformConfig != null ? platformConfig : PlatformConfig.builder().build(); + this.logConfig = logConfig != null ? logConfig : LogConfig.builder().build(); + this.builderImage = builderImage != null ? builderImage : DEFAULT_BUILDER_IMAGE; + this.runImage = runImage; + this.outputImage = outputImage; + this.application = application != null ? application : new ArrayList<>(); + + if(this.outputImage==null){ + throw new BuildpackException("Output Image missing and must be specified", new IllegalArgumentException()); + } + if(this.application.size()==0){ + throw new BuildpackException("Application content missing and must be specified", new IllegalArgumentException()); + } + + exitCode = new BuildpackBuild(this).build(); + } + + public DockerConfig getDockerConfig(){ + return dockerConfig; + } + public CacheConfig getBuildCacheConfig(){ + return buildCacheConfig; + } + public CacheConfig getLaunchCacheConfig(){ + return launchCacheConfig; + } + public CacheConfig getKanikoCacheConfig(){ + return kanikoCacheConfig; + } + public PlatformConfig getPlatformConfig(){ + return platformConfig; + } + public LogConfig getLogConfig(){ + return logConfig; + } + public ImageReference getBuilderImage(){ + return builderImage; + } + public ImageReference getRunImage(){ + return runImage; + } + public ImageReference getOutputImage(){ + return outputImage; + } + public List getApplication(){ + return application; + } + public int getExitCode() { + return this.exitCode; + } +} + + diff --git a/client/src/main/java/dev/snowdrop/buildpack/BuilderImage.java b/client/src/main/java/dev/snowdrop/buildpack/BuilderImage.java new file mode 100644 index 0000000..edcd00d --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/BuilderImage.java @@ -0,0 +1,123 @@ +package dev.snowdrop.buildpack; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.PlatformConfig; +import dev.snowdrop.buildpack.docker.ImageUtils; +import dev.snowdrop.buildpack.docker.ImageUtils.ImageInfo; +import dev.snowdrop.buildpack.lifecycle.Version; +import dev.snowdrop.buildpack.utils.BuildpackMetadata; + +public class BuilderImage { + + private int DEFAULT_USER_ID = 1000; + private int DEFAULT_GROUP_ID = 1000; + + private ImageReference image; + + private int userId; + private int groupId; + + private boolean hasExtensions; + + private String metadataJson; + private ImageReference runImage; + private List runImages; + + private List builderSupportedPlatforms; + + public BuilderImage(BuilderImage original, boolean addedExtensions, ImageReference extended) { + this.userId = original.userId; + this.groupId = original.groupId; + this.runImages = original.runImages; + this.builderSupportedPlatforms = original.builderSupportedPlatforms; + this.hasExtensions = original.hasExtensions || addedExtensions; + System.out.println("Extended builder image hasExtensions? "+hasExtensions+" orig?"+original.hasExtensions+" add?"+addedExtensions); + this.image = extended; + } + + public BuilderImage(DockerConfig dc, PlatformConfig pc, ImageReference runImage, ImageReference builderImage){ + image = builderImage; + + // pull and inspect the builderImage to obtain builder metadata. + ImageUtils.pullImages(dc.getDockerClient(), + dc.getPullTimeout(), + builderImage.getReference()); + + ImageInfo ii = ImageUtils.inspectImage(dc.getDockerClient(), + builderImage.getReference()); + + // read the userid/groupid for the buildpack from it's env. + userId = DEFAULT_USER_ID; + groupId = DEFAULT_GROUP_ID; + for (String s : ii.env) { + if (s.startsWith("CNB_USER_ID=")) { + userId = Integer.valueOf(s.substring("CNB_USER_ID=".length())); + } + if (s.startsWith("CNB_GROUP_ID=")) { + groupId = Integer.valueOf(s.substring("CNB_GROUP_ID=".length())); + } + } + // override userid/groupid if cnb vars present within environment + if(pc.getEnvironment().containsKey("CNB_USER_ID")) { + userId = Integer.valueOf(pc.getEnvironment().get("CNB_USER_ID")); + } + if(pc.getEnvironment().containsKey("CNB_GROUP_ID")) { + userId = Integer.valueOf(pc.getEnvironment().get("CNB_GROUP_ID")); + } + + String xtnLayers = ii.labels.get("io.buildpacks.extension.layers"); + System.out.println("Builder image got xtnLayers label "+xtnLayers); + hasExtensions = (xtnLayers!=null && !xtnLayers.isEmpty()); + System.out.println("BuilderImage hasExtensions? "+hasExtensions); + + //defer the calculation of run images to the getter, because we + //need to know the selected platform level to know how to find the run metadata. + metadataJson = ii.labels.get("io.buildpacks.builder.metadata"); + this.runImage = runImage; + runImages = null; + + builderSupportedPlatforms = BuildpackMetadata.getSupportedPlatformsFromMetadata(metadataJson); + } + + public ImageReference getImage(){ + return image; + } + public int getUserId() { + return userId; + } + public int getGroupId() { + return groupId; + } + public boolean hasExtensions() { + return hasExtensions; + } + public List getRunImages(Version activePlatformLevel) { + if(runImages==null){ + runImages = new ArrayList<>(); + if(runImage!=null){ + // use user specified run image + runImages.add(runImage.getReference()); + }else{ + Set runSet = new HashSet<>(); + if(activePlatformLevel.atLeast("0.12")){ + runSet.addAll(BuildpackMetadata.getRunImageFromRunTOML(image.getReference())); + }else{ + // obtain the buildpack metadata json identified runImage + runSet.add(BuildpackMetadata.getRunImageFromMetadataJSON(metadataJson)); + } + runImages.addAll(runSet); + } + } + return runImages; + } + public List getBuilderSupportedPlatforms() { + return builderSupportedPlatforms; + } + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/Buildpack.java b/client/src/main/java/dev/snowdrop/buildpack/Buildpack.java deleted file mode 100644 index 14bc1f7..0000000 --- a/client/src/main/java/dev/snowdrop/buildpack/Buildpack.java +++ /dev/null @@ -1,239 +0,0 @@ -package dev.snowdrop.buildpack; - -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import com.github.dockerjava.api.DockerClient; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import dev.snowdrop.buildpack.docker.ContainerUtils; -import dev.snowdrop.buildpack.docker.Content; -import dev.snowdrop.buildpack.docker.DockerClientUtils; -import dev.snowdrop.buildpack.docker.ImageUtils; -import dev.snowdrop.buildpack.docker.ImageUtils.ImageInfo; -import dev.snowdrop.buildpack.phases.ContainerStatus; -import dev.snowdrop.buildpack.phases.LifecyclePhase; -import dev.snowdrop.buildpack.phases.LifecyclePhaseFactory; -import dev.snowdrop.buildpack.utils.BuildpackMetadata; -import io.sundr.builder.annotations.Buildable; - - -@Buildable(generateBuilderPackage=true, builderPackage="dev.snowdrop.buildpack.builder") -public class Buildpack { - - public static BuildpackBuilder builder() { - return new BuildpackBuilder(); - } - - private static final Logger log = LoggerFactory.getLogger(Buildpack.class); - - private static final String DEFAULT_BUILD_IMAGE = "paketobuildpacks/builder:base"; - private static final Integer DEFAULT_PULL_TIMEOUT = 60; - private static final String DEFAULT_LOG_LEVEL = "debug"; - - // defaults for images - private final String builderImage; - private final String finalImage; - private String runImage; - - private final Integer pullTimeoutSeconds; - - private final String dockerHost; - - private final boolean useDaemon; - private final String buildCacheVolumeName; - private final boolean removeBuildCacheAfterBuild; - private final String launchCacheVolumeName; - private final boolean removeLaunchCacheAfterBuild; - - private final String logLevel; - private final boolean useTimestamps; - - private Integer userId; - private Integer groupId; - - Map environment = new HashMap<>(); - private List content = new LinkedList<>(); - private final DockerClient dockerClient; - private final dev.snowdrop.buildpack.Logger logger; - - private final int exitCode; - - private final String dockerSocket; - - public Buildpack(String builderImage, String runImage, String finalImage, Integer pullTimeoutSeconds, String dockerHost, String dockerSocket, - boolean useDaemon, String buildCacheVolumeName, boolean removeBuildCacheAfterBuild, - String launchCacheVolumeName, boolean removeLaunchCacheAfterBuild, String logLevel, boolean useTimestamps, Map environment, List content, DockerClient dockerClient, dev.snowdrop.buildpack.Logger logger) { - - this.builderImage = builderImage != null ? builderImage : DEFAULT_BUILD_IMAGE; - this.runImage = runImage; - this.finalImage = finalImage; - this.pullTimeoutSeconds = pullTimeoutSeconds != null ? pullTimeoutSeconds : DEFAULT_PULL_TIMEOUT; - this.dockerHost = dockerHost != null ? dockerHost : DockerClientUtils.getDockerHost(); - this.useDaemon = useDaemon; - this.buildCacheVolumeName = buildCacheVolumeName; - this.removeBuildCacheAfterBuild = removeBuildCacheAfterBuild || buildCacheVolumeName!=null; - this.launchCacheVolumeName = launchCacheVolumeName; - this.removeLaunchCacheAfterBuild = removeLaunchCacheAfterBuild || launchCacheVolumeName!=null; - this.logLevel = logLevel != null ? logLevel : DEFAULT_LOG_LEVEL; - this.useTimestamps = useTimestamps; - this.environment = environment != null ? environment : new HashMap<>(); - this.content = content; - this.dockerClient = DockerClientUtils.getDockerClient(dockerHost); - this.logger = logger != null ? logger : new SystemLogger(); - // We still only support docker daemon execution, and if - // dockerHost is configured, then test if it is a unix:// path - // and set dockerSocket path appropriately. - this.dockerSocket = dockerSocket != null ? dockerSocket : (this.dockerHost.startsWith("unix://") ? this.dockerHost.substring("unix://".length()) : "/var/run/docker.sock"); - - - //run the build. - this.exitCode = build(logger); - } - - private int build(dev.snowdrop.buildpack.Logger logger) { - - log.info("Buildpack build invoked, preparing environment..."); - prep(); - - LifecyclePhaseFactory lifecycle = new LifecyclePhaseFactory(dockerClient, userId, groupId, this); - - //create and run the creator phase - LifecyclePhase creator = lifecycle.getCreator(); - ContainerStatus cs = creator.runPhase(logger, useTimestamps); - - log.info("Buildpack build complete, cleaning up..."); - ContainerUtils.removeContainer(dockerClient, cs.getContainerId()); - lifecycle.tidyUp(); - - return cs.getRc(); - } - - /** - * Prep for a build.. this should pull the builder/runImage, and configure - * the uid/gid to be used for the build. - */ - private void prep() { - - // pull and inspect the builderImage to obtain builder metadata. - ImageUtils.pullImages(dockerClient, pullTimeoutSeconds, builderImage); - ImageInfo ii = ImageUtils.inspectImage(dockerClient, builderImage); - - // read the userid/groupid for the buildpack from it's env. - for (String s : ii.env) { - if (s.startsWith("CNB_USER_ID=")) { - userId = Integer.valueOf(s.substring("CNB_USER_ID=".length())); - } - if (s.startsWith("CNB_GROUP_ID=")) { - groupId = Integer.valueOf(s.substring("CNB_GROUP_ID=".length())); - } - } - // override userid/groupid if cnb vars present within environment - if(environment.containsKey("CNB_USER_ID")) { userId = Integer.valueOf(environment.get("CNB_USER_ID")); } - if(environment.containsKey("CNB_GROUP_ID")) { userId = Integer.valueOf(environment.get("CNB_GROUP_ID")); } - - // obtain the buildpack metadata json. - String metadataJson = ii.labels.get("io.buildpacks.builder.metadata"); - if(runImage==null) - runImage = BuildpackMetadata.getRunImageFromMetadata(metadataJson); - - // TODO: read metadata from builderImage to confirm lifecycle version/platform - // version compatibility. - - // pull the runImage, so it will be available for the build. - ImageUtils.pullImages(dockerClient, pullTimeoutSeconds, runImage); - - // log out current config. - log.info("Build configured with.."); - log.info("- build image : "+builderImage); - log.info("- run image : "+runImage); - log.info("- uid:"+userId+" gid:"+groupId); - log.info("- dockerHost:"+dockerHost); - log.info("- dockerSocket:"+dockerSocket); - } - - public String getBuilderImage() { - return builderImage; - } - - public String getDockerSocket() { - return dockerSocket; - } - - public String getRunImage() { - return runImage; - } - - public String getFinalImage() { - return finalImage; - } - - public Integer getPullTimeoutSeconds() { - return pullTimeoutSeconds; - } - - public String getDockerHost() { - return dockerHost; - } - - public boolean getUseDaemon() { - return useDaemon; - } - - public String getBuildCacheVolumeName() { - return buildCacheVolumeName; - } - - public boolean getRemoveBuildCacheAfterBuild() { - return removeBuildCacheAfterBuild; - } - - public String getLaunchCacheVolumeName() { - return launchCacheVolumeName; - } - - public boolean getRemoveLaunchCacheAfterBuild() { - return removeLaunchCacheAfterBuild; - } - - public String getLogLevel() { - return logLevel; - } - - public boolean getUseTimestamps() { - return useTimestamps; - } - - public Map getEnvironment() { - return environment; - } - - public void setEnvironment(Map environment) { - this.environment = environment; - } - - public List getContent() { - return content; - } - - public void setContent(List content) { - this.content = content; - } - - public DockerClient getDockerClient() { - return dockerClient; - } - - public dev.snowdrop.buildpack.Logger getLogger() { - return logger; - } - - public int getExitCode() { - return this.exitCode; - } - -} diff --git a/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java b/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java new file mode 100644 index 0000000..7b97360 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java @@ -0,0 +1,94 @@ +package dev.snowdrop.buildpack; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.PlatformConfig; +import dev.snowdrop.buildpack.docker.BuildContainerUtils; +import dev.snowdrop.buildpack.lifecycle.LifecycleExecutor; +import dev.snowdrop.buildpack.utils.LifecycleMetadata; + +public class BuildpackBuild { + + //platforms stored in order of preference. + private final List supportedPlatformLevels =Stream.of("0.12", "0.11", "0.10", "0.9", "0.8", "0.7", "0.6", "0.5", "0.4").collect(Collectors.toList()); + //default platform level, used if not overridden + public final String DEFAULT_PLATFORM_LEVEL = supportedPlatformLevels.get(0); + + BuildConfig config; + + public BuildpackBuild(BuildConfig config){ + this.config = config; + } + + private String selectPlatformLevel(DockerConfig dc, PlatformConfig pc, BuilderImage builder) { + List platformsToConsider; + + if(pc.getLifecycleImage() == null){ + //using lifecycle from builder, so use builder platforms + platformsToConsider = builder.getBuilderSupportedPlatforms(); + }else{ + //using specified lifecycle, so use lifecycle platforms + LifecycleMetadata lm = new LifecycleMetadata(dc, pc.getLifecycleImage()); + platformsToConsider = lm.getSupportedPlatformLevels(); + } + + //if platform config requests specific platform, filter to only that platform level + List platformsToTest = new ArrayList<>(supportedPlatformLevels); + if(pc.getPlatformLevel()!=null && supportedPlatformLevels.contains(pc.getPlatformLevel())){ + platformsToTest.clear(); + platformsToTest.add(pc.getPlatformLevel()); + } + + //find & return first match. + for(String platform: platformsToTest){ + if(platformsToConsider.contains(platform)){ + return platform; + } + } + + //no match? appropriate exception. + if(pc.getLifecycleImage()!=null){ + throw new BuildpackException("Unable to determine compatible platform for supplied lifecycle image", new IllegalStateException()); + }else{ + throw new BuildpackException("Unable to determine compatible platform for builder lifecycle image", new IllegalStateException()); + } + + } + + public int build(){ + + //obtain & pull & inspect Builder image. + BuilderImage builder = new BuilderImage(config.getDockerConfig(), + config.getPlatformConfig(), + config.getRunImage(), + config.getBuilderImage()); + + //select active platform level. + String activePlatformLevel = selectPlatformLevel(config.getDockerConfig(), + config.getPlatformConfig(), + builder); + + System.out.println("Selected platform level "+activePlatformLevel); + + //create the extended builder image. + BuilderImage extendedBuilder = BuildContainerUtils.createBuildImage(config.getDockerConfig().getDockerClient(), + builder, + null, + null, + null); + try{ + // execute the build using the lifecycle. + LifecycleExecutor le = new LifecycleExecutor(config, builder, extendedBuilder, activePlatformLevel); + return le.execute(); + }finally{ + //clean up the extended builder image + config.getDockerConfig().getDockerClient().removeImageCmd(extendedBuilder.getImage().getReference()).exec(); + } + } + + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/TestDriver.java b/client/src/main/java/dev/snowdrop/buildpack/TestDriver.java index 8126c1c..8cb8649 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/TestDriver.java +++ b/client/src/main/java/dev/snowdrop/buildpack/TestDriver.java @@ -1,21 +1,54 @@ package dev.snowdrop.buildpack; import java.io.File; +import java.util.HashMap; -public class TestDriver { +import dev.snowdrop.buildpack.config.ImageReference; - public TestDriver() throws Exception { - Buildpack.builder() - .addNewFileContent() - .withFile(new File("/home/ozzy/Work/java-buildpack-client")) - .endFileContent() - .withFinalImage("test/testimage:latest") - .withLogLevel("debug") - .build(); - } +public class TestDriver { public static void main(String[] args) throws Exception { - @SuppressWarnings("unused") - TestDriver td = new TestDriver(); + + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack","debug"); + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack.docker","debug"); + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack.lifecycle","debug"); + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack.lifecycle.phases","debug"); + + HashMap env = new HashMap<>(); + + + int exitCode = + BuildConfig.builder().withNewDockerConfig() + .withUseDaemon(false) + .withDockerNetwork("host") + .and() + .withNewLogConfig() + .withLogger(new SystemLogger()) + .withLogLevel("debug") + .and() + .withNewKanikoCacheConfig() + //.withCacheVolumeName("mykanikocache") + .withDeleteCacheAfterBuild(true) + .and() + .withNewBuildCacheConfig() + //.withCacheVolumeName("buildcachevol") + .withDeleteCacheAfterBuild(true) + .and() + .withNewLaunchCacheConfig() + //.withCacheVolumeName("launchcachevol") + .withDeleteCacheAfterBuild(true) + .and() + .withNewPlatformConfig() + .withEnvironment(env) + .withTrustBuilder(false) + .and() + .withBuilderImage(new ImageReference("paketocommunity/builder-ubi-base")) + .withOutputImage(new ImageReference("localhost:5000/testdriver/newimage6:latest")) + .addNewFileContentApplication(new File("/tmp/sample-springboot-java-app/")) + .build() + .getExitCode(); + + System.out.println("Build for completed with exit code "+exitCode); + } -} +} \ No newline at end of file diff --git a/client/src/main/java/dev/snowdrop/buildpack/config/CacheConfig.java b/client/src/main/java/dev/snowdrop/buildpack/config/CacheConfig.java new file mode 100644 index 0000000..9fd8260 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/config/CacheConfig.java @@ -0,0 +1,30 @@ +package dev.snowdrop.buildpack.config; + +import io.sundr.builder.annotations.Buildable; + +@Buildable(generateBuilderPackage=true, builderPackage="dev.snowdrop.buildpack.builder") +public class CacheConfig { + + private static Boolean DEFAULT_DELETE_CACHE = Boolean.TRUE; + + public static CacheConfigBuilder builder() { + return new CacheConfigBuilder(); + } + + private String cacheVolumeName; + private Boolean deleteCacheAfterBuild; + + public CacheConfig(String cacheVolumeName, + Boolean deleteCacheAfterBuild){ + this.cacheVolumeName = cacheVolumeName; + this.deleteCacheAfterBuild = deleteCacheAfterBuild!=null ? deleteCacheAfterBuild : DEFAULT_DELETE_CACHE; //default if not set + } + + public String getCacheVolumeName(){ + return this.cacheVolumeName; + } + + public Boolean getDeleteCacheAfterBuild(){ + return this.deleteCacheAfterBuild; + } +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/config/DockerConfig.java b/client/src/main/java/dev/snowdrop/buildpack/config/DockerConfig.java new file mode 100644 index 0000000..eb6d04c --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/config/DockerConfig.java @@ -0,0 +1,63 @@ +package dev.snowdrop.buildpack.config; + +import com.github.dockerjava.api.DockerClient; + +import dev.snowdrop.buildpack.docker.DockerClientUtils; +import io.sundr.builder.annotations.Buildable; + +@Buildable(generateBuilderPackage=true, builderPackage="dev.snowdrop.buildpack.builder") +public class DockerConfig { + public static DockerConfigBuilder builder() { + return new DockerConfigBuilder(); + } + + private static final Integer DEFAULT_PULL_TIMEOUT = 60; + + private Integer pullTimeoutSeconds; + private String dockerHost; + private String dockerSocket; + private String dockerNetwork; + private Boolean useDaemon; + private DockerClient dockerClient; + + public DockerConfig( + Integer pullTimeoutSeconds, + String dockerHost, + String dockerSocket, + String dockerNetwork, + Boolean useDaemon, + DockerClient dockerClient + ){ + this.pullTimeoutSeconds = pullTimeoutSeconds != null ? pullTimeoutSeconds : DEFAULT_PULL_TIMEOUT; + this.dockerHost = dockerHost != null ? dockerHost : DockerClientUtils.getDockerHost(); + this.dockerSocket = dockerSocket != null ? dockerSocket : (this.dockerHost.startsWith("unix://") ? this.dockerHost.substring("unix://".length()) : "/var/run/docker.sock"); + this.dockerNetwork = dockerNetwork; + this.useDaemon = useDaemon != null ? useDaemon : Boolean.TRUE; //default daemon to true for back compat. + this.dockerClient = dockerClient != null ? dockerClient : DockerClientUtils.getDockerClient(this.dockerHost); + } + + public Integer getPullTimeout(){ + return this.pullTimeoutSeconds; + } + + public String getDockerHost(){ + return this.dockerHost; + } + + public String getDockerSocket(){ + return this.dockerSocket; + } + + public String getDockerNetwork(){ + return this.dockerNetwork; + } + + public DockerClient getDockerClient(){ + return this.dockerClient; + } + + public Boolean getUseDaemon(){ + return this.useDaemon; + } +} + diff --git a/client/src/main/java/dev/snowdrop/buildpack/config/ImageReference.java b/client/src/main/java/dev/snowdrop/buildpack/config/ImageReference.java new file mode 100644 index 0000000..273cccb --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/config/ImageReference.java @@ -0,0 +1,12 @@ +package dev.snowdrop.buildpack.config; + +public class ImageReference { + private String refString; + public ImageReference(String reference){ + this.refString = reference; + } + + public String getReference(){ + return this.refString; + } +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/config/LogConfig.java b/client/src/main/java/dev/snowdrop/buildpack/config/LogConfig.java new file mode 100644 index 0000000..3ba8b20 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/config/LogConfig.java @@ -0,0 +1,44 @@ +package dev.snowdrop.buildpack.config; + +import dev.snowdrop.buildpack.Logger; +import dev.snowdrop.buildpack.SystemLogger; +import io.sundr.builder.annotations.Buildable; + +@Buildable(generateBuilderPackage=true, builderPackage="dev.snowdrop.buildpack.builder") +public class LogConfig { + + private String DEFAULT_LOG_LEVEL="info"; + private Boolean DEFAULT_USE_TIMESTAMPS=Boolean.TRUE; + + public static LogConfigBuilder builder() { + return new LogConfigBuilder(); + } + + private String logLevel; + private Boolean useTimestamps; + private Logger logger; + + + public LogConfig( + String logLevel, + Boolean useTimestamps, + Logger logger + ){ + this.logLevel = logLevel!=null ? logLevel : DEFAULT_LOG_LEVEL; + this.useTimestamps = useTimestamps!=null ? useTimestamps : DEFAULT_USE_TIMESTAMPS; + this.logger = logger!=null ? logger : new SystemLogger(); + } + + public String getLogLevel(){ + return logLevel; + } + + public Logger getLogger(){ + return logger; + } + + public Boolean getUseTimestamps(){ + return useTimestamps; + } + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/config/PlatformConfig.java b/client/src/main/java/dev/snowdrop/buildpack/config/PlatformConfig.java new file mode 100644 index 0000000..1812fb4 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/config/PlatformConfig.java @@ -0,0 +1,50 @@ +package dev.snowdrop.buildpack.config; + +import java.util.HashMap; +import java.util.Map; + +import io.sundr.builder.annotations.Buildable; + +@Buildable(generateBuilderPackage=true, builderPackage="dev.snowdrop.buildpack.builder") +public class PlatformConfig { + + public static PlatformConfigBuilder builder() { + return new PlatformConfigBuilder(); + } + + private String DEFAULT_PLATFORM_LEVEL="0.10"; + + private String platformLevel; + private Map environment; + private ImageReference lifecycleImage; + private Boolean trustBuilder; //use creator when possible. + + public PlatformConfig( + String platformLevel, + ImageReference lifecycleImage, + Map environment, + Boolean trustBuilder){ + this.platformLevel = platformLevel!=null ? platformLevel : DEFAULT_PLATFORM_LEVEL; + this.environment = environment!=null ? environment : new HashMap<>(); + this.lifecycleImage = lifecycleImage; + this.trustBuilder = trustBuilder; + } + + public String getPlatformLevel(){ + return platformLevel; + } + + public Map getEnvironment(){ + return environment; + } + + public ImageReference getLifecycleImage(){ + return lifecycleImage; + } + + public Boolean getTrustBuilder(){ + return trustBuilder; + } + + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java new file mode 100644 index 0000000..2b17a83 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java @@ -0,0 +1,149 @@ +package dev.snowdrop.buildpack.docker; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.GZIPOutputStream; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; +import com.github.dockerjava.api.exception.NotFoundException; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.BuildpackException; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; + +//used to create ephemeral builder, streaming tarball content from one image to another. +public class BuildContainerUtils { + + private static InputStream getArchiveStreamFromContainer(DockerClient dc, String containerId, String path){ + CopyArchiveFromContainerCmd copyLifecyleFromImageCmd = dc.copyArchiveFromContainerCmd(containerId, path); + try{ + return copyLifecyleFromImageCmd.exec(); + }catch(NotFoundException nfe){ + throw BuildpackException.launderThrowable("Unable to locate container '"+containerId+"'", nfe); + } + } + + private static void putArchiveStreamToContainer(DockerClient dc, String containerId, String atPath, InputStream tarStream){ + dc.copyArchiveToContainerCmd(containerId).withTarInputStream(tarStream).withRemotePath(atPath).exec(); + } + + private static void processBuildModule(DockerClient dc, String targetContainerId, String moduleImageReference, String fromPath, String toPath){ + String containerId = null; + List command = Stream.of("").collect(Collectors.toList()); + try{ + containerId = ContainerUtils.createContainer(dc, moduleImageReference, command); + InputStream stream = getArchiveStreamFromContainer(dc, containerId, fromPath); + putArchiveStreamToContainer(dc, targetContainerId, toPath, stream); + }finally{ + if(containerId!=null){ + ContainerUtils.removeContainer(dc, containerId); + } + } + } + + private static void populateMountPointDirs(DockerClient dc, String targetContainerId, int uid, int gid, List dirs){ + try (PipedInputStream in = new PipedInputStream(4096); PipedOutputStream out = new PipedOutputStream(in)) { + AtomicReference writerException = new AtomicReference<>(); + + Runnable writer = new Runnable() { + @Override + public void run() { + try (TarArchiveOutputStream tout = new TarArchiveOutputStream(new GZIPOutputStream(new BufferedOutputStream(out)));) { + tout.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + for (String dir : dirs) { + TarArchiveEntry tae = new TarArchiveEntry(dir + "/"); + tae.setSize(0); + tae.setUserId(uid); + tae.setGroupId(gid); + tout.putArchiveEntry(tae); + tout.closeArchiveEntry(); + } + } catch (Exception e) { + writerException.set(e); + } + } + }; + + Runnable reader = new Runnable() { + @Override + public void run() { + dc.copyArchiveToContainerCmd(targetContainerId).withRemotePath("/").withTarInputStream(in).exec(); + } + }; + + Thread t1 = new Thread(writer); + Thread t2 = new Thread(reader); + t1.start(); + t2.start(); + try { + t1.join(); + t2.join(); + } catch (InterruptedException ie) { + throw BuildpackException.launderThrowable(ie); + } + + // did the write thread complete without issues? if not, bubble the cause. + Exception wio = writerException.get(); + if (wio != null) { + throw BuildpackException.launderThrowable(wio); + } + } catch (IOException e) { + throw BuildpackException.launderThrowable(e); + } + } + + + /** + * Creates a build image using a builder image as the base, overlaying lifecycle/extensions/buildpack content + * from supplied images. + * + * @param dc Dockerclient to use + * @param baseBuilder ImageReference for builder imager to start with + * @param lifecycle ImageReference for lifecycle image to take lifecycle from + * @param extensions List of ImageReferences to take extensions from + * @param buildpacks List of ImageReferences to take buildpacks from + * @return + */ + public static BuilderImage createBuildImage(DockerClient dc, BuilderImage baseBuilder, ImageReference lifecycle, List extensions, List buildpacks) { + + List command = Stream.of("").collect(Collectors.toList()); + String builderContainerId = ContainerUtils.createContainer(dc, baseBuilder.getImage().getReference(), command); + + if(lifecycle!=null) + processBuildModule(dc, builderContainerId, lifecycle.getReference(), "/cnb/lifecycle", "/cnb"); + + if(extensions!=null) + for(ImageReference extension: extensions) + processBuildModule(dc, builderContainerId, extension.getReference(), "/cnb/extensions", "/cnb"); + + if(buildpacks!=null) + for(ImageReference buildpack: buildpacks) + processBuildModule(dc, builderContainerId, buildpack.getReference(), "/cnb/buildpacks", "/cnb"); + + populateMountPointDirs(dc, builderContainerId, baseBuilder.getUserId(), baseBuilder.getGroupId(), + Stream.of(LifecyclePhaseFactory.KANIKO_VOL_PATH, + LifecyclePhaseFactory.WORKSPACE_VOL_PATH, + LifecyclePhaseFactory.LAYERS_VOL_PATH, + LifecyclePhaseFactory.CACHE_VOL_PATH, + LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH, + LifecyclePhaseFactory.PLATFORM_VOL_PATH, + LifecyclePhaseFactory.PLATFORM_VOL_PATH+LifecyclePhaseFactory.ENV_PATH_PREFIX) + .collect(Collectors.toList())); + + return new BuilderImage(baseBuilder, + (extensions!=null && !extensions.isEmpty()), + new ImageReference(ContainerUtils.commitContainer(dc, builderContainerId))); + } +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java b/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java index 858d1fe..df60d3a 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java @@ -9,7 +9,7 @@ */ public interface ContainerEntry { - + public String getPath(); public long getSize(); diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerUtils.java index 9146288..384d1fc 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerUtils.java @@ -1,30 +1,39 @@ package dev.snowdrop.buildpack.docker; import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.zip.GZIPOutputStream; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; -import dev.snowdrop.buildpack.docker.ContainerEntry.DataSupplier; import dev.snowdrop.buildpack.BuildpackException; +import dev.snowdrop.buildpack.docker.ContainerEntry.DataSupplier; public class ContainerUtils { @@ -41,14 +50,32 @@ private static Bind createBind(VolumeBind vb) { public static String createContainer(DockerClient dc, String imageReference, List command, VolumeBind... volumes) { - return createContainer(dc, imageReference, command, 0, null, null, volumes); + return createContainer(dc, imageReference, command, 0, null, null, null, volumes); + } + + public static String createContainer(DockerClient dc, String imageReference, List command, + Integer runAsId, Map env, String securityOpts, String network, + List volumes) { + + CreateContainerCmd ccc = dc.createContainerCmd(imageReference); + if (volumes != null) { + List binds = new ArrayList<>(); + for (VolumeBind vb : volumes) { + Bind bind = createBind(vb); + binds.add(bind); + } + + ccc.getHostConfig().withBinds(binds); + } + + return createContainerInternal(dc,imageReference,command,runAsId,env,securityOpts,network,ccc); } public static String createContainer(DockerClient dc, String imageReference, List command, - Integer runAsId, Map env, String securityOpts, + Integer runAsId, Map env, String securityOpts, String network, VolumeBind... volumes) { - + CreateContainerCmd ccc = dc.createContainerCmd(imageReference); if (volumes != null) { List binds = new ArrayList<>(); @@ -59,6 +86,12 @@ public static String createContainer(DockerClient dc, String imageReference, Lis ccc.getHostConfig().withBinds(binds); } + return createContainerInternal(dc,imageReference,command,runAsId,env,securityOpts,network,ccc); + } + + private static String createContainerInternal(DockerClient dc, String imageReference, List command, + Integer runAsId, Map env, String securityOpts, String network, CreateContainerCmd ccc) { + if(runAsId!=null){ ccc.withUser(""+runAsId); } @@ -75,15 +108,18 @@ public static String createContainer(DockerClient dc, String imageReference, Lis ccc.withCmd(command); } - // String networkMode="host"; - // if (networkMode!=null){ - // ccc.getHostConfig().with - // } + if (network!=null){ + ccc.withHostConfig(ccc.getHostConfig().withNetworkMode(network)); + } CreateContainerResponse ccr = ccc.exec(); return ccr.getId(); } + public static String commitContainer(DockerClient dc, String containerId) { + return dc.commitCmd(containerId).exec(); + } + public static void removeContainer(DockerClient dc, String containerId) { dc.removeContainerCmd(containerId).exec(); } @@ -239,6 +275,33 @@ public void run() { } } + public static byte[] getFileFromContainer(DockerClient dc, String id, String path) { + CopyArchiveFromContainerCmd copycmd = dc.copyArchiveFromContainerCmd(id, path); + ByteArrayOutputStream file = new ByteArrayOutputStream(); + try{ + InputStream tarStream = copycmd.exec(); + TarArchiveInputStream tarInput = new TarArchiveInputStream(tarStream); + try{ + TarArchiveEntry tarEntry = tarInput.getNextTarEntry(); + while(tarEntry!=null){ + //tarEntry.getName does not include path.. + //if(path.equals(tarEntry.getName())){ + copy(tarInput, file); + file.close(); + //} + tarEntry = tarInput.getNextTarEntry(); + } + return file.toByteArray(); + }finally{ + if(tarInput!=null)tarInput.close(); + } + }catch(NotFoundException nfe){ + throw BuildpackException.launderThrowable("Unable to locate container '"+id+"'", nfe); + } catch (IOException e) { + throw BuildpackException.launderThrowable("Unable to retrieve '"+path+"' from container", e); + } + } + private static final void copy(InputStream in, OutputStream out) { byte[] buf = new byte[8192]; int length; diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/FileContent.java b/client/src/main/java/dev/snowdrop/buildpack/docker/FileContent.java index ef1d8f2..129f2ea 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/FileContent.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/FileContent.java @@ -2,6 +2,7 @@ import java.io.BufferedInputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -31,6 +32,9 @@ public FileContent(File file) { public FileContent(String prefix, File file) { this(prefix, file, null); + if(!file.exists()){ + throw new RuntimeException(new FileNotFoundException(file.getAbsolutePath())); + } } private FileContent(String prefix, File file, File root) { diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java index 190afe1..790861e 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java @@ -2,6 +2,8 @@ import java.io.File; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.exception.NotFoundException; @@ -31,17 +33,17 @@ public static void removeVolume(DockerClient dc, String volumeName) { dc.removeVolumeCmd(volumeName).exec(); } - public static boolean addContentToVolume(DockerClient dc, String volumeName, String pathInVolume, File content) { - return internalAddContentToVolume(dc, volumeName, mountPrefix, 0,0, new FileContent(content).getContainerEntries()); + public static boolean addContentToVolume(DockerClient dc, String volumeName, String useImage, String pathInVolume, File content) { + return internalAddContentToVolume(dc, volumeName, useImage, mountPrefix, 0,0, new FileContent(content).getContainerEntries()); } - public static boolean addContentToVolume(DockerClient dc, String volumeName, String name, String content) { - return internalAddContentToVolume(dc, volumeName, mountPrefix, 0,0, new StringContent(name, content).getContainerEntries()); + public static boolean addContentToVolume(DockerClient dc, String volumeName, String useImage, String name, String content) { + return internalAddContentToVolume(dc, volumeName, useImage, mountPrefix, 0,0, new StringContent(name, content).getContainerEntries()); } - public static boolean addContentToVolume(DockerClient dc, String volumeName, String prefix, int uid, int gid, List entries) { + public static boolean addContentToVolume(DockerClient dc, String volumeName, String useImage, String prefix, int uid, int gid, List entries) { if(!prefix.startsWith("/")) prefix = "/"+prefix; - return internalAddContentToVolume(dc, volumeName, mountPrefix+prefix, uid, gid, entries); + return internalAddContentToVolume(dc, volumeName, useImage, mountPrefix+prefix, uid, gid, entries); } private static boolean internalCreateVolume(DockerClient dc, String volumeName) { @@ -49,18 +51,16 @@ private static boolean internalCreateVolume(DockerClient dc, String volumeName) return exists(dc, volumeName); } - private static boolean internalAddContentToVolume(DockerClient dc, String volumeName, String prefix, int uid, int gid, List entries) { - return internalAddContentToVolume(dc, volumeName, prefix, uid, gid, entries.toArray(new ContainerEntry[entries.size()])); + private static boolean internalAddContentToVolume(DockerClient dc, String volumeName, String useImage, String prefix, int uid, int gid, List entries) { + return internalAddContentToVolume(dc, volumeName, useImage, prefix, uid, gid, entries.toArray(new ContainerEntry[entries.size()])); } - private static boolean internalAddContentToVolume(DockerClient dc, String volumeName, String prefix, int uid, int gid, ContainerEntry... entries) { - // TODO: find better 'no-op' container to use? - String dummyId = ContainerUtils.createContainer(dc, "tianon/true", new VolumeBind(volumeName, "/volumecontent")); - + private static boolean internalAddContentToVolume(DockerClient dc, String volumeName, String useImage, String prefix, int uid, int gid, ContainerEntry... entries) { + + List command = Stream.of("").collect(Collectors.toList()); + String dummyId = ContainerUtils.createContainer(dc, useImage, command, new VolumeBind(volumeName, mountPrefix)); ContainerUtils.addContentToContainer(dc, dummyId, prefix, uid, gid, entries); - ContainerUtils.removeContainer(dc, dummyId); - return true; } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/phases/ContainerStatus.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/ContainerStatus.java similarity index 81% rename from client/src/main/java/dev/snowdrop/buildpack/phases/ContainerStatus.java rename to client/src/main/java/dev/snowdrop/buildpack/lifecycle/ContainerStatus.java index f252499..8d6089e 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/phases/ContainerStatus.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/ContainerStatus.java @@ -1,4 +1,4 @@ -package dev.snowdrop.buildpack.phases; +package dev.snowdrop.buildpack.lifecycle; import lombok.Data; import lombok.ToString; diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java new file mode 100644 index 0000000..d9cf106 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java @@ -0,0 +1,148 @@ +package dev.snowdrop.buildpack.lifecycle; + +import org.tomlj.Toml; +import org.tomlj.TomlParseResult; + +import dev.snowdrop.buildpack.BuildConfig; +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.docker.ContainerUtils; +import dev.snowdrop.buildpack.docker.ImageUtils; +import dev.snowdrop.buildpack.lifecycle.phases.Analyzer; +import dev.snowdrop.buildpack.lifecycle.phases.Detector; + +public class LifecycleExecutor { + + private final BuildConfig config; + private final LifecyclePhaseFactory factory; + private final Version activePlatformLevel; + private final boolean useCreator; + + private boolean useCreator(boolean extensionsPresent, Boolean trustBuilder) { + if(trustBuilder==null){ + return extensionsPresent; + } + + if(!trustBuilder) + return false; + else{ + if(extensionsPresent) { + config.getLogConfig().getLogger().stdout("request to trust builder ignored, as extensions are present"); + return false; + } + + return true; + } + } + + public LifecycleExecutor(BuildConfig config, BuilderImage originalBuilder, BuilderImage extendedBuilder, String activePlatformLevel) { + this.config = config; + this.useCreator = useCreator(extendedBuilder.hasExtensions(), config.getPlatformConfig().getTrustBuilder()); + this.activePlatformLevel = new Version(activePlatformLevel); + this.factory = new LifecyclePhaseFactory(config.getDockerConfig(), + config.getBuildCacheConfig(), + config.getLaunchCacheConfig(), + config.getKanikoCacheConfig(), + config.getPlatformConfig(), + config.getLogConfig(), + config.getOutputImage(), + originalBuilder, + extendedBuilder, + activePlatformLevel); + } + + public int execute() { + int rc; + try{ + //have factory create volumes for caches/application etc + factory.createVolumes(config.getApplication()); + + //do build phases, pay attention to useCreator & activePlatformLevel + if(useCreator) { + //although creator phase doesn't support extensions, it's possible + //the build may select an order/group that doesn't make use of + //any extensions that are present, so we should not prevent use of + //creator if the builder image has extensions. + + //create and run the creator phase + rc = runPhase(factory.getCreator()); + } else { + do{ + Analyzer analyzer = (Analyzer)factory.getAnalyzer(); + Detector detector = (Detector)factory.getDetector(); + + //spec below 0.7 use detect/analyze ordering, 0.7 and above use analyze/detect ordering + if(activePlatformLevel.lessThan("0.7")) { + rc=runPhase(detector); + if(rc!=0) break; + + rc=runPhase(analyzer); + if(rc!=0) break; + }else{ + rc=runPhase(analyzer); + if(rc!=0) break; + + rc=runPhase(detector); + if(rc!=0) break; + } + + //move this to new method + boolean extendedRunImage=false; + if(activePlatformLevel.atLeast("0.10") && factory.getBuilderImage().hasExtensions()){ + byte[] analyzerToml = detector.getAnalyzedToml(); + + TomlParseResult analyzed = Toml.parse(new String(analyzerToml)); + String newRunImage = analyzed.getString("run-image.reference"); + + if(activePlatformLevel.atLeast("0.12")){ + Boolean extend = analyzed.getBoolean("run-image.extend"); + extendedRunImage = extend == null ? false : extend.booleanValue(); + } + + //pull the new image.. + ImageUtils.pullImages(config.getDockerConfig().getDockerClient(), factory.getDockerConfig().getPullTimeout(), newRunImage); + + //update run image associated with our builder image. + factory.getBuilderImage().getRunImages(activePlatformLevel).clear(); + factory.getBuilderImage().getRunImages(activePlatformLevel).add(newRunImage); + + //TODO: if config.getRunImage is non-null, should we override the run-image from an extension here? + } + + rc=runPhase(factory.getRestorer()); + if(rc!=0) break; + + System.out.println("Lifecycle Executor buildimage has extensions? "+factory.getBuilderImage().hasExtensions()); + + if(activePlatformLevel.atLeast("0.10") && factory.getBuilderImage().hasExtensions()){ + rc=runPhase(factory.getBuildImageExtender()); + if(rc!=0) break; + + //if platform is atleast 0.12, and analyzerToml run-image.extend is true, we must run run image extender. + if(extendedRunImage){ + rc=runPhase(factory.getRunImageExtender()); + if(rc!=0) break; + } + }else{ + rc=runPhase(factory.getBuilder()); + if(rc!=0) break; + } + + //if platform is at least 0.12, and run image extension happened, add -extended flag to exporter + rc=runPhase(factory.getExporter(extendedRunImage)); + if(rc!=0) break; + }while(false); + } + return rc; + }finally{ + //allow factory to clean up any volumes created + factory.tidyUp(); + } + } + + + private int runPhase(LifecyclePhase phase){ + ContainerStatus phaseRC = phase.runPhase(config.getLogConfig().getLogger(), config.getLogConfig().getUseTimestamps()); + ContainerUtils.removeContainer(config.getDockerConfig().getDockerClient(), phaseRC.getContainerId()); + return phaseRC.getRc(); + } +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/phases/LifecyclePhase.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhase.java similarity index 78% rename from client/src/main/java/dev/snowdrop/buildpack/phases/LifecyclePhase.java rename to client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhase.java index 554d4cd..8f92ad6 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/phases/LifecyclePhase.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhase.java @@ -1,4 +1,4 @@ -package dev.snowdrop.buildpack.phases; +package dev.snowdrop.buildpack.lifecycle; import dev.snowdrop.buildpack.Logger; diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java new file mode 100644 index 0000000..b367f62 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java @@ -0,0 +1,268 @@ +package dev.snowdrop.buildpack.lifecycle; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.config.CacheConfig; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.config.PlatformConfig; +import dev.snowdrop.buildpack.docker.ContainerEntry; +import dev.snowdrop.buildpack.docker.ContainerUtils; +import dev.snowdrop.buildpack.docker.Content; +import dev.snowdrop.buildpack.docker.StringContent; +import dev.snowdrop.buildpack.docker.VolumeBind; +import dev.snowdrop.buildpack.docker.VolumeUtils; +import dev.snowdrop.buildpack.lifecycle.phases.Analyzer; +import dev.snowdrop.buildpack.lifecycle.phases.Builder; +import dev.snowdrop.buildpack.lifecycle.phases.Creator; +import dev.snowdrop.buildpack.lifecycle.phases.Detector; +import dev.snowdrop.buildpack.lifecycle.phases.Exporter; +import dev.snowdrop.buildpack.lifecycle.phases.Extender; +import dev.snowdrop.buildpack.lifecycle.phases.Restorer; + +public class LifecyclePhaseFactory { + + private static final Logger log = LoggerFactory.getLogger(LifecyclePhaseFactory.class); + + //paths we use for mountpoints within build container. + public final static String CACHE_VOL_PATH = "/cache-dir"; + public final static String LAUNCH_CACHE_VOL_PATH = "/launch-cache-dir"; + public final static String WORKSPACE_VOL_PATH = "/workspace"; + public final static String LAYERS_VOL_PATH = "/layers"; + public final static String PLATFORM_VOL_PATH = "/platform"; + public final static String KANIKO_VOL_PATH = "/kaniko"; + public final static String DOCKER_SOCKET_PATH = "/var/run/docker.sock"; + + public final static String APP_PATH_PREFIX = ""; //previously /content to avoid permissions, should not be issue with extended builder. + public final static String ENV_PATH_PREFIX = "/env"; + + private final DockerConfig dockerConfig; + private final CacheConfig buildCacheConfig; + private final CacheConfig launchCacheConfig; + private final CacheConfig kanikoCacheConfig; + private final PlatformConfig platformConfig; + private final LogConfig logConfig; + + private final ImageReference outputImage; + private final BuilderImage originalBuilder; + private final BuilderImage builder; + private final Version platformLevel; + + //names of the volumes during buildtime. + final String buildCacheVolume; + final String launchCacheVolume; + final String kanikoCacheVolume; + final String applicationVolume; + final String outputVolume; + final String platformVolume; + + // util method for random suffix. + private String randomString(int length) { + return (new Random()).ints('a', 'z' + 1).limit(length) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(); + } + + private String getVolumeName(CacheConfig config, String prefix) { + String volumeName; + if(config!=null && config.getCacheVolumeName() != null){ + volumeName = config.getCacheVolumeName(); + }else{ + volumeName = prefix + randomString(10); + } + return volumeName; + } + + public String getContainerForPhase(String args[], Integer runAsId){ + ArrayList binds = new ArrayList<>(Arrays.asList( + new VolumeBind(buildCacheVolume, LifecyclePhaseFactory.CACHE_VOL_PATH), + new VolumeBind(launchCacheVolume, LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH), + new VolumeBind(applicationVolume, LifecyclePhaseFactory.WORKSPACE_VOL_PATH), + new VolumeBind(platformVolume, LifecyclePhaseFactory.PLATFORM_VOL_PATH), + new VolumeBind(outputVolume, LifecyclePhaseFactory.LAYERS_VOL_PATH), + new VolumeBind(kanikoCacheVolume, LifecyclePhaseFactory.KANIKO_VOL_PATH) + )); + + if(dockerConfig.getUseDaemon()) + binds.add(new VolumeBind(dockerConfig.getDockerSocket(), LifecyclePhaseFactory.DOCKER_SOCKET_PATH)); + + // create a container using builderImage that will invoke the creator process + String id = ContainerUtils.createContainer(dockerConfig.getDockerClient(), + builder.getImage().getReference(), + Arrays.asList(args), + runAsId, + platformConfig.getEnvironment(), + "label=disable", + dockerConfig.getDockerNetwork(), + binds); + + log.info("- mounted " + buildCacheVolume + " at " + CACHE_VOL_PATH); + log.info("- mounted " + launchCacheVolume + " at " + LAUNCH_CACHE_VOL_PATH); + log.info("- mounted " + kanikoCacheVolume + " at " + KANIKO_VOL_PATH); + log.info("- mounted " + applicationVolume + " at " + WORKSPACE_VOL_PATH); + log.info("- mounted " + platformVolume + " at " + PLATFORM_VOL_PATH); + if(dockerConfig.getUseDaemon()) + log.info("- mounted " + dockerConfig.getDockerSocket() + " at " + LifecyclePhaseFactory.DOCKER_SOCKET_PATH); + log.info("- mounted " + outputVolume + " at " + LAYERS_VOL_PATH); + log.info("- build container id " + id); + return id; + } + + public LifecyclePhaseFactory(DockerConfig dockerConfig, + CacheConfig buildCacheConfig, + CacheConfig launchCacheConfig, + CacheConfig kanikoCacheConfig, + PlatformConfig platformConfig, + LogConfig logConfig, + ImageReference outputImage, + BuilderImage originalBuilder, + BuilderImage extendedBuilder, + String platformLevel) { + + this.buildCacheVolume = getVolumeName(buildCacheConfig, "buildpack-build-"); + this.launchCacheVolume = getVolumeName(launchCacheConfig, "buildpack-launch-"); + this.applicationVolume = getVolumeName(null,"buildpack-app-"); + this.outputVolume = getVolumeName(null,"buildpack-output-"); + this.platformVolume = getVolumeName(null,"buildpack-platform-"); + this.kanikoCacheVolume = getVolumeName(kanikoCacheConfig,"buildpack-kaniko-"); + + this.dockerConfig = dockerConfig; + this.buildCacheConfig = buildCacheConfig; + this.launchCacheConfig = launchCacheConfig; + this.kanikoCacheConfig = kanikoCacheConfig; + this.platformConfig = platformConfig; + this.logConfig = logConfig; + this.outputImage = outputImage; + this.originalBuilder = originalBuilder; + this.builder = extendedBuilder; + this.platformLevel = new Version(platformLevel); + } + + public void createVolumes(List content){ + // create the volumes. + VolumeUtils.createVolumeIfRequired(dockerConfig.getDockerClient(), buildCacheVolume); + VolumeUtils.createVolumeIfRequired(dockerConfig.getDockerClient(), launchCacheVolume); + VolumeUtils.createVolumeIfRequired(dockerConfig.getDockerClient(), applicationVolume); + VolumeUtils.createVolumeIfRequired(dockerConfig.getDockerClient(), outputVolume); + VolumeUtils.createVolumeIfRequired(dockerConfig.getDockerClient(), platformVolume); + VolumeUtils.createVolumeIfRequired(dockerConfig.getDockerClient(), kanikoCacheVolume); + + // add the application to the volume. Note we are placing it at /content, + // because the volume mountpoint is mounted such that the user has no perms to create + // new content there, but subdirs are ok. + log.info("There are "+content.size()+" entries to add for the app dir"); + List appEntries = content + .stream() + .flatMap(c -> c.getContainerEntries().stream()) + .collect(Collectors.toList()); + + VolumeUtils.addContentToVolume(dockerConfig.getDockerClient(), + applicationVolume, + originalBuilder.getImage().getReference(), + LifecyclePhaseFactory.APP_PATH_PREFIX, + builder.getUserId(), + builder.getGroupId(), + appEntries); + + //add workarounds to environment. + if(!platformConfig.getEnvironment().containsKey("CNB_PLATFORM_API")) platformConfig.getEnvironment().put("CNB_PLATFORM_API", platformLevel.toString()); + + // This a workaround for a bug in older lifecyle revisions. https://github.com/buildpacks/lifecycle/issues/339 + if(!platformConfig.getEnvironment().containsKey("CNB_REGISTRY_AUTH")) platformConfig.getEnvironment().put("CNB_REGISTRY_AUTH", "{}"); + + //enable experimental features when required. + if(builder.hasExtensions() && + platformLevel.atLeast("0.10") && + !platformConfig.getEnvironment().containsKey("CNB_EXPERIMENTAL_MODE")) { + platformConfig.getEnvironment().put("CNB_EXPERIMENTAL_MODE", "warn"); + } + + //add the environment entries to the platform volume. + List envEntries = platformConfig.getEnvironment().entrySet() + .stream() + .flatMap(e -> new StringContent(e.getKey(), e.getValue()).getContainerEntries().stream()) + .collect(Collectors.toList()); + + VolumeUtils.addContentToVolume(dockerConfig.getDockerClient(), + platformVolume, + originalBuilder.getImage().getReference(), + LifecyclePhaseFactory.ENV_PATH_PREFIX, + builder.getUserId(), + builder.getGroupId(), + envEntries); + } + + public void tidyUp(){ + // remove volumes + // (note when/if we persist the cache between builds, we'll be more selective here over what we remove) + if (buildCacheConfig.getDeleteCacheAfterBuild()) { + VolumeUtils.removeVolume(dockerConfig.getDockerClient(), buildCacheVolume); + } + if (launchCacheConfig.getDeleteCacheAfterBuild()) { + VolumeUtils.removeVolume(dockerConfig.getDockerClient(), launchCacheVolume); + } + if (kanikoCacheConfig.getDeleteCacheAfterBuild()) { + VolumeUtils.removeVolume(dockerConfig.getDockerClient(), kanikoCacheVolume); + } + + //always remove the app/output/platform vols, they are unique to each build. + VolumeUtils.removeVolume(dockerConfig.getDockerClient(), applicationVolume); + VolumeUtils.removeVolume(dockerConfig.getDockerClient(), outputVolume); + VolumeUtils.removeVolume(dockerConfig.getDockerClient(), platformVolume); + + log.info("- build volumes tidied up"); + } + + public BuilderImage getBuilderImage(){ + return builder; + } + public DockerConfig getDockerConfig(){ + return dockerConfig; + } + public LogConfig getLogConfig(){ + return logConfig; + } + public ImageReference getOutputImage(){ + return outputImage; + } + public Version getPlatformLevel(){ + return platformLevel; + } + + public LifecyclePhase getCreator(){ + return new Creator(this); + } + public LifecyclePhase getAnalyzer(){ + return new Analyzer(this); + } + public LifecyclePhase getDetector(){ + return new Detector(this); + } + public LifecyclePhase getRestorer(){ + return new Restorer(this, originalBuilder); + } + public LifecyclePhase getBuilder(){ + return new Builder(this); + } + public LifecyclePhase getBuildImageExtender(){ + return getExtender("build"); + } + public LifecyclePhase getRunImageExtender(){ + return getExtender("run"); + } + public LifecyclePhase getExtender(String kind){ + return new Extender(this, kind); + } + public LifecyclePhase getExporter(boolean extended){ + return new Exporter(this, extended); + } + +} \ No newline at end of file diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/Version.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/Version.java new file mode 100644 index 0000000..7c14f7a --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/Version.java @@ -0,0 +1,62 @@ +package dev.snowdrop.buildpack.lifecycle; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import dev.snowdrop.buildpack.BuildpackException; + +public class Version { + String version; + int major; + int minor; + + final Pattern p = Pattern.compile("^(\\d+)\\.(\\d+)(\\..*)?$"); + + public Version(String v){ + version = v; + Matcher m = p.matcher(v); + if(m.find()){ + major = Integer.parseInt(m.group(1)); + minor = Integer.parseInt(m.group(2)); + }else{ + throw new BuildpackException("Invalid format for Version "+v, new IllegalArgumentException()); + } + } + + public String toString() { + return version; + } + + public boolean equals(Version v){ + return this.major == v.major && this.minor == v.minor; + } + public boolean greaterThan(Version v){ + return this.major > v.major || ( this.major == v.major && this.minor > v.minor); + } + public boolean atLeast(Version v){ + return this.major > v.major || ( this.major == v.major && this.minor >= v.minor); + } + public boolean atMost(Version v){ + return this.major < v.major || ( this.major == v.major && this.minor <= v.minor); + } + public boolean lessThan(Version v){ + return this.major < v.major || ( this.major == v.major && this.minor < v.minor); + } + + //convenience methods to keep code a little more readable. + public boolean equals(String ver){ + return this.equals(new Version(ver)); + } + public boolean greaterThan(String ver){ + return this.greaterThan(new Version(ver)); + } + public boolean atLeast(String ver){ + return this.atLeast(new Version(ver)); + } + public boolean atMost(String ver){ + return this.atMost(new Version(ver)); + } + public boolean lessThan(String ver){ + return this.lessThan(new Version(ver)); + } +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java new file mode 100644 index 0000000..26e28b8 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java @@ -0,0 +1,88 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.utils.LifecycleArgs; + + +public class Analyzer implements LifecyclePhase{ + + private static final Logger log = LoggerFactory.getLogger(Analyzer.class); + + final LifecyclePhaseFactory factory; + + public Analyzer( LifecyclePhaseFactory factory ){ + this.factory = factory; + } + + @Override + public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean useTimestamps) { + + LifecycleArgs args = new LifecycleArgs("/cnb/lifecycle/analyzer", factory.getOutputImage().getReference()); + + // 0.7 onwards.. removed + // -cache-dir, Path to cach directory + // -group, Path to group definition (group.toml) + // -skip-layers, Do not perform layer analysis + // added + // -previous-image, Image reference to be analyzed (usually the result of the previous build) + // -run-image, Run image reference + // -stack, Path to stack.toml + // -tag, additional tag to apply to exported image + + + // 0.9 onwards.. added + // -launch-cache flag, used when restoring SBOM layer from prev image in daemon. + // -skip-layers flag .. skips SBOM restoration entirely + + // 0.12 onwards.. -stack is removed and replaced with -run + + args.addArg("-uid", "" + factory.getBuilderImage().getUserId()); + args.addArg("-gid", "" + factory.getBuilderImage().getGroupId()); + args.addArg("-run-image", factory.getBuilderImage().getRunImages(factory.getPlatformLevel()).get(0)); + args.addArg("-log-level", factory.getLogConfig().getLogLevel()); + args.addArg("-analyzed", LifecyclePhaseFactory.LAYERS_VOL_PATH + "/analyzed.toml"); + + if(factory.getPlatformLevel().atLeast("0.9") && factory.getDockerConfig().getUseDaemon()){ + args.addArg("-launch-cache", LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH); + } + + //if using daemon, add daemon arg + if(factory.getDockerConfig().getUseDaemon()){ + args.addArg("-daemon"); + } + + int runAsId = factory.getBuilderImage().getUserId(); + String id = factory.getContainerForPhase(args.toArray(), runAsId); + log.info("- analyze container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching analyze container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack analyze container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + } + + + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Builder.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Builder.java new file mode 100644 index 0000000..68161b5 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Builder.java @@ -0,0 +1,61 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.utils.LifecycleArgs; + + +public class Builder implements LifecyclePhase{ + + private static final Logger log = LoggerFactory.getLogger(Builder.class); + + final LifecyclePhaseFactory factory; + + public Builder( LifecyclePhaseFactory factory){ + this.factory = factory; + } + + @Override + public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean useTimestamps) { + + LifecycleArgs args = new LifecycleArgs("/cnb/lifecycle/builder", null); + + //args.addArg("-analyzed", LifecyclePhaseFactory.LAYERS_VOL_PATH + "/analyzed.toml"); + args.addArg("-app", LifecyclePhaseFactory.WORKSPACE_VOL_PATH + LifecyclePhaseFactory.APP_PATH_PREFIX); + args.addArg("-layers", LifecyclePhaseFactory.LAYERS_VOL_PATH); + args.addArg("-platform", LifecyclePhaseFactory.PLATFORM_VOL_PATH); + args.addArg("-log-level", factory.getLogConfig().getLogLevel()); + + //builder process has to run as root. + int runAsId = factory.getBuilderImage().getUserId(); + String id = factory.getContainerForPhase(args.toArray(), runAsId); + log.info("- extender container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching builder container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack builder container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + } + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java new file mode 100644 index 0000000..ff7e4c8 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java @@ -0,0 +1,76 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.utils.LifecycleArgs; + + +public class Creator implements LifecyclePhase{ + + private static final Logger log = LoggerFactory.getLogger(Creator.class); + + final LifecyclePhaseFactory factory; + + public Creator( LifecyclePhaseFactory factory ){ + this.factory = factory; + } + + @Override + public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean useTimestamps) { + + LifecycleArgs args = new LifecycleArgs("/cnb/lifecycle/creator", factory.getOutputImage().getReference()); + + args.addArg("-uid", "" + factory.getBuilderImage().getUserId()); + args.addArg("-gid", "" + factory.getBuilderImage().getGroupId()); + args.addArg("-cache-dir", LifecyclePhaseFactory.CACHE_VOL_PATH); + args.addArg("-app", LifecyclePhaseFactory.WORKSPACE_VOL_PATH + LifecyclePhaseFactory.APP_PATH_PREFIX); + args.addArg("-layers", LifecyclePhaseFactory.LAYERS_VOL_PATH); + args.addArg("-platform", LifecyclePhaseFactory.PLATFORM_VOL_PATH); + args.addArg("-run-image", factory.getBuilderImage().getRunImages(factory.getPlatformLevel()).get(0)); + args.addArg("-log-level", factory.getLogConfig().getLogLevel()); + args.addArg("-skip-restore", "false"); + + if(factory.getPlatformLevel().atLeast("0.9") && factory.getDockerConfig().getUseDaemon()){ + args.addArg("-launch-cache", LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH); + } + + //if using daemon, add daemon arg + if(factory.getDockerConfig().getUseDaemon()){ + args.addArg("-daemon"); + } + + // TODO: add labels for container for creator etc (as per spec) + + //creator process always has to run as root. + int runAsId = 0; + String id = factory.getContainerForPhase(args.toArray(), runAsId); + log.info("- creator container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching build container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + } + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java new file mode 100644 index 0000000..869b2f2 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java @@ -0,0 +1,94 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.docker.ContainerUtils; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.utils.LifecycleArgs; + + +public class Detector implements LifecyclePhase{ + + private static final Logger log = LoggerFactory.getLogger(Detector.class); + + private final LifecyclePhaseFactory factory; + + private byte[] analyzedToml = null; + + public Detector( LifecyclePhaseFactory factory ){ + this.factory = factory; + } + + @Override + public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean useTimestamps) { + + //0.7 onwards.. detector will look for order.toml in /layers before checking other paths. + // allowing platforms to write an order.toml & override the builders order.toml + + //0.10 onwards.. when processing an extensions build, add -generated (default: layers/generated) + + //0.12 onwards.. when run Images are present, invoke with -run to specify /cnb/run.toml + // (creates warning if switched image is not in set). + + LifecycleArgs args = new LifecycleArgs("/cnb/lifecycle/detector", null); + + args.addArg("-app", LifecyclePhaseFactory.WORKSPACE_VOL_PATH + LifecyclePhaseFactory.APP_PATH_PREFIX); + args.addArg("-analyzed", LifecyclePhaseFactory.LAYERS_VOL_PATH + "/analyzed.toml"); + args.addArg("-layers", LifecyclePhaseFactory.LAYERS_VOL_PATH); + args.addArg("-platform", LifecyclePhaseFactory.PLATFORM_VOL_PATH); + args.addArg("-log-level", factory.getLogConfig().getLogLevel()); + + if(factory.getPlatformLevel().atLeast("0.10") && factory.getBuilderImage().hasExtensions() ){ + args.addArg("-generated", LifecyclePhaseFactory.LAYERS_VOL_PATH + "/generated"); + } + + if(factory.getPlatformLevel().atLeast("0.12") && factory.getBuilderImage().hasExtensions() ){ + args.addArg("-run", "/cnb/run.toml"); + } + + //if using daemon, add daemon arg + if(factory.getDockerConfig().getUseDaemon()){ + args.addArg("-daemon"); + } + + // detector phase must run as non-root + int runAsId = factory.getBuilderImage().getUserId(); + String id = factory.getContainerForPhase(args.toArray(), runAsId); + log.info("- detect container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching detect container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack detect container complete, with exit code " + rc); + + analyzedToml = ContainerUtils.getFileFromContainer(factory.getDockerConfig().getDockerClient(), + id, + LifecyclePhaseFactory.LAYERS_VOL_PATH + "/analyzed.toml"); + + return ContainerStatus.of(rc,id); + } + + public byte[] getAnalyzedToml(){ + return analyzedToml; + } + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java new file mode 100644 index 0000000..6603fb8 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java @@ -0,0 +1,81 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.utils.LifecycleArgs; + + +public class Exporter implements LifecyclePhase{ + + private static final Logger log = LoggerFactory.getLogger(Exporter.class); + + final LifecyclePhaseFactory factory; + final boolean runExtended; + + public Exporter( LifecyclePhaseFactory factory, boolean runExtended){ + this.factory = factory; + this.runExtended = runExtended; + } + + @Override + public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean useTimestamps) { + + //0.6 onwards.. added -process-type (to set default process type) + //0.7 onwards.. removed -run-image + //0.12 onwards.. -stack is removed and replaced with -run + // /cnb/stack.toml removed, replaced with /cnb/run.toml + + LifecycleArgs args = new LifecycleArgs("/cnb/lifecycle/exporter", factory.getOutputImage().getReference()); + + args.addArg("-uid", "" + factory.getBuilderImage().getUserId()); + args.addArg("-gid", "" + factory.getBuilderImage().getGroupId()); + args.addArg("-app", LifecyclePhaseFactory.WORKSPACE_VOL_PATH + LifecyclePhaseFactory.APP_PATH_PREFIX); + args.addArg("-layers", LifecyclePhaseFactory.LAYERS_VOL_PATH); + args.addArg("-cache-dir", LifecyclePhaseFactory.CACHE_VOL_PATH); + args.addArg("-launch-cache", LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH); + args.addArg("-log-level", factory.getLogConfig().getLogLevel()); + + if(factory.getPlatformLevel().lessThan("0.7")){ + args.addArg("-run-image", factory.getBuilderImage().getRunImages(factory.getPlatformLevel()).get(0)); + } + if(factory.getPlatformLevel().atLeast("0.12") && runExtended){ + args.addArg("-run", "/cnb/run.toml"); + } + + //if using daemon, add daemon arg + if(factory.getDockerConfig().getUseDaemon()){ + args.addArg("-daemon"); + } + + int runAsId = factory.getBuilderImage().getUserId(); + String id = factory.getContainerForPhase(args.toArray(), runAsId); + log.info("- export container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching export container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack export container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + } + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Extender.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Extender.java new file mode 100644 index 0000000..8a1d438 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Extender.java @@ -0,0 +1,72 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.utils.LifecycleArgs; + + +public class Extender implements LifecyclePhase{ + + private static final Logger log = LoggerFactory.getLogger(Extender.class); + + final LifecyclePhaseFactory factory; + final String kind; + + public Extender( LifecyclePhaseFactory factory, String kind ){ + this.factory = factory; + this.kind = kind; + } + + @Override + public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean useTimestamps) { + + LifecycleArgs args = new LifecycleArgs("/cnb/lifecycle/extender", null); + + args.addArg("-uid", "" + factory.getBuilderImage().getUserId()); + args.addArg("-gid", "" + factory.getBuilderImage().getGroupId()); + args.addArg("-app", LifecyclePhaseFactory.WORKSPACE_VOL_PATH + LifecyclePhaseFactory.APP_PATH_PREFIX); + args.addArg("-layers", LifecyclePhaseFactory.LAYERS_VOL_PATH); + args.addArg("-platform", LifecyclePhaseFactory.PLATFORM_VOL_PATH); + args.addArg("-log-level", factory.getLogConfig().getLogLevel()); + + if(factory.getPlatformLevel().atLeast("0.12")){ + args.addArg("-kind", kind); + } + + //extender process has to run as root. + // + //as per https://buildpacks.io/docs/reference/spec/migration/platform-api-0.9-0.10/ + //... The extender user should have sufficient permissions to execute all RUN instructions, + // typically it should run as root. + int runAsId = 0; + String id = factory.getContainerForPhase(args.toArray(), runAsId); + log.info("- extender container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching extender container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack extender container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + } + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java new file mode 100644 index 0000000..c0f3dad --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java @@ -0,0 +1,82 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.utils.LifecycleArgs; + + +public class Restorer implements LifecyclePhase{ + + private static final Logger log = LoggerFactory.getLogger(Restorer.class); + + final LifecyclePhaseFactory factory; + final BuilderImage originalBuilder; + + public Restorer( LifecyclePhaseFactory factory , BuilderImage originalBuilder){ + this.factory = factory; + this.originalBuilder = originalBuilder; + } + + @Override + public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean useTimestamps) { + + //0.7 onwards.. added -analyzed, -skip-layers + //0.10 onwards.. when building with extensions, add -build-image flag + //0.12 onwards.. if in daemon mode, use new -daemon flag for restorer + + LifecycleArgs args = new LifecycleArgs("/cnb/lifecycle/restorer", null); + + args.addArg("-uid", "" + factory.getBuilderImage().getUserId()); + args.addArg("-gid", "" + factory.getBuilderImage().getGroupId()); + args.addArg("-layers", LifecyclePhaseFactory.LAYERS_VOL_PATH); + args.addArg("-cache-dir", LifecyclePhaseFactory.CACHE_VOL_PATH); + args.addArg("-log-level", factory.getLogConfig().getLogLevel()); + + if(factory.getPlatformLevel().atLeast("0.10") && factory.getBuilderImage().hasExtensions()){ + //Spec unclear, setting this should be allowed, but causes errors + //Not an issue, because kaniko dir MUST always be /kaniko (lpf.KANIKO_VOL_PATH) + //args.addArg("-kaniko-dir", LifecyclePhaseFactory.KANIKO_VOL_PATH); + + //Can't use ephemeral/extended builder here as not in registry and restore tries to pull image.. + //Use original configured builder instead.. + args.addArg("-build-image", originalBuilder.getImage().getReference()); + } + + if(factory.getPlatformLevel().atLeast("0.12") && factory.getDockerConfig().getUseDaemon()){ + args.addArg("-daemon"); + } + + int runAsId = factory.getBuilderImage().getUserId(); + String id = factory.getContainerForPhase(args.toArray(), runAsId); + log.info("- restorer container id " + id+ " will be run with uid "+runAsId+" and args "+args); + System.out.println("- restorer container id " + id+ " will be run with uid "+runAsId+" and args "+args); + + // launch the container! + log.info("- launching restorer container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack restorer container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + } + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/phases/Creator.java b/client/src/main/java/dev/snowdrop/buildpack/phases/Creator.java deleted file mode 100644 index 38f0958..0000000 --- a/client/src/main/java/dev/snowdrop/buildpack/phases/Creator.java +++ /dev/null @@ -1,74 +0,0 @@ -package dev.snowdrop.buildpack.phases; - -import java.util.Arrays; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.github.dockerjava.api.command.WaitContainerResultCallback; -import dev.snowdrop.buildpack.ContainerLogReader; - - -public class Creator implements LifecyclePhase{ - - private static final Logger log = LoggerFactory.getLogger(Creator.class); - - final LifecyclePhaseFactory factory; - - public Creator( LifecyclePhaseFactory factory ){ - this.factory = factory; - } - - @Override - public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean useTimestamps) { - - // TODO: make daemon a config option. - boolean useDaemon = true; - - // configure our call to 'creator' which will do all the work. - String[] args = { "/cnb/lifecycle/creator", - "-uid", "" + factory.buildUserId, - "-gid", "" + factory.buildGroupId, - "-cache-dir", LifecyclePhaseFactory.BUILD_VOL_PATH, - "-app", LifecyclePhaseFactory.APP_VOL_PATH + LifecyclePhaseFactory.APP_PATH_PREFIX, - "-layers", LifecyclePhaseFactory.OUTPUT_VOL_PATH, - "-platform", LifecyclePhaseFactory.PLATFORM_VOL_PATH, - "-run-image", factory.runImageName, - "-launch-cache", LifecyclePhaseFactory.LAUNCH_VOL_PATH, - "-log-level", factory.buildLogLevel, - "-skip-restore", - factory.finalImageName }; - - //if using daemon, inject daemon arg before final image name. - if(useDaemon){ - int len = args.length; - args = Arrays.copyOf(args, len+1); - args[len] = args[len-1]; - args[len-1] = "-daemon"; - } - - // TODO: add labels for container for creator etc (as per spec) - - String id = factory.getContainerForPhase(args, (useDaemon ? 0 : factory.buildUserId)); - log.info("- build container id " + id); - - // launch the container! - log.info("- launching build container"); - factory.dockerClient.startContainerCmd(id).exec(); - - log.info("- attaching log relay"); - // grab the logs to stdout. - factory.dockerClient.logContainerCmd(id) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withTimestamps(useTimestamps) - .exec(new ContainerLogReader(logger)); - - // wait for the container to complete, and retrieve the exit code. - int rc = factory.dockerClient.waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); - log.info("Buildpack container complete, with exit code " + rc); - - return ContainerStatus.of(rc,id); - } - -} diff --git a/client/src/main/java/dev/snowdrop/buildpack/phases/LifecyclePhaseFactory.java b/client/src/main/java/dev/snowdrop/buildpack/phases/LifecyclePhaseFactory.java deleted file mode 100644 index f5d04e4..0000000 --- a/client/src/main/java/dev/snowdrop/buildpack/phases/LifecyclePhaseFactory.java +++ /dev/null @@ -1,176 +0,0 @@ -package dev.snowdrop.buildpack.phases; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.github.dockerjava.api.DockerClient; - -import dev.snowdrop.buildpack.Buildpack; -import dev.snowdrop.buildpack.docker.ContainerEntry; -import dev.snowdrop.buildpack.docker.ContainerUtils; -import dev.snowdrop.buildpack.docker.Content; -import dev.snowdrop.buildpack.docker.StringContent; -import dev.snowdrop.buildpack.docker.VolumeBind; -import dev.snowdrop.buildpack.docker.VolumeUtils; - - - -public class LifecyclePhaseFactory { - - private static final Logger log = LoggerFactory.getLogger(LifecyclePhaseFactory.class); - - //paths we use for mountpoints within build container. - public final static String BUILD_VOL_PATH = "/bld"; - public final static String LAUNCH_VOL_PATH = "/launch"; - public final static String APP_VOL_PATH = "/app"; - public final static String OUTPUT_VOL_PATH = "/out"; - public final static String PLATFORM_VOL_PATH = "/platform"; - public final static String DOCKER_SOCKET_PATH = "/var/run/docker.sock"; - - public final static String APP_PATH_PREFIX = "/content"; - - //provided for this build by caller. - final DockerClient dockerClient; - final String dockerSocket; - - final Integer buildUserId; - final Integer buildGroupId; - final String buildLogLevel; - - //image names. - final String builderImageName; - final String runImageName; - final String finalImageName; - - //names of the volumes during runtime. - final String buildCacheVolume; - final String launchCacheVolume; - final String applicationVolume; - final String outputVolume; - final String platformVolume; - - final List content; - final Map environment; - - final boolean removeBuildCacheAfterBuild; - final boolean removeLaunchCacheAfterBuild; - - public LifecyclePhaseFactory( DockerClient dockerClient, - Integer buildUserId, - Integer buildGroupId, - Buildpack buildConfig - ){ - this.dockerClient = dockerClient; - this.dockerSocket = buildConfig.getDockerSocket(); - - this.buildUserId = buildUserId; - this.buildGroupId = buildGroupId; - this.buildLogLevel = buildConfig.getLogLevel(); - - this.builderImageName = buildConfig.getBuilderImage(); - this.runImageName = buildConfig.getRunImage(); - this.finalImageName = buildConfig.getFinalImage(); - - this.content = buildConfig.getContent(); - this.environment = buildConfig.getEnvironment()==null ? new HashMap<>() : buildConfig.getEnvironment(); - - this.removeBuildCacheAfterBuild = buildConfig.getRemoveBuildCacheAfterBuild(); - this.removeLaunchCacheAfterBuild = buildConfig.getRemoveLaunchCacheAfterBuild(); - - this.buildCacheVolume = buildConfig.getBuildCacheVolumeName() == null ? "buildpack-build-" + randomString(10) : buildConfig.getBuildCacheVolumeName(); - this.launchCacheVolume = buildConfig.getLaunchCacheVolumeName() == null ? "buildpack-launch-" + randomString(10) : buildConfig.getLaunchCacheVolumeName(); - this.applicationVolume = "buildpack-app-" + randomString(10); - this.outputVolume = "buildpack-output-" + randomString(10); - this.platformVolume = "buildpack-platform-" + randomString(10); - - prep(); - } - - private void prep(){ - // create the volumes. - VolumeUtils.createVolumeIfRequired(dockerClient, buildCacheVolume); - VolumeUtils.createVolumeIfRequired(dockerClient, launchCacheVolume); - VolumeUtils.createVolumeIfRequired(dockerClient, applicationVolume); - VolumeUtils.createVolumeIfRequired(dockerClient, outputVolume); - VolumeUtils.createVolumeIfRequired(dockerClient, platformVolume); - - // add the application to the volume. Note we are placing it at /content, - // because the volume mountpoint is mounted such that the user has no perms to create - // new content there, but subdirs are ok. - List appEntries = content - .stream() - .flatMap(c -> c.getContainerEntries().stream()) - .collect(Collectors.toList()); - VolumeUtils.addContentToVolume(dockerClient, applicationVolume, LifecyclePhaseFactory.APP_PATH_PREFIX, buildUserId, buildGroupId, appEntries); - - //add the environment entries to the platform volume. - List envEntries = environment.entrySet() - .stream() - .flatMap(e -> new StringContent(e.getKey(), e.getValue()).getContainerEntries().stream()) - .collect(Collectors.toList()); - VolumeUtils.addContentToVolume(dockerClient, platformVolume, "/env", buildUserId, buildGroupId, envEntries); - - //add workarounds to environment. - if(!environment.containsKey("CNB_PLATFORM_API")) environment.put("CNB_PLATFORM_API", "0.4"); - // This a workaround for a bug in older lifecyle revisions. https://github.com/buildpacks/lifecycle/issues/339 - if(!environment.containsKey("CNB_REGISTRY_AUTH")) environment.put("CNB_REGISTRY_AUTH", "{}"); - } - - // util method for random suffix. - private String randomString(int length) { - return (new Random()).ints('a', 'z' + 1).limit(length) - .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(); - } - - public void tidyUp(){ - // remove volumes - // (note when/if we persist the cache between builds, we'll be more selective here over what we remove) - if (removeBuildCacheAfterBuild) { - VolumeUtils.removeVolume(dockerClient, buildCacheVolume); - } - if (removeLaunchCacheAfterBuild) { - VolumeUtils.removeVolume(dockerClient, launchCacheVolume); - } - VolumeUtils.removeVolume(dockerClient, applicationVolume); - VolumeUtils.removeVolume(dockerClient, outputVolume); - VolumeUtils.removeVolume(dockerClient, platformVolume); - - log.info("- build volumes tidied up"); - } - - - String getContainerForPhase(String args[], Integer runAsId){ - // create a container using builderImage that will invoke the creator process - String id = ContainerUtils.createContainer(this.dockerClient, this.builderImageName, Arrays.asList(args), - runAsId, environment, "label=disable", - new VolumeBind(buildCacheVolume, LifecyclePhaseFactory.BUILD_VOL_PATH), - new VolumeBind(launchCacheVolume, LifecyclePhaseFactory.LAUNCH_VOL_PATH), - new VolumeBind(applicationVolume, LifecyclePhaseFactory.APP_VOL_PATH), - new VolumeBind(platformVolume, LifecyclePhaseFactory.PLATFORM_VOL_PATH), - new VolumeBind(dockerSocket, LifecyclePhaseFactory.DOCKER_SOCKET_PATH), - new VolumeBind(outputVolume, LifecyclePhaseFactory.OUTPUT_VOL_PATH) - ); - - log.info("- mounted " + buildCacheVolume + " at " + BUILD_VOL_PATH); - log.info("- mounted " + launchCacheVolume + " at " + LAUNCH_VOL_PATH); - log.info("- mounted " + applicationVolume + " at " + APP_VOL_PATH); - log.info("- mounted " + platformVolume + " at " + PLATFORM_VOL_PATH); - log.info("- mounted " + dockerSocket + " at " + LifecyclePhaseFactory.DOCKER_SOCKET_PATH); - log.info("- mounted " + outputVolume + " at " + OUTPUT_VOL_PATH); - log.info("- build container id " + id); - - return id; - } - - public LifecyclePhase getCreator(){ - return new Creator(this); - } -} - diff --git a/client/src/main/java/dev/snowdrop/buildpack/utils/BuildpackMetadata.java b/client/src/main/java/dev/snowdrop/buildpack/utils/BuildpackMetadata.java index 6ca6cff..e930beb 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/utils/BuildpackMetadata.java +++ b/client/src/main/java/dev/snowdrop/buildpack/utils/BuildpackMetadata.java @@ -1,14 +1,38 @@ package dev.snowdrop.buildpack.utils; -import dev.snowdrop.buildpack.BuildpackException; +import java.util.ArrayList; +import java.util.List; + +import org.tomlj.Toml; +import org.tomlj.TomlArray; +import org.tomlj.TomlParseResult; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import dev.snowdrop.buildpack.BuildpackException; +import dev.snowdrop.buildpack.docker.ContainerUtils; + public class BuildpackMetadata { - public static String getRunImageFromMetadata(String json) throws BuildpackException { - // else, dig into metadata for runImage. + //used for builders created for platform 0.12 onwards + public static List getRunImageFromRunTOML(String imageReference) throws BuildpackException { + List runImages = new ArrayList<>(); + + byte[] runTomlData = ContainerUtils.getFileFromContainer(null, imageReference, "/cnb/run.toml"); + TomlParseResult analyzed = Toml.parse(new String(runTomlData)); + + TomlArray ta = analyzed.getArray("images"); + for(int i=0; i getSupportedPlatformsFromMetadata(String json) throws BuildpackException { + //dig into metadata for supported platforms. + ObjectMapper om = new ObjectMapper(); + om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try { + JsonNode root = om.readTree(json); + + List platforms = JsonUtils.getArray(root, "lifecycle/apis/platform/supported"); + + // if supported platform metadata is unparseable. + if(platforms==null){ + throw new Exception("Bad platform metadata in builder image"); + } + + return platforms; + } catch (Exception e) { + throw BuildpackException.launderThrowable(e); + } + } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/utils/JsonUtils.java b/client/src/main/java/dev/snowdrop/buildpack/utils/JsonUtils.java index a74cbea..0f6ab76 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/utils/JsonUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/utils/JsonUtils.java @@ -1,5 +1,9 @@ package dev.snowdrop.buildpack.utils; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + import com.fasterxml.jackson.databind.JsonNode; public class JsonUtils { @@ -13,5 +17,24 @@ public static String getValue(JsonNode root, String path) { return null; } return next.asText(); - } + } + public static List getArray(JsonNode root, String path) { + String[] parts = path.split("/"); + JsonNode next = root.get(parts[0]); + if (next != null && parts.length > 1) { + return getArray(next, path.substring(path.indexOf("/") + 1)); + } + if (next == null) { + return null; + } + if(next.isArray()){ + ArrayList vals = new ArrayList<>(); + Iterator els = next.elements(); + while(els.hasNext()){ + vals.add(els.next().asText()); + } + return vals; + } + return null; + } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/utils/LifecycleArgs.java b/client/src/main/java/dev/snowdrop/buildpack/utils/LifecycleArgs.java new file mode 100644 index 0000000..e49f64d --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/utils/LifecycleArgs.java @@ -0,0 +1,58 @@ +package dev.snowdrop.buildpack.utils; + +import java.util.ArrayList; +import java.util.List; + +public class LifecycleArgs { + + private final String lifecycle; + private final List args; + private final String imageName; + + public LifecycleArgs(String lifecycle, String[] args, String finalImageName) { + this.lifecycle = lifecycle; + + this.args = new ArrayList<>(); + for(String arg: args){ + this.args.add(arg); + } + this.imageName = finalImageName; + } + + public LifecycleArgs(String lifecycle, String finalImageName) { + this.lifecycle = lifecycle; + this.args = new ArrayList<>(); + this.imageName = finalImageName; + } + + public void addArg(String optName, String value){ + this.args.add(optName); + this.args.add(value); + } + + public void addArg(String optName){ + this.args.add(optName); + } + + + public List toList(){ + ArrayList allArgs = new ArrayList<>(); + allArgs.add(lifecycle); + allArgs.addAll(args); + if(imageName!=null) + allArgs.add(imageName); + return allArgs; + } + + public String[] toArray(){ + return toList().toArray(new String[]{}); + } + + public String toString(){ + return toList().toString(); + } + + + + +} diff --git a/client/src/main/java/dev/snowdrop/buildpack/utils/LifecycleMetadata.java b/client/src/main/java/dev/snowdrop/buildpack/utils/LifecycleMetadata.java new file mode 100644 index 0000000..500f8e0 --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/utils/LifecycleMetadata.java @@ -0,0 +1,61 @@ +package dev.snowdrop.buildpack.utils; + +import java.util.List; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import dev.snowdrop.buildpack.BuildpackException; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.docker.ImageUtils; +import dev.snowdrop.buildpack.docker.ImageUtils.ImageInfo; + +public class LifecycleMetadata { + + private List platformLevels; + private List buildpackLevels; + + public LifecycleMetadata(DockerConfig dc, ImageReference lifecycleImage) throws BuildpackException { + + // pull and inspect the builderImage to obtain builder metadata. + ImageUtils.pullImages(dc.getDockerClient(), + dc.getPullTimeout(), + lifecycleImage.getReference()); + + ImageInfo ii = ImageUtils.inspectImage(dc.getDockerClient(), + lifecycleImage.getReference()); + + String metadataJson = ii.labels.get("io.buildpacks.lifecycle.apis"); + + //dig into metadata for supported platforms. + ObjectMapper om = new ObjectMapper(); + om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try { + JsonNode root = om.readTree(metadataJson); + + platformLevels = JsonUtils.getArray(root, "/platform/supported"); + // if supported platform metadata is unparseable. + if(platformLevels==null){ + throw new Exception("Bad platform metadata in lifecycle image"); + } + + buildpackLevels = JsonUtils.getArray(root, "/buildpack/supported"); + // if supported platform metadata is unparseable. + if(buildpackLevels==null){ + throw new Exception("Bad buildpack metadata in lifecycle image"); + } + + } catch (Exception e) { + throw BuildpackException.launderThrowable(e); + } + } + + public List getSupportedPlatformLevels(){ + return platformLevels; + } + public List getSupportedBuildpackLevels(){ + return buildpackLevels; + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/config/CacheConfigTest.java b/client/src/test/java/dev/snowdrop/buildpack/config/CacheConfigTest.java new file mode 100644 index 0000000..2333fb0 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/config/CacheConfigTest.java @@ -0,0 +1,29 @@ +package dev.snowdrop.buildpack.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class CacheConfigTest { + @Test + void constructorTest(){ + CacheConfig c1 = new CacheConfig("fred", null); + assertTrue(c1.getDeleteCacheAfterBuild()); + assertEquals("fred", c1.getCacheVolumeName()); + + CacheConfig c2 = new CacheConfig(null, null); + assertTrue(c2.getDeleteCacheAfterBuild()); + assertNull(c2.getCacheVolumeName()); + + CacheConfig c3 = new CacheConfig("fish", false); + assertFalse(c3.getDeleteCacheAfterBuild()); + assertEquals("fish", c3.getCacheVolumeName()); + + CacheConfig c4 = new CacheConfig("stilettos", true); + assertTrue(c4.getDeleteCacheAfterBuild()); + assertEquals("stilettos", c4.getCacheVolumeName()); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/config/DockerConfigTest.java b/client/src/test/java/dev/snowdrop/buildpack/config/DockerConfigTest.java new file mode 100644 index 0000000..d7da285 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/config/DockerConfigTest.java @@ -0,0 +1,80 @@ +package dev.snowdrop.buildpack.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.github.dockerjava.api.DockerClient; + +@ExtendWith(MockitoExtension.class) +public class DockerConfigTest { + @Test + void checkTimeout() { + DockerConfig dc1 = new DockerConfig(null, null, null, null, null, null); + assertEquals(60, dc1.getPullTimeout()); + + DockerConfig dc2 = new DockerConfig(245017, null, null, null, null, null); + assertEquals(dc2.getPullTimeout(), 245017); + } + + @Test + void checkDockerHost() { + DockerConfig dc1 = new DockerConfig(null, null, null, null, null, null); + assertNotNull(dc1.getDockerHost()); + + DockerConfig dc2 = new DockerConfig(null, "tcp://stilettos", null, null, null, null); + assertEquals("tcp://stilettos", dc2.getDockerHost()); + } + + @Test + void checkDockerSocket() { + DockerConfig dc1 = new DockerConfig(null, null, null, null, null, null); + assertNotNull(dc1.getDockerSocket()); + + DockerConfig dc2 = new DockerConfig(null, "unix:///stilettos", null, null, null, null); + assertEquals("/stilettos", dc2.getDockerSocket()); + + DockerConfig dc3 = new DockerConfig(null, "tcp://stilettos", null, null, null, null); + assertEquals("/var/run/docker.sock", dc3.getDockerSocket()); + + DockerConfig dc4 = new DockerConfig(null, null, "fish", null, null, null); + assertEquals("fish", dc4.getDockerSocket()); + } + + @Test + void checkDockerNetwork() { + DockerConfig dc1 = new DockerConfig(null, null, null, "kitten", null, null); + assertEquals("kitten", dc1.getDockerNetwork()); + + DockerConfig dc2 = new DockerConfig(null, null, null, null, null, null); + assertNull(dc2.getDockerNetwork()); + } + + @Test + void checkUseDaemon() { + DockerConfig dc1 = new DockerConfig(null, null, null, null, null, null); + assertTrue(dc1.getUseDaemon()); + + DockerConfig dc2 = new DockerConfig(null, null, null, null, true, null); + assertTrue(dc2.getUseDaemon()); + + DockerConfig dc3 = new DockerConfig(null, null, null, null, false, null); + assertFalse(dc3.getUseDaemon()); + } + + @Test + void checkDockerClient(@Mock DockerClient dockerClient){ + DockerConfig dc1 = new DockerConfig(null, null, null, null, null, null); + assertNotNull(dc1.getDockerClient()); + + DockerConfig dc2 = new DockerConfig(null, null, null, null, null, dockerClient); + assertEquals(dockerClient, dc2.getDockerClient()); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/config/ImageReferenceTest.java b/client/src/test/java/dev/snowdrop/buildpack/config/ImageReferenceTest.java new file mode 100644 index 0000000..4e895cd --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/config/ImageReferenceTest.java @@ -0,0 +1,18 @@ +package dev.snowdrop.buildpack.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class ImageReferenceTest { + + @Test + void checkImageReference(){ + ImageReference ir1 = new ImageReference(null); + assertNull(ir1.getReference()); + + ImageReference ir2 = new ImageReference("wibble"); + assertEquals("wibble", ir2.getReference()); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/config/LogConfigTest.java b/client/src/test/java/dev/snowdrop/buildpack/config/LogConfigTest.java new file mode 100644 index 0000000..93471f8 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/config/LogConfigTest.java @@ -0,0 +1,42 @@ +package dev.snowdrop.buildpack.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import dev.snowdrop.buildpack.Logger; + +@ExtendWith(MockitoExtension.class) +public class LogConfigTest { + @Test + void checkLogLevel() { + LogConfig lc1 = new LogConfig(null, null, null); + assertEquals("info", lc1.getLogLevel()); + LogConfig lc2 = new LogConfig("debug", null, null); + assertEquals("debug", lc2.getLogLevel()); + } + + @Test + void checkUseTimestamps() { + LogConfig lc1 = new LogConfig(null, null, null); + assertTrue(lc1.getUseTimestamps()); + LogConfig lc2 = new LogConfig(null, true, null); + assertTrue(lc2.getUseTimestamps()); + LogConfig lc3 = new LogConfig(null, false, null); + assertFalse(lc3.getUseTimestamps()); + } + + @Test + void checkLogger(@Mock Logger logger) { + LogConfig lc1 = new LogConfig(null, null, null); + assertNotNull(lc1.getLogger()); + LogConfig lc2 = new LogConfig(null, null, logger); + assertEquals(logger, lc2.getLogger()); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/config/PlatformConfigTest.java b/client/src/test/java/dev/snowdrop/buildpack/config/PlatformConfigTest.java new file mode 100644 index 0000000..fd52787 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/config/PlatformConfigTest.java @@ -0,0 +1,56 @@ +package dev.snowdrop.buildpack.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +public class PlatformConfigTest { + @Test + void checkPlatformLevel(){ + PlatformConfig pc1 = new PlatformConfig(null, null, null, null); + assertNotNull(pc1.getPlatformLevel()); + + PlatformConfig pc2 = new PlatformConfig("0.7", null, null, null); + assertNotNull(pc2.getPlatformLevel()); + assertEquals("0.7", pc2.getPlatformLevel()); + } + + @Test + void checkEnv() { + PlatformConfig pc1 = new PlatformConfig(null, null, null, null); + assertNotNull(pc1.getEnvironment()); + + Map m = new HashMap<>(); + PlatformConfig pc2 = new PlatformConfig(null, null, m, null); + assertEquals(m, pc2.getEnvironment()); + } + + @Test + void checkLifecycleImage() { + PlatformConfig pc1 = new PlatformConfig(null, null, null, null); + assertNull(pc1.getLifecycleImage()); + + PlatformConfig pc2 = new PlatformConfig(null, new ImageReference("fish"), null, null); + assertNotNull(pc2.getLifecycleImage()); + assertEquals("fish", pc2.getLifecycleImage().getReference()); + } + + @Test + void checkTrustBuilder() { + PlatformConfig pc1 = new PlatformConfig(null, null, null, null); + assertNull(pc1.getTrustBuilder()); + + PlatformConfig pc2 = new PlatformConfig(null, null, null, true); + assertTrue(pc2.getTrustBuilder()); + + PlatformConfig pc3 = new PlatformConfig(null, null, null, false); + assertFalse(pc3.getTrustBuilder()); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerUtilsTest.java b/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerUtilsTest.java index af38d80..e87a953 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerUtilsTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerUtilsTest.java @@ -569,7 +569,7 @@ public InputStream getData() { }); verify(catcc).exec(); } - + @Test void addContentToContainerViaFile(@Mock DockerClient dc, @Mock CopyArchiveToContainerCmd catcc, diff --git a/client/src/test/java/dev/snowdrop/buildpack/docker/ImageUtilsTest.java b/client/src/test/java/dev/snowdrop/buildpack/docker/ImageUtilsTest.java index 38748a8..e08d391 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/docker/ImageUtilsTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/docker/ImageUtilsTest.java @@ -1,6 +1,5 @@ package dev.snowdrop.buildpack.docker; - import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.eq; diff --git a/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java b/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java index ed23c83..4a24a5a 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java @@ -2,62 +2,40 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.spi.FileSystemProvider; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; -import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.stubbing.Answer; import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.command.CopyArchiveToContainerCmd; -import com.github.dockerjava.api.command.CreateContainerCmd; -import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.CreateVolumeCmd; import com.github.dockerjava.api.command.InspectVolumeCmd; import com.github.dockerjava.api.command.InspectVolumeResponse; -import com.github.dockerjava.api.command.RemoveContainerCmd; import com.github.dockerjava.api.command.RemoveVolumeCmd; import com.github.dockerjava.api.exception.NotFoundException; -import com.github.dockerjava.api.model.Bind; -import com.github.dockerjava.api.model.HostConfig; import dev.snowdrop.buildpack.BuildpackException; @@ -105,6 +83,7 @@ void testRemoveVolume(@Mock DockerClient dc, @Mock RemoveVolumeCmd rvc) + @SuppressWarnings("resource") @Test void addContentToVolumeViaString(@Mock DockerClient dc ) { @@ -117,7 +96,7 @@ void addContentToVolumeViaString(@Mock DockerClient dc ) { scu.when(() -> ContainerUtils.createContainer(eq(dc), anyString(), ArgumentMatchers.any())).thenReturn(containerId); - boolean result = VolumeUtils.addContentToVolume(dc, volumeName, entryName, entryContent); + boolean result = VolumeUtils.addContentToVolume(dc, volumeName, "tianon/true", entryName, entryContent); assertTrue(result); scu.verify(() -> ContainerUtils.addContentToContainer(eq(dc), eq(containerId), anyString(), anyInt(), anyInt(), ArgumentMatchers.argThat(ce -> { @@ -139,6 +118,7 @@ void addContentToVolumeViaString(@Mock DockerClient dc ) { } + @SuppressWarnings("resource") @Test void addContentToContainerViaFile(@Mock DockerClient dc, @Mock File f, @@ -175,15 +155,14 @@ void addContentToContainerViaFile(@Mock DockerClient dc, scu.when(() -> ContainerUtils.createContainer(eq(dc), anyString(), ArgumentMatchers.any())).thenReturn(containerId); - boolean result = VolumeUtils.addContentToVolume(dc, volumeName, pathInVolume, f); + boolean result = VolumeUtils.addContentToVolume(dc, volumeName, "tianon/true", pathInVolume, f); assertTrue(result); scu.verify(() -> ContainerUtils.addContentToContainer(eq(dc), eq(containerId), anyString(), anyInt(), anyInt(), ArgumentMatchers.argThat(ce -> { assertEquals("/"+fileName, ce.getPath()); assertEquals(fileSize, ce.getSize()); - try{ - InputStream is = ce.getDataSupplier().getData(); + try (InputStream is = ce.getDataSupplier().getData();) { //TODO: not sure why, but mockito invokes the verify twice, but the underlying stream is already depleted on the 2nd. // since it doesn't apply to the real code, just gate on when the stream has data to only check it the first time. diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/ContainerStatusTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/ContainerStatusTest.java new file mode 100644 index 0000000..6f16192 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/ContainerStatusTest.java @@ -0,0 +1,14 @@ +package dev.snowdrop.buildpack.lifecycle; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class ContainerStatusTest { + @Test + void testContainerStatus(){ + ContainerStatus cs1 = ContainerStatus.of(66, "fish"); + assertEquals(66,cs1.getRc()); + assertEquals("fish", cs1.getContainerId()); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutorTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutorTest.java new file mode 100644 index 0000000..572f86e --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutorTest.java @@ -0,0 +1,582 @@ +package dev.snowdrop.buildpack.lifecycle; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mockStatic; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.commons.util.ReflectionUtils; +import org.mockito.Answers; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.BuildConfig; +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.Logger; +import dev.snowdrop.buildpack.config.CacheConfig; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.config.PlatformConfig; +import dev.snowdrop.buildpack.docker.ContainerUtils; +import dev.snowdrop.buildpack.docker.ImageUtils; +import dev.snowdrop.buildpack.lifecycle.phases.Analyzer; +import dev.snowdrop.buildpack.lifecycle.phases.Builder; +import dev.snowdrop.buildpack.lifecycle.phases.Creator; +import dev.snowdrop.buildpack.lifecycle.phases.Detector; +import dev.snowdrop.buildpack.lifecycle.phases.Exporter; +import dev.snowdrop.buildpack.lifecycle.phases.Extender; +import dev.snowdrop.buildpack.lifecycle.phases.Restorer; + +@ExtendWith(MockitoExtension.class) +public class LifecycleExecutorTest { + + @SuppressWarnings("resource") + @Test + void testPre7( + @Mock LifecyclePhaseFactory lifecycleFactory, + @Mock BuilderImage extendedBuilder, + @Mock BuilderImage origBuilder, + @Mock LogConfig logConfig, + @Mock CacheConfig buildCacheConfig, + @Mock CacheConfig launchCacheConfig, + @Mock CacheConfig kanikoCacheConfig, + @Mock PlatformConfig platformConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger, + @Mock BuildConfig config, + @Mock Analyzer analyzer, + @Mock Builder builder, + @Mock Creator creator, + @Mock Detector detector, + @Mock Exporter exporter, + @Mock Extender extender, + @Mock Restorer restorer + ){ + String PLATFORM_LEVEL="0.6"; + String OUTPUT_IMAGE="fish"; + boolean HAS_EXTENSIONS=false; + + List runImages = new ArrayList<>(); + runImages.add("run-image"); + + lenient().when(logConfig.getUseTimestamps()).thenReturn(true); + lenient().when(logConfig.getLogger()).thenReturn(logger); + + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(dockerConfig.getPullTimeout()).thenReturn(66); + + lenient().when(config.getDockerConfig()).thenReturn(dockerConfig); + lenient().when(config.getBuildCacheConfig()).thenReturn(buildCacheConfig); + lenient().when(config.getLaunchCacheConfig()).thenReturn(launchCacheConfig); + lenient().when(config.getKanikoCacheConfig()).thenReturn(kanikoCacheConfig); + lenient().when(config.getPlatformConfig()).thenReturn(platformConfig); + lenient().when(config.getLogConfig()).thenReturn(logConfig); + lenient().when(config.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + lenient().doNothing().when(lifecycleFactory).createVolumes(any()); + lenient().doNothing().when(lifecycleFactory).tidyUp(); + lenient().when(lifecycleFactory.getAnalyzer()).thenReturn(analyzer); + lenient().when(lifecycleFactory.getDetector()).thenReturn(detector); + lenient().when(lifecycleFactory.getBuilder()).thenReturn(builder); + lenient().when(lifecycleFactory.getCreator()).thenReturn(creator); + lenient().when(lifecycleFactory.getExporter(false)).thenReturn(exporter); + lenient().when(lifecycleFactory.getExtender(any())).thenReturn(extender); + lenient().when(lifecycleFactory.getRestorer()).thenReturn(restorer); + lenient().when(lifecycleFactory.getBuilderImage()).thenReturn(extendedBuilder); + + lenient().when(extendedBuilder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(extendedBuilder.getRunImages(any())).thenReturn(runImages); + + lenient().when(analyzer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"analyzer-id")); + lenient().when(detector.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"detector-id")); + lenient().when(detector.getAnalyzedToml()).thenReturn("[run-image]\n reference=newfish\n extend=false".getBytes()); + lenient().when(builder.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"builder-id")); + lenient().when(creator.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"creator-id")); + lenient().when(exporter.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"exporter-id")); + lenient().when(extender.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"extender-id")); + lenient().when(restorer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"restorer-id")); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class); + MockedStatic imageUtils = mockStatic(ImageUtils.class)) { + + containerUtils.when(() -> ContainerUtils.removeContainer(eq(dockerClient), any())).thenAnswer(Answers.RETURNS_DEFAULTS); + imageUtils.when(() -> ImageUtils.pullImages(dockerClient, 66, "newfish")).thenAnswer(Answers.RETURNS_DEFAULTS); + + LifecycleExecutor le = new LifecycleExecutor(config, extendedBuilder, origBuilder, PLATFORM_LEVEL); + + try{ + Field factory = ReflectionUtils.findFields(LifecycleExecutor.class, + f->f.getName().equals("factory"), + ReflectionUtils.HierarchyTraversalMode.TOP_DOWN) + .get(0); + factory.setAccessible(true); + factory.set(le, lifecycleFactory); + }catch(Exception e){ + fail(); + } + + + le.execute(); + + //ensure correct phases driven in expected order for this platform revision. + InOrder order = Mockito.inOrder(detector,analyzer,restorer,builder,exporter); + order.verify(detector).runPhase(logger, true); + order.verify(analyzer).runPhase(logger, true); + order.verify(restorer).runPhase(logger, true); + order.verify(builder).runPhase(logger, true); + order.verify(exporter).runPhase(logger, true); + } + } + + @SuppressWarnings("resource") + @Test + void test7Onwards( + @Mock LifecyclePhaseFactory lifecycleFactory, + @Mock BuilderImage extendedBuilder, + @Mock BuilderImage origBuilder, + @Mock LogConfig logConfig, + @Mock CacheConfig buildCacheConfig, + @Mock CacheConfig launchCacheConfig, + @Mock CacheConfig kanikoCacheConfig, + @Mock PlatformConfig platformConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger, + @Mock BuildConfig config, + @Mock Analyzer analyzer, + @Mock Builder builder, + @Mock Creator creator, + @Mock Detector detector, + @Mock Exporter exporter, + @Mock Extender extender, + @Mock Restorer restorer + ){ + String PLATFORM_LEVEL="0.7"; + String OUTPUT_IMAGE="fish"; + boolean HAS_EXTENSIONS=false; + + List runImages = new ArrayList<>(); + runImages.add("run-image"); + + lenient().when(logConfig.getUseTimestamps()).thenReturn(true); + lenient().when(logConfig.getLogger()).thenReturn(logger); + + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(dockerConfig.getPullTimeout()).thenReturn(66); + + lenient().when(config.getDockerConfig()).thenReturn(dockerConfig); + lenient().when(config.getBuildCacheConfig()).thenReturn(buildCacheConfig); + lenient().when(config.getLaunchCacheConfig()).thenReturn(launchCacheConfig); + lenient().when(config.getKanikoCacheConfig()).thenReturn(kanikoCacheConfig); + lenient().when(config.getPlatformConfig()).thenReturn(platformConfig); + lenient().when(config.getLogConfig()).thenReturn(logConfig); + lenient().when(config.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + lenient().doNothing().when(lifecycleFactory).createVolumes(any()); + lenient().doNothing().when(lifecycleFactory).tidyUp(); + lenient().when(lifecycleFactory.getAnalyzer()).thenReturn(analyzer); + lenient().when(lifecycleFactory.getDetector()).thenReturn(detector); + lenient().when(lifecycleFactory.getBuilder()).thenReturn(builder); + lenient().when(lifecycleFactory.getCreator()).thenReturn(creator); + lenient().when(lifecycleFactory.getExporter(false)).thenReturn(exporter); + lenient().when(lifecycleFactory.getExtender(any())).thenReturn(extender); + lenient().when(lifecycleFactory.getRestorer()).thenReturn(restorer); + lenient().when(lifecycleFactory.getBuilderImage()).thenReturn(extendedBuilder); + + lenient().when(extendedBuilder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(extendedBuilder.getRunImages(any())).thenReturn(runImages); + + lenient().when(analyzer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"analyzer-id")); + lenient().when(detector.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"detector-id")); + lenient().when(detector.getAnalyzedToml()).thenReturn("[run-image]\n reference=newfish\n extend=false".getBytes()); + lenient().when(builder.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"builder-id")); + lenient().when(creator.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"creator-id")); + lenient().when(exporter.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"exporter-id")); + lenient().when(extender.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"extender-id")); + lenient().when(restorer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"restorer-id")); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class); + MockedStatic imageUtils = mockStatic(ImageUtils.class)) { + + containerUtils.when(() -> ContainerUtils.removeContainer(eq(dockerClient), any())).thenAnswer(Answers.RETURNS_DEFAULTS); + imageUtils.when(() -> ImageUtils.pullImages(dockerClient, 66, "newfish")).thenAnswer(Answers.RETURNS_DEFAULTS); + + LifecycleExecutor le = new LifecycleExecutor(config, extendedBuilder, origBuilder, PLATFORM_LEVEL); + + try{ + Field factory = ReflectionUtils.findFields(LifecycleExecutor.class, + f->f.getName().equals("factory"), + ReflectionUtils.HierarchyTraversalMode.TOP_DOWN) + .get(0); + factory.setAccessible(true); + factory.set(le, lifecycleFactory); + }catch(Exception e){ + fail(); + } + + + le.execute(); + + //ensure correct phases driven in expected order for this platform revision. + InOrder order = Mockito.inOrder(detector,analyzer,restorer,builder,exporter); + order.verify(analyzer).runPhase(logger, true); + order.verify(detector).runPhase(logger, true); + order.verify(restorer).runPhase(logger, true); + order.verify(builder).runPhase(logger, true); + order.verify(exporter).runPhase(logger, true); + } + } + + @SuppressWarnings("resource") + @Test + void test10OnwardsNoXtns( + @Mock LifecyclePhaseFactory lifecycleFactory, + @Mock BuilderImage extendedBuilder, + @Mock BuilderImage origBuilder, + @Mock LogConfig logConfig, + @Mock CacheConfig buildCacheConfig, + @Mock CacheConfig launchCacheConfig, + @Mock CacheConfig kanikoCacheConfig, + @Mock PlatformConfig platformConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger, + @Mock BuildConfig config, + @Mock Analyzer analyzer, + @Mock Builder builder, + @Mock Creator creator, + @Mock Detector detector, + @Mock Exporter exporter, + @Mock Extender extender, + @Mock Restorer restorer + ){ + String PLATFORM_LEVEL="0.10"; + String OUTPUT_IMAGE="fish"; + boolean HAS_EXTENSIONS=false; + + List runImages = new ArrayList<>(); + runImages.add("run-image"); + + lenient().when(logConfig.getUseTimestamps()).thenReturn(true); + lenient().when(logConfig.getLogger()).thenReturn(logger); + + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(dockerConfig.getPullTimeout()).thenReturn(66); + + lenient().when(config.getDockerConfig()).thenReturn(dockerConfig); + lenient().when(config.getBuildCacheConfig()).thenReturn(buildCacheConfig); + lenient().when(config.getLaunchCacheConfig()).thenReturn(launchCacheConfig); + lenient().when(config.getKanikoCacheConfig()).thenReturn(kanikoCacheConfig); + lenient().when(config.getPlatformConfig()).thenReturn(platformConfig); + lenient().when(config.getLogConfig()).thenReturn(logConfig); + lenient().when(config.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + lenient().doNothing().when(lifecycleFactory).createVolumes(any()); + lenient().doNothing().when(lifecycleFactory).tidyUp(); + lenient().when(lifecycleFactory.getAnalyzer()).thenReturn(analyzer); + lenient().when(lifecycleFactory.getDetector()).thenReturn(detector); + lenient().when(lifecycleFactory.getBuilder()).thenReturn(builder); + lenient().when(lifecycleFactory.getCreator()).thenReturn(creator); + lenient().when(lifecycleFactory.getExporter(false)).thenReturn(exporter); + lenient().when(lifecycleFactory.getExtender(any())).thenReturn(extender); + lenient().when(lifecycleFactory.getRestorer()).thenReturn(restorer); + lenient().when(lifecycleFactory.getBuilderImage()).thenReturn(extendedBuilder); + + lenient().when(extendedBuilder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(extendedBuilder.getRunImages(any())).thenReturn(runImages); + + lenient().when(analyzer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"analyzer-id")); + lenient().when(detector.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"detector-id")); + lenient().when(detector.getAnalyzedToml()).thenReturn("[run-image]\n reference=newfish\n extend=false".getBytes()); + lenient().when(builder.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"builder-id")); + lenient().when(creator.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"creator-id")); + lenient().when(exporter.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"exporter-id")); + lenient().when(extender.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"extender-id")); + lenient().when(restorer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"restorer-id")); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class); + MockedStatic imageUtils = mockStatic(ImageUtils.class)) { + + containerUtils.when(() -> ContainerUtils.removeContainer(eq(dockerClient), any())).thenAnswer(Answers.RETURNS_DEFAULTS); + imageUtils.when(() -> ImageUtils.pullImages(dockerClient, 66, "newfish")).thenAnswer(Answers.RETURNS_DEFAULTS); + + LifecycleExecutor le = new LifecycleExecutor(config, extendedBuilder, origBuilder, PLATFORM_LEVEL); + + try{ + Field factory = ReflectionUtils.findFields(LifecycleExecutor.class, + f->f.getName().equals("factory"), + ReflectionUtils.HierarchyTraversalMode.TOP_DOWN) + .get(0); + factory.setAccessible(true); + factory.set(le, lifecycleFactory); + }catch(Exception e){ + fail(); + } + + + le.execute(); + + //ensure correct phases driven in expected order for this platform revision. + InOrder order = Mockito.inOrder(detector,analyzer,restorer,builder,exporter); + order.verify(analyzer).runPhase(logger, true); + order.verify(detector).runPhase(logger, true); + order.verify(restorer).runPhase(logger, true); + order.verify(builder).runPhase(logger, true); + order.verify(exporter).runPhase(logger, true); + } + } + + @SuppressWarnings("resource") + @Test + void test10OnwardsXtns( + @Mock LifecyclePhaseFactory lifecycleFactory, + @Mock BuilderImage extendedBuilder, + @Mock BuilderImage origBuilder, + @Mock LogConfig logConfig, + @Mock CacheConfig buildCacheConfig, + @Mock CacheConfig launchCacheConfig, + @Mock CacheConfig kanikoCacheConfig, + @Mock PlatformConfig platformConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger, + @Mock BuildConfig config, + @Mock Analyzer analyzer, + @Mock Builder builder, + @Mock Creator creator, + @Mock Detector detector, + @Mock Exporter exporter, + @Mock Extender runExtender, + @Mock Extender buildExtender, + @Mock Restorer restorer + ){ + String PLATFORM_LEVEL="0.10"; + String OUTPUT_IMAGE="fish"; + boolean HAS_EXTENSIONS=true; + + List runImages = new ArrayList<>(); + runImages.add("run-image"); + + lenient().when(logConfig.getUseTimestamps()).thenReturn(true); + lenient().when(logConfig.getLogger()).thenReturn(logger); + + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(dockerConfig.getPullTimeout()).thenReturn(66); + + lenient().when(config.getDockerConfig()).thenReturn(dockerConfig); + lenient().when(config.getBuildCacheConfig()).thenReturn(buildCacheConfig); + lenient().when(config.getLaunchCacheConfig()).thenReturn(launchCacheConfig); + lenient().when(config.getKanikoCacheConfig()).thenReturn(kanikoCacheConfig); + lenient().when(config.getPlatformConfig()).thenReturn(platformConfig); + lenient().when(config.getLogConfig()).thenReturn(logConfig); + lenient().when(config.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + lenient().doNothing().when(lifecycleFactory).createVolumes(any()); + lenient().doNothing().when(lifecycleFactory).tidyUp(); + lenient().when(lifecycleFactory.getAnalyzer()).thenReturn(analyzer); + lenient().when(lifecycleFactory.getDetector()).thenReturn(detector); + lenient().when(lifecycleFactory.getBuilder()).thenReturn(builder); + lenient().when(lifecycleFactory.getCreator()).thenReturn(creator); + lenient().when(lifecycleFactory.getExporter(false)).thenReturn(exporter); + lenient().when(lifecycleFactory.getRunImageExtender()).thenReturn(runExtender); + lenient().when(lifecycleFactory.getBuildImageExtender()).thenReturn(buildExtender); + lenient().when(lifecycleFactory.getRestorer()).thenReturn(restorer); + lenient().when(lifecycleFactory.getBuilderImage()).thenReturn(extendedBuilder); + lenient().when(lifecycleFactory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + lenient().when(lifecycleFactory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().when(extendedBuilder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(extendedBuilder.getRunImages(any())).thenReturn(runImages); + + lenient().when(analyzer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"analyzer-id")); + lenient().when(detector.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"detector-id")); + lenient().when(detector.getAnalyzedToml()).thenReturn("[run-image]\nreference = \"newfish\"\nextend = false".getBytes()); + lenient().when(builder.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"builder-id")); + lenient().when(creator.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"creator-id")); + lenient().when(exporter.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"exporter-id")); + lenient().when(runExtender.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"run-extender-id")); + lenient().when(buildExtender.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"build-extender-id")); + lenient().when(restorer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"restorer-id")); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class); + MockedStatic imageUtils = mockStatic(ImageUtils.class)) { + + containerUtils.when(() -> ContainerUtils.removeContainer(eq(dockerClient), any())).thenAnswer(Answers.RETURNS_DEFAULTS); + imageUtils.when(() -> ImageUtils.pullImages(dockerClient, 66, "newfish")).thenAnswer(Answers.RETURNS_DEFAULTS); + + LifecycleExecutor le = new LifecycleExecutor(config, extendedBuilder, origBuilder, PLATFORM_LEVEL); + + try{ + Field factory = ReflectionUtils.findFields(LifecycleExecutor.class, + f->f.getName().equals("factory"), + ReflectionUtils.HierarchyTraversalMode.TOP_DOWN) + .get(0); + factory.setAccessible(true); + factory.set(le, lifecycleFactory); + }catch(Exception e){ + fail(); + } + + + le.execute(); + + //check that we swapped to the new run image by reading the toml. + assertEquals("newfish", runImages.get(0)); + + //ensure correct phases driven in expected order for this platform revision. + InOrder order = Mockito.inOrder(detector,analyzer,restorer,buildExtender,exporter); + order.verify(analyzer).runPhase(logger, true); + order.verify(detector).runPhase(logger, true); + order.verify(restorer).runPhase(logger, true); + order.verify(buildExtender).runPhase(logger, true); + order.verify(exporter).runPhase(logger, true); + } + } + + @SuppressWarnings("resource") + @Test + void test12OnwardsXtns( + @Mock LifecyclePhaseFactory lifecycleFactory, + @Mock BuilderImage extendedBuilder, + @Mock BuilderImage origBuilder, + @Mock LogConfig logConfig, + @Mock CacheConfig buildCacheConfig, + @Mock CacheConfig launchCacheConfig, + @Mock CacheConfig kanikoCacheConfig, + @Mock PlatformConfig platformConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger, + @Mock BuildConfig config, + @Mock Analyzer analyzer, + @Mock Builder builder, + @Mock Creator creator, + @Mock Detector detector, + @Mock Exporter exporter, + @Mock Extender runExtender, + @Mock Extender buildExtender, + @Mock Restorer restorer + ){ + String PLATFORM_LEVEL="0.12"; + String OUTPUT_IMAGE="fish"; + boolean HAS_EXTENSIONS=true; + + List runImages = new ArrayList<>(); + runImages.add("run-image"); + + lenient().when(logConfig.getUseTimestamps()).thenReturn(true); + lenient().when(logConfig.getLogger()).thenReturn(logger); + + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(dockerConfig.getPullTimeout()).thenReturn(66); + + lenient().when(config.getDockerConfig()).thenReturn(dockerConfig); + lenient().when(config.getBuildCacheConfig()).thenReturn(buildCacheConfig); + lenient().when(config.getLaunchCacheConfig()).thenReturn(launchCacheConfig); + lenient().when(config.getKanikoCacheConfig()).thenReturn(kanikoCacheConfig); + lenient().when(config.getPlatformConfig()).thenReturn(platformConfig); + lenient().when(config.getLogConfig()).thenReturn(logConfig); + lenient().when(config.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + lenient().doNothing().when(lifecycleFactory).createVolumes(any()); + lenient().doNothing().when(lifecycleFactory).tidyUp(); + lenient().when(lifecycleFactory.getAnalyzer()).thenReturn(analyzer); + lenient().when(lifecycleFactory.getDetector()).thenReturn(detector); + lenient().when(lifecycleFactory.getBuilder()).thenReturn(builder); + lenient().when(lifecycleFactory.getCreator()).thenReturn(creator); + lenient().when(lifecycleFactory.getExporter(true)).thenReturn(exporter); + lenient().when(lifecycleFactory.getRunImageExtender()).thenReturn(runExtender); + lenient().when(lifecycleFactory.getBuildImageExtender()).thenReturn(buildExtender); + lenient().when(lifecycleFactory.getRestorer()).thenReturn(restorer); + lenient().when(lifecycleFactory.getBuilderImage()).thenReturn(extendedBuilder); + lenient().when(lifecycleFactory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + lenient().when(lifecycleFactory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().when(extendedBuilder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(extendedBuilder.getRunImages(any())).thenReturn(runImages); + + lenient().when(analyzer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"analyzer-id")); + lenient().when(detector.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"detector-id")); + lenient().when(detector.getAnalyzedToml()).thenReturn("[run-image]\nreference = \"newfish\"\nextend = true".getBytes()); + lenient().when(builder.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"builder-id")); + lenient().when(creator.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"creator-id")); + lenient().when(exporter.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"exporter-id")); + lenient().when(runExtender.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"run-extender-id")); + lenient().when(buildExtender.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"build-extender-id")); + lenient().when(restorer.runPhase(logger, true)).thenReturn(ContainerStatus.of(0,"restorer-id")); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class); + MockedStatic imageUtils = mockStatic(ImageUtils.class)) { + + containerUtils.when(() -> ContainerUtils.removeContainer(eq(dockerClient), any())).thenAnswer(Answers.RETURNS_DEFAULTS); + imageUtils.when(() -> ImageUtils.pullImages(dockerClient, 66, "newfish")).thenAnswer(Answers.RETURNS_DEFAULTS); + + LifecycleExecutor le = new LifecycleExecutor(config, extendedBuilder, origBuilder, PLATFORM_LEVEL); + + try{ + Field factory = ReflectionUtils.findFields(LifecycleExecutor.class, + f->f.getName().equals("factory"), + ReflectionUtils.HierarchyTraversalMode.TOP_DOWN) + .get(0); + factory.setAccessible(true); + factory.set(le, lifecycleFactory); + }catch(Exception e){ + fail(); + } + + + le.execute(); + + //check that we swapped to the new run image by reading the toml. + assertEquals("newfish", runImages.get(0)); + + //ensure correct phases driven in expected order for this platform revision. + InOrder order = Mockito.inOrder(detector,analyzer,restorer,buildExtender,runExtender,exporter); + order.verify(analyzer).runPhase(logger, true); + order.verify(detector).runPhase(logger, true); + order.verify(restorer).runPhase(logger, true); + order.verify(buildExtender).runPhase(logger, true); + order.verify(runExtender).runPhase(logger, true); + order.verify(exporter).runPhase(logger, true); + } + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/AnalzyerTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/AnalzyerTest.java new file mode 100644 index 0000000..b1d49d9 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/AnalzyerTest.java @@ -0,0 +1,373 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.Logger; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.lifecycle.Version; + +@ExtendWith(MockitoExtension.class) +public class AnalzyerTest { + + @Captor + ArgumentCaptor argsCaptor; + + @Test + void testPre9(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.7"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Analyzer a = new Analyzer(factory); + + ContainerStatus cs = a.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/analyzer", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify no launch cache for a pre7 run + assertFalse(argList.contains("-launch-cache")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void testDisableTimestamps(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) + { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Analyzer a = new Analyzer(factory); + + ContainerStatus cs = a.runPhase(logger, false); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/analyzer", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify launch cache for a 9 onwards run + //launch cache only valid with daemon mode. + assertFalse(argList.contains("-launch-cache")); + //verify dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(false); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void test9OnwardsWithoutDaemon(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) + { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Analyzer a = new Analyzer(factory); + + ContainerStatus cs = a.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/analyzer", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify launch cache for a 9 onwards run + //launch cache only valid with daemon mode. + assertFalse(argList.contains("-launch-cache")); + //verify dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void test9OnwardsWithDaemon(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) + { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=true; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Analyzer a = new Analyzer(factory); + + ContainerStatus cs = a.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/analyzer", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify launch cache for a 9 onwards run + assertTrue(argList.contains("-launch-cache")); + //verify dameon + assertTrue(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/BuilderTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/BuilderTest.java new file mode 100644 index 0000000..9b443e6 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/BuilderTest.java @@ -0,0 +1,199 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.Logger; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.lifecycle.Version; + +@ExtendWith(MockitoExtension.class) +public class BuilderTest { + + @Captor + ArgumentCaptor argsCaptor; + + @Test + void testBuilder(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.7"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Builder b = new Builder(factory); + + ContainerStatus cs = b.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/builder", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void testDisableTimestamps(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) + { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Builder b = new Builder(factory); + + ContainerStatus cs = b.runPhase(logger, false); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/builder", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(false); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/CreatorTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/CreatorTest.java new file mode 100644 index 0000000..408b216 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/CreatorTest.java @@ -0,0 +1,373 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.Logger; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.lifecycle.Version; + +@ExtendWith(MockitoExtension.class) +public class CreatorTest { + + @Captor + ArgumentCaptor argsCaptor; + + @Test + void testPre9(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.7"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Creator c = new Creator(factory); + + ContainerStatus cs = c.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/creator", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify no launch cache for a pre7 run + assertFalse(argList.contains("-launch-cache")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(0)); + } + + @Test + void testDisableTimestamps(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) + { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Creator c = new Creator(factory); + + ContainerStatus cs = c.runPhase(logger, false); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/creator", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify launch cache for a 9 onwards run + //launch cache only valid with daemon mode. + assertFalse(argList.contains("-launch-cache")); + //verify dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(false); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(0)); + } + + @Test + void test9OnwardsWithoutDaemon(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) + { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Creator c = new Creator(factory); + + ContainerStatus cs = c.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/creator", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify launch cache for a 9 onwards run + //launch cache only valid with daemon mode. + assertFalse(argList.contains("-launch-cache")); + //verify dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(0)); + } + + @Test + void test9OnwardsWithDaemon(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) + { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=true; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Creator c = new Creator(factory); + + ContainerStatus cs = c.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/creator", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify launch cache for a 9 onwards run + assertTrue(argList.contains("-launch-cache")); + //verify dameon + assertTrue(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(0)); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/DetectorTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/DetectorTest.java new file mode 100644 index 0000000..878a76c --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/DetectorTest.java @@ -0,0 +1,681 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.Logger; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.docker.ContainerUtils; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.lifecycle.Version; + +@ExtendWith(MockitoExtension.class) +public class DetectorTest { + + @Captor + ArgumentCaptor argsCaptor; + + @SuppressWarnings("resource") + @Test + void testPre10(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.7"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + boolean HAS_EXTENSIONS=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class)) { + Detector d = new Detector(factory); + + containerUtils.when(() -> ContainerUtils.getFileFromContainer(eq(dockerClient), any(), any())).thenReturn("EmptyTOML".getBytes()); + + ContainerStatus cs = d.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + assertEquals("EmptyTOML", new String(d.getAnalyzedToml())); + } + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/detector", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify no generated for a pre 0.10 run. + assertFalse(argList.contains("-generated")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @SuppressWarnings("resource") + @Test + void testPre10WithExtensions(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.7"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + boolean HAS_EXTENSIONS=true; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class)) { + Detector d = new Detector(factory); + + containerUtils.when(() -> ContainerUtils.getFileFromContainer(eq(dockerClient), any(), any())).thenReturn("EmptyTOML".getBytes()); + + ContainerStatus cs = d.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + assertEquals("EmptyTOML", new String(d.getAnalyzedToml())); + } + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/detector", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify no generated for a pre 0.10 run. + assertFalse(argList.contains("-generated")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @SuppressWarnings("resource") + @Test + void test10WithExtensions(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.10"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + boolean HAS_EXTENSIONS=true; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class)) { + Detector d = new Detector(factory); + + containerUtils.when(() -> ContainerUtils.getFileFromContainer(eq(dockerClient), any(), any())).thenReturn("EmptyTOML".getBytes()); + + ContainerStatus cs = d.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + assertEquals("EmptyTOML", new String(d.getAnalyzedToml())); + } + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/detector", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify generated for a 0.10 run with extensions + assertTrue(argList.contains("-generated")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @SuppressWarnings("resource") + @Test + void test12WithExtensions(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.12"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + boolean HAS_EXTENSIONS=true; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class)) { + Detector d = new Detector(factory); + + containerUtils.when(() -> ContainerUtils.getFileFromContainer(eq(dockerClient), any(), any())).thenReturn("EmptyTOML".getBytes()); + + ContainerStatus cs = d.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + assertEquals("EmptyTOML", new String(d.getAnalyzedToml())); + } + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/detector", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify generated for a 0.10 run with extensions + assertTrue(argList.contains("-generated")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + //verify run arg is present + assertTrue(argList.contains("-run")); + assertTrue(argList.contains("/cnb/run.toml")); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @SuppressWarnings("resource") + @Test + void test10WithoutExtensions(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.10"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + boolean HAS_EXTENSIONS=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class)) { + Detector d = new Detector(factory); + + containerUtils.when(() -> ContainerUtils.getFileFromContainer(eq(dockerClient), any(), any())).thenReturn("EmptyTOML".getBytes()); + + ContainerStatus cs = d.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + assertEquals("EmptyTOML", new String(d.getAnalyzedToml())); + } + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/detector", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify generated for a 0.10 run without extensions + assertFalse(argList.contains("-generated")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @SuppressWarnings("resource") + @Test + void test10WithDaemon(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.10"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=true; + boolean HAS_EXTENSIONS=true; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class)) { + Detector d = new Detector(factory); + + containerUtils.when(() -> ContainerUtils.getFileFromContainer(eq(dockerClient), any(), any())).thenReturn("EmptyTOML".getBytes()); + + ContainerStatus cs = d.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + assertEquals("EmptyTOML", new String(d.getAnalyzedToml())); + } + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/detector", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify generated for a 0.10 run with extensions + assertTrue(argList.contains("-generated")); + //verify dameon + assertTrue(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @SuppressWarnings("resource") + @Test + void testDisableTimestamps(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.10"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=true; + boolean HAS_EXTENSIONS=true; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class)) { + Detector d = new Detector(factory); + + containerUtils.when(() -> ContainerUtils.getFileFromContainer(eq(dockerClient), any(), any())).thenReturn("EmptyTOML".getBytes()); + + ContainerStatus cs = d.runPhase(logger, false); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + assertEquals("EmptyTOML", new String(d.getAnalyzedToml())); + } + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/detector", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify generated for a 0.10 run with extensions + assertTrue(argList.contains("-generated")); + //verify dameon + assertTrue(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(false); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExporterTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExporterTest.java new file mode 100644 index 0000000..7f59240 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExporterTest.java @@ -0,0 +1,534 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.Logger; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.lifecycle.Version; + +@ExtendWith(MockitoExtension.class) +public class ExporterTest { + + @Captor + ArgumentCaptor argsCaptor; + + @Test + void testPre7(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.6"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Exporter e = new Exporter(factory, false); + + ContainerStatus cs = e.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/exporter", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify run-image is used for a pre7 run + assertTrue(argList.contains("-run-image")); + assertFalse(argList.contains("-run")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void test7Onwards(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.7"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Exporter e = new Exporter(factory, false); + + ContainerStatus cs = e.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/exporter", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify run-image is not used for a 7 onwards run + assertFalse(argList.contains("-run-image")); + assertFalse(argList.contains("-run")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void test12OnwardsNoRunExt(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.12"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Exporter e = new Exporter(factory, false); + + ContainerStatus cs = e.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/exporter", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify run is not used for a 12 run with no run extns + assertFalse(argList.contains("-run-image")); + assertFalse(argList.contains("-run")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void test12OnwardsRunExt(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.12"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Exporter e = new Exporter(factory, true); + + ContainerStatus cs = e.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/exporter", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify run is not used for a 12 run with no run extns + assertFalse(argList.contains("-run-image")); + assertTrue(argList.contains("-run")); + //verify no dameon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void testDisableTimestamps(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) + { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + + Exporter e = new Exporter(factory, false); + + ContainerStatus cs = e.runPhase(logger, false); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/exporter", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify daemon + assertFalse(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(false); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void testWithDaemon(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) + { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=true; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Exporter e = new Exporter(factory, false); + + ContainerStatus cs = e.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/exporter", args[0]); + assertEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify dameon + assertTrue(argList.contains("-daemon")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExtenderTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExtenderTest.java new file mode 100644 index 0000000..ef554b7 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExtenderTest.java @@ -0,0 +1,284 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.Logger; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.lifecycle.Version; + +@ExtendWith(MockitoExtension.class) +public class ExtenderTest { + + @Captor + ArgumentCaptor argsCaptor; + + @Test + void testPre12(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.10"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Extender e = new Extender(factory, null); + + ContainerStatus cs = e.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/extender", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify kind not present in pre12 + assertFalse(argList.contains("-kind")); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(0)); //extender always runs as root + } + + @Test + void test12OnwardsBuild(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.12"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Extender e = new Extender(factory, "build"); + + ContainerStatus cs = e.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/extender", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify kind not present in pre12 + assertTrue(argList.contains("-kind")); + assertEquals("build", argList.get(argList.indexOf("-kind")+1)); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(0)); //extender always runs as root + } + + @Test + void test12OnwardsRun(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.12"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Extender e = new Extender(factory, "run"); + + ContainerStatus cs = e.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/extender", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify kind not present in pre12 + assertTrue(argList.contains("-kind")); + assertEquals("run", argList.get(argList.indexOf("-kind")+1)); + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(0)); //extender always runs as root + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/RestorerTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/RestorerTest.java new file mode 100644 index 0000000..9cc6136 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/RestorerTest.java @@ -0,0 +1,308 @@ +package dev.snowdrop.buildpack.lifecycle.phases; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.api.command.WaitContainerResultCallback; + +import dev.snowdrop.buildpack.BuilderImage; +import dev.snowdrop.buildpack.Logger; +import dev.snowdrop.buildpack.config.DockerConfig; +import dev.snowdrop.buildpack.config.ImageReference; +import dev.snowdrop.buildpack.config.LogConfig; +import dev.snowdrop.buildpack.lifecycle.ContainerStatus; +import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; +import dev.snowdrop.buildpack.lifecycle.Version; + +@ExtendWith(MockitoExtension.class) +public class RestorerTest { + + @Captor + ArgumentCaptor argsCaptor; + + @Test + void testPre10(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock BuilderImage origBuilder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.9"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(origBuilder.getImage()).thenReturn(new ImageReference("fish")); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Restorer r = new Restorer(factory,origBuilder); + + ContainerStatus cs = r.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/restorer", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify build image not present in pre 0.10 + assertFalse(argList.contains("-build-image")); + + //verify daemon is not set pre 0.12 + assertFalse(argList.contains("-daemon")); + + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void testPost10Pre12NoXtns(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock BuilderImage origBuilder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.10"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + boolean HAS_EXTENSIONS=false; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(origBuilder.getImage()).thenReturn(new ImageReference("fish")); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Restorer r = new Restorer(factory,origBuilder); + + ContainerStatus cs = r.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/restorer", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify build image not present in 0.12 without xtns + assertFalse(argList.contains("-build-image")); + + //verify daemon is not set 0.12 when use daemon false + assertFalse(argList.contains("-daemon")); + + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + + @Test + void testPost10Pre12Xtns(@Mock LifecyclePhaseFactory factory, + @Mock BuilderImage builder, + @Mock BuilderImage origBuilder, + @Mock LogConfig logConfig, + @Mock DockerConfig dockerConfig, + @Mock DockerClient dockerClient, + @Mock StartContainerCmd startCmd, + @Mock LogContainerCmd logCmd, + @Mock WaitContainerCmd waitCmd, + @Mock WaitContainerResultCallback waitResult, + @Mock Logger logger + ) { + + String PLATFORM_LEVEL="0.10"; + String LOG_LEVEL="debug"; + String CONTAINER_ID="999"; + int CONTAINER_RC=99; + int USER_ID=77; + int GROUP_ID=88; + String OUTPUT_IMAGE="stiletto"; + boolean USE_DAEMON=false; + boolean HAS_EXTENSIONS=true; + + lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); + lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); + lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); + + lenient().doNothing().when(startCmd).exec(); + lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); + + lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); + lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); + lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); + lenient().when(logCmd.exec(any())).thenReturn(null); + lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); + + lenient().when(waitCmd.exec(any())).thenReturn(waitResult); + lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); + lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); + + lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); + lenient().when(factory.getLogConfig()).thenReturn(logConfig); + + lenient().when(builder.getUserId()).thenReturn(USER_ID); + lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); + lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); + lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); + lenient().when(factory.getBuilderImage()).thenReturn(builder); + + lenient().when(origBuilder.getImage()).thenReturn(new ImageReference("fish")); + + lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); + + lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); + + lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); + + Restorer r = new Restorer(factory,origBuilder); + + ContainerStatus cs = r.runPhase(logger, true); + + assertNotNull(cs); + assertEquals(CONTAINER_ID, cs.getContainerId()); + assertEquals(CONTAINER_RC, cs.getRc()); + + String[] args = argsCaptor.getValue(); + assertNotNull(args); + //verify 1st & last elements + assertEquals("/cnb/lifecycle/restorer", args[0]); + assertNotEquals(args[args.length-1], OUTPUT_IMAGE); + + List argList = Arrays.asList(args); + //verify build image present in for 0.12 with xtns + assertTrue(argList.contains("-build-image")); + + //verify daemon is not set 0.12 when use daemon false + assertFalse(argList.contains("-daemon")); + + //verify log as expected + assertTrue(argList.contains(LOG_LEVEL)); + + verify(logCmd).withTimestamps(true); + verify(dockerClient).logContainerCmd(CONTAINER_ID); + verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + } + +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/utils/BuildpackMetadataTest.java b/client/src/test/java/dev/snowdrop/buildpack/utils/BuildpackMetadataTest.java new file mode 100644 index 0000000..839d2a1 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/utils/BuildpackMetadataTest.java @@ -0,0 +1,71 @@ +package dev.snowdrop.buildpack.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import dev.snowdrop.buildpack.BuildpackException; +import dev.snowdrop.buildpack.docker.ContainerUtils; + +@ExtendWith(MockitoExtension.class) +public class BuildpackMetadataTest { + @Test + void checkRunImage(){ + String json = "{\"stack\":{\"runImage\":{\"image\":\"patent:stilettos\"}}}}"; + String image = BuildpackMetadata.getRunImageFromMetadataJSON(json); + assertNotNull(image); + assertEquals("patent:stilettos", image); + + + String dockerJson = "{\"stack\":{\"runImage\":{\"image\":\"index.docker.io/patent:stilettos\"}}}}"; + String dockerImage = BuildpackMetadata.getRunImageFromMetadataJSON(dockerJson); + assertNotNull(dockerImage); + assertEquals("docker.io/patent:stilettos", dockerImage); + + try{ + String badJson = "{\"stack\":{\"runImage\":{\"notimage\":\"index.docker.io/patent:stilettos\"}}}}"; + BuildpackMetadata.getRunImageFromMetadataJSON(badJson); + fail(); + }catch(BuildpackException be){ + + } + + byte[] runTomlData = "[[images]]\n image = \"docker.io/paketocommunity/run-java-8-ubi-base\"\n[[images]]\n image = \"docker.io/paketocommunity/run-java-11-ubi-base\"\n[[images]]\n image = \"docker.io/paketocommunity/run-java-17-ubi-base\"\n".getBytes(); + try (MockedStatic containerUtils = mockStatic(ContainerUtils.class)) { + containerUtils.when(() -> ContainerUtils.getFileFromContainer(any(), any(), any())).thenReturn(runTomlData); + List runImages = BuildpackMetadata.getRunImageFromRunTOML("dummy"); + assertNotNull(runImages); + assertEquals(3, runImages.size()); + assertTrue(runImages.contains("docker.io/paketocommunity/run-java-8-ubi-base")); + assertTrue(runImages.contains("docker.io/paketocommunity/run-java-11-ubi-base")); + assertTrue(runImages.contains("docker.io/paketocommunity/run-java-17-ubi-base")); + } + + } + + @Test + void checkSupportedPlatforms(){ + String json = "{\"lifecycle\":{\"apis\":{\"platform\":{\"supported\":[\"0.1\",\"0.2\",\"0.3\"]}}}}}"; + List platforms = BuildpackMetadata.getSupportedPlatformsFromMetadata(json); + assertNotNull(platforms); + assertEquals(3, platforms.size()); + + try{ + String badJson = "{\"lifecycle\":{\"apis\":{\"platform\":{\"tobeornottobe\":[\"0.1\",\"0.2\",\"0.3\"]}}}}}"; + BuildpackMetadata.getSupportedPlatformsFromMetadata(badJson); + fail(); + }catch(BuildpackException be){ + + } + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/utils/JsonUtilsTest.java b/client/src/test/java/dev/snowdrop/buildpack/utils/JsonUtilsTest.java new file mode 100644 index 0000000..309c065 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/utils/JsonUtilsTest.java @@ -0,0 +1,50 @@ +package dev.snowdrop.buildpack.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonUtilsTest { + @Test + void testJsonUtils() { + + String json = "{\"heels\":[\"kitten\",\"stiletto\",\"wedge\"], \"aNumber\":1337, \"aWord\":\"wibble\", \"sizes\":[11,12], \"models\":{\"patent\":{\"color\":\"red\"}}}}"; + + ObjectMapper om = new ObjectMapper(); + om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try { + JsonNode root = om.readTree(json); + + String word = JsonUtils.getValue(root, "aWord"); + assertEquals("wibble", word); + + String nestedWord = JsonUtils.getValue(root, "models/patent/color"); + assertEquals("red",nestedWord); + + String number = JsonUtils.getValue(root, "aNumber"); + assertEquals("1337", number); + + List wordList = JsonUtils.getArray(root, "heels"); + assertNotNull(wordList); + assertEquals(3, wordList.size()); + + List numberList = JsonUtils.getArray(root, "sizes"); + assertNotNull(numberList); + assertEquals(2, numberList.size()); + } catch (JsonMappingException e) { + fail(e); + } catch (JsonProcessingException e) { + fail(e); + } + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/utils/LifecycleArgsTest.java b/client/src/test/java/dev/snowdrop/buildpack/utils/LifecycleArgsTest.java new file mode 100644 index 0000000..a5b0229 --- /dev/null +++ b/client/src/test/java/dev/snowdrop/buildpack/utils/LifecycleArgsTest.java @@ -0,0 +1,29 @@ +package dev.snowdrop.buildpack.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class LifecycleArgsTest { + @Test + void testLifecycleArgs(){ + LifecycleArgs la1 = new LifecycleArgs("/command", null); + assertEquals(1, la1.toList().size()); + assertEquals("/command", la1.toList().get(0)); + + la1.addArg("-daemon"); + assertEquals(2, la1.toList().size()); + + la1.addArg("-flag", "value"); + assertEquals(4, la1.toList().size()); + + LifecycleArgs la2 = new LifecycleArgs("/command", "image-name"); + assertEquals(2, la2.toList().size()); + assertEquals("/command", la2.toList().get(0)); + assertEquals("image-name", la2.toList().get(1)); + + la2.addArg("-flag","value"); + assertEquals(4, la2.toList().size()); + assertEquals("image-name", la2.toList().get(3)); + } +} diff --git a/pom.xml b/pom.xml index 24f7083..b9daf67 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ 1.8 1.8 - 3.2.13 + 3.3.4 1.7.30 1.20 2.12.3 @@ -101,6 +101,11 @@ + + org.tomlj + tomlj + 1.1.1 + org.apache.commons commons-compress diff --git a/samples/hello-quarkus/pack.java b/samples/hello-quarkus/pack.java index 8b6cc17..520cf5b 100755 --- a/samples/hello-quarkus/pack.java +++ b/samples/hello-quarkus/pack.java @@ -8,10 +8,12 @@ public class pack { public static void main(String... args) { - Buildpack.builder() - .addNewFileContent(new File(".")) - .withBuildImage("redhat/buildpacks-builder-quarkus-jvm:latest") - .withFinalImage("snowdrop/hello-quarkus:latest") - .build(); + + int exitCode = BuildConfig.builder() + .withBuilderImage(new ImageReference("paketocommunity/builder-ubi-base")) + .withOutputImage(new ImageReference("snowdrop/hello-quarkus:latest")) + .addNewFileContentApplication(new File(".")) + .build() + .getExitCode(); } } diff --git a/samples/hello-spring/pack.java b/samples/hello-spring/pack.java index 33b8a1a..67f1760 100755 --- a/samples/hello-spring/pack.java +++ b/samples/hello-spring/pack.java @@ -8,9 +8,10 @@ public class pack { public static void main(String... args) { - Buildpack.builder() - .addNewFileContent(new File(".")) - .withFinalImage("snowdrop/hello-spring:latest") - .build(); + int exitCode = BuildConfig.builder() + .withOutputImage(new ImageReference("snowdrop/hello-quarkus:latest")) + .addNewFileContentApplication(new File(".")) + .build() + .getExitCode(); } } From c9133a222e08e2d5539da0da8f0e5badad30cd0e Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Tue, 20 Feb 2024 07:42:11 -0500 Subject: [PATCH 2/9] Update README.md --- README.md | 193 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 125 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 2c5e6f4..b069288 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,19 @@ Prototype of a simple buildpack (https://buildpacks.io/) client for java. This project represents a simple implementation of the buildpack platform spec, and can be used to build projects using specified buildpacks. -The client uses the combined `creator` lifecycle phase from the buildpack to -drive the entire build. A fluent interface is provided to allow creation & configuration +This project implements up to version 0.10 of the buildpack platform spec, and should +work with builders requesting from 0.4 through to 0.10. 0.12 is available but experimental. + +A fluent interface is provided to allow creation & configuration of the build to be performed. A very simple build can be performed with as little as. -``` -Buildpack.builder() - .withContent(new File("/home/user/java-project")) - .withFinalImage("test/testimage:latest") - .build(); +```java +int exitCode = BuildConfig.builder() + .withOutputImage(new ImageReference("test/testimage:latest")) + .addNewFileContentApplication(new File("/home/user/java-project")) + .build() + .getExitCode(); ``` This will use the default builder image from (https://paketo.io) to handle the build @@ -26,18 +29,28 @@ be stored in the local docker daemon as `test/testimage:latest`. ## Overview -The [`BuildpackBuilder`](src/main/java/dev/snowdrop/buildpack/BuildpackBuilder.java) offers other configuration methods to customise behavior. - -- run/build Image can be specified -- docker socket location can be configured -- cache volume names for build/launch can be specified, and optionally auto deleted after the build -- `creator` debug level can be set -- pull timeout can be configured in seconds - -Options exist, but are not (yet) active, for. - -- use docker registry instead of daemon (requires additional auth config, not implemented yet) -- passing Env content to the build stage (not implemented yet) +The [`BuildpackConfig`](src/main/java/dev/snowdrop/buildpack/BuildConfig.java) offers other configuration methods to customise behavior. + +- run/build/output Image can be specified +- docker can be configured with.. + - pull timeout + - host + - network + - docker socket path + - if Daemon should be used or not. (If yes, docker socket is mounted into build container so buildpack can make use of daemon directly, + if no, then docker will read/create output images with remote registry directly. Note that daemon is + still used to run the various build containers, just that the containers do not themselves have access to the daemon.) +- caches (launch/build/kaniko) can be configured with.. + - cache volume name. (if omitted, a randomly generated name is used) + - cache delete after build. (if yes, cache volume will be removed after build exits, defaults to TRUE) +- logging from the build containers can be customized.. + - log level, can be info, warn, debug. Affects the amount of verbosity from the lifecycle during build. + - logger instance, system logger (to sysout/syserr) and slf4j loggers are supplied. +- platform aspects can be customized.. + - platform level can be forced to a version, by default platform level is derived from the intersection of builder/lifecycle and platform supported versions. + - environment vars can be set that will be accessible during the build as platform env vars. + - the builder can be 'trusted'. This means the creator lifecycle is used, where all phases happen within a single container, faster, but does not support extensions, and can expose some lifecycle phases to credentials that may be otherwise protected. + - lifecycle image can be specified. If set, the lifecycle from the specified image will be used instead of the one within the builder image. Allows for easy testing with newer lifecycles. A variety of methods are supported for adding content to be build, content is combined in the order passed, allowing for sparse source directories, or multiple project dirs to be combined. @@ -47,7 +60,6 @@ passed, allowing for sparse source directories, or multiple project dirs to be c - InputStream Content, with path. Similar to String, except with data pulled from an InputStream. - [`ContainerEntry`](src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java) interface, for custom integration. - Build/RunImages will be pulled as required. The builder will use docker via the `DOCKER_HOST` env var, if configured, or via the platform appropriate docker socket if not. @@ -59,48 +71,66 @@ docker socket is extracted and used during the build phase. If unset, this defau Want to try out this project? The packages/api are not fixed in stone yet, so be aware! But here are the basic steps to get you up and running. -1. Add this project as a dependency via jitpack. -``` +1. Add this project as a dependency.. (use the latest version instead of XXX) +```xml dev.snowdrop buildpack-client - 0.0.6 + 0.0.XXX ``` -2. Instantiate a BuildpackBuilder -``` - BuildpackBuilder bpb = Buildpack.builder(); +2. Instantiate a BuildConfig +```java + BuildConfig bc = BuildConfig.builder(); ``` 3. Define the content to be built.. -``` - bpb = bpb.addNewFileContent(new File("/path/to/the/project/to/build)); +```java + bc = bc.addNewFileContentApplication(new File("/path/to/the/project/to/build)); ``` 4. Configure the name/tags for the image to create -``` - bpb = bpb.withFinalImage("myorg/myimage:mytag"); +```java + bc = bc.withOutputImage(new ImageReference("myorg/myimage:mytag")); ``` 5. Invoke the build +```java + bc.build(); ``` - bpb.build(); + +6. Retrieve the build exit code +```java + bc.getExitCode(); ``` Or combine all the above steps into a single callchain. +```java +int exitCode = BuildConfig.builder() + .withOutputImage(new ImageReference("test/testimage:latest")) + .addNewFileContentApplication(new File("/home/user/java-project")) + .build() + .getExitCode(); ``` -Buildpack.builder() - .withFileContent(new File("/path/to/the/project/to/build)) - .withFinalImage("myorg/myimage:mytag") - .build(); -``` -There are many more ways to customize & configure the BuildpackBuilder, take a look at the [interface](src/main/java/dev/snowdrop/buildpack/BuildpackBuilder.java) to see everything thats currently possible. +There are many more ways to customize & configure the BuildConfig, take a look at the [interface](src/main/java/dev/snowdrop/buildpack/BuildConfig.java) to see everything thats currently possible. + +A demo project has been created to allow easy exploration of uses of `BuildConfig` [here](https://github.com/snowdrop/java-buildpack-demo) :-) -A demo project has been created to play with the Java `BuildpackBuilder` [here](https://github.com/snowdrop/java-buildpack-demo) :-) +Most likely if you are using this to integrate to existing tooling, you will want to supply a custom LogReader to receive the messages output by the Build Containers during the build. You may also want to associate cache names to a project, to enable faster rebuilds for a given project. Note that if you wish caches to survive beyond a build, you should set `deleteCacheAfterBuild` to `false` for each cache. Eg. -Most likely if you are using this to integrate to existing tooling, you will want to supply a custom LogReader to receive the messages output by BuildPacks during the build. You may also want to associate cache names to a project, to enable faster rebuilds for a given project. +```java +int exitCode = BuildConfig.builder() + .withNewBuildCacheConfig() + .withCacheVolumeName("my-project-specific-cache-name") + .withDeleteCacheAfterBuild(true) + .and() + .withOutputImage(new ImageReference("test/testimage:latest")) + .addNewFileContentApplication(new File("/home/user/java-project")) + .build() + .getExitCode(); +``` ## Logging @@ -113,22 +143,30 @@ At the moment two kinds of logger are supported: Both can be configured using the builder: ```java - Buildpack.builder() - .withContent(new File(".")) - .withFinalImage("test/my-image:latest") - .withLogger(new SystemLogger()) - .build(); +int exitCode = BuildConfig.builder() + .withNewLogConfig() + .withLogger(new SystemLogger()) + .withLogLevel("debug") + .and() + .withOutputImage(new ImageReference("test/testimage:latest")) + .addNewFileContentApplication(new File("/home/user/java-project")) + .build() + .getExitCode(); ``` or ```java - Buildpack.builder() - .withContent(new File(".")) - .withFinalImage("test/my-image:latest") - .withLogger(new Slf4jLogger()) - .build(); +int exitCode = BuildConfig.builder() + .withNewLogConfig() + .withLogger(new SystemLogger()) + .withLogLevel("debug") + .and() + .withOutputImage(new ImageReference("test/testimage:latest")) + .addNewFileContentApplication(new File("/home/user/java-project")) + .build() + .getExitCode(); ``` @@ -138,11 +176,15 @@ The builder DSL supports inlining `Logger` configuration: ```java - Buildpack.builder() - .withContent(new File(".")) - .withFinalImage("test/my-image:latest") - .withNewSystemLogger(false) //Explicitly disables ansi colors - .build(); +int exitCode = BuildConfig.builder() + .withNewLogConfig() + .withNewSystemLogger(false) + .withLogLevel("debug") + .and() + .withOutputImage(new ImageReference("test/testimage:latest")) + .addNewFileContentApplication(new File("/home/user/java-project")) + .build() + .getExitCode(); ``` @@ -152,26 +194,32 @@ Similarly, with `Slf4jLogger` one can inline the name of the logger: ```java - Buildpack.builder() - .withContent(new File(".")) - .withFinalImage("test/my-image:latest") - .withNewSlf4jLogger(MyApp.class.getCanonicalName()) //Explicitly specify the Logger - .build(); +int exitCode = BuildConfig.builder() + .withNewLogConfig() + .withNewSlf4jLogger(MyApp.class.getCanonicalName()) + .withLogLevel("debug") + .and() + .withOutputImage(new ImageReference("test/testimage:latest")) + .addNewFileContentApplication(new File("/home/user/java-project")) + .build() + .getExitCode(); ``` +## Error Handling +If the build fails for any reason, a `BuildpackException` will be thrown, this is a RuntimeException, so does not need an explicit catch block. There are many many ways in which a build can fail, from something environmental, like docker being unavailable, to build related issues, like the chosen builder image requiring a platformlevel not implemented by this library. ## Using the buildpack client with jbang The easiest way to invoke arbitrary java code, without much hassle is by using [jbang](https://www.jbang.dev/). -So, you can drop the following file in your project: +So, you can drop the following file in your project: (swap XXX for latest release of buildpack-client) ```java ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS dev.snowdrop:buildpack-client:0.0.6 +//DEPS dev.snowdrop:buildpack-client:0.0.XXX import static java.lang.System.*; import java.io.File; @@ -180,10 +228,12 @@ import dev.snowdrop.buildpack.*; public class pack { public static void main(String... args) { - Buildpack.builder() - .withContent(new File(".")) - .withFinalImage("test/my-image:latest") - .build(); + int exitCode = BuildConfig.builder() + .withOutputImage(new ImageReference("test/testimage:latest")) + .addNewFileContentApplication(new File("/home/user/java-project")) + .build() + .getExitCode(); + System.exit(exitCode); } } @@ -201,7 +251,7 @@ See how it's used in the included [samples](./samples). **Will this work with Podman?:** -Not yet. Once the regular `pack` cli works with Podman, I'll revisit this and ensure it works too. +It should do =) I've got some testing to run still. **Does this work on Windows?:** @@ -210,8 +260,15 @@ Tested with Win10 + Docker on WSL2 **Does this work on Linux?:** -Yes.. with Docker (real docker, not podman pretending to be docker). -Tested with Ubuntu + Docker +Yes.. +Tested with Ubuntu & Fedora with Docker + +**Can I supply buildpacks/extensions to add to a builder like pack?:** + +The code should support this now, internally. It hasn't been exposed yet, as +in addition to supplying the buildpacks/extensions, a new order.toml must be +created combining the builder order with supplied content, and work is required +to see how that should look. From 38273ece8602daca9e3fd142755a9f82e4b7833f Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Tue, 20 Feb 2024 07:44:36 -0500 Subject: [PATCH 3/9] fix file link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b069288..d08fb6a 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ int exitCode = BuildConfig.builder() .getExitCode(); ``` -There are many more ways to customize & configure the BuildConfig, take a look at the [interface](src/main/java/dev/snowdrop/buildpack/BuildConfig.java) to see everything thats currently possible. +There are many more ways to customize & configure the BuildConfig, take a look at the [interface](client/src/main/java/dev/snowdrop/buildpack/BuildConfig.java) to see everything thats currently possible. A demo project has been created to allow easy exploration of uses of `BuildConfig` [here](https://github.com/snowdrop/java-buildpack-demo) :-) From 9031bad0ddc6ed9255b52a3afb9835b0fdac6b45 Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Tue, 20 Feb 2024 07:46:18 -0500 Subject: [PATCH 4/9] fix paths in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d08fb6a..e95c297 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ be stored in the local docker daemon as `test/testimage:latest`. ## Overview -The [`BuildpackConfig`](src/main/java/dev/snowdrop/buildpack/BuildConfig.java) offers other configuration methods to customise behavior. +The [`BuildpackConfig`](client/src/main/java/dev/snowdrop/buildpack/BuildConfig.java) offers other configuration methods to customise behavior. - run/build/output Image can be specified - docker can be configured with.. @@ -58,7 +58,7 @@ passed, allowing for sparse source directories, or multiple project dirs to be c - File/Directory, with prefix. Eg, take this directory /home/fish/wibble, and make it appear in the application content as /prodcode - String Content, with path. Eg, take this String content "FISH" and make it appear in the application content as /prodcode/fish.txt - InputStream Content, with path. Similar to String, except with data pulled from an InputStream. -- [`ContainerEntry`](src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java) interface, for custom integration. +- [`ContainerEntry`](client/src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java) interface, for custom integration. Build/RunImages will be pulled as required. From e652f53e577baadd72471ffb730f1c3e00509279 Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Wed, 21 Feb 2024 07:54:58 -0500 Subject: [PATCH 5/9] update mock method signature in testcase --- .../java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java b/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java index 4a24a5a..63c94b7 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; @@ -94,7 +95,7 @@ void addContentToVolumeViaString(@Mock DockerClient dc ) { try (MockedStatic scu = Mockito.mockStatic(ContainerUtils.class)){ - scu.when(() -> ContainerUtils.createContainer(eq(dc), anyString(), ArgumentMatchers.any())).thenReturn(containerId); + scu.when(() -> ContainerUtils.createContainer(eq(dc), anyString(), anyList(), ArgumentMatchers.any())).thenReturn(containerId); boolean result = VolumeUtils.addContentToVolume(dc, volumeName, "tianon/true", entryName, entryContent); assertTrue(result); @@ -153,7 +154,7 @@ void addContentToContainerViaFile(@Mock DockerClient dc, try (MockedStatic scu = Mockito.mockStatic(ContainerUtils.class)){ - scu.when(() -> ContainerUtils.createContainer(eq(dc), anyString(), ArgumentMatchers.any())).thenReturn(containerId); + scu.when(() -> ContainerUtils.createContainer(eq(dc), anyString(), anyList(), ArgumentMatchers.any())).thenReturn(containerId); boolean result = VolumeUtils.addContentToVolume(dc, volumeName, "tianon/true", pathInVolume, f); assertTrue(result); From 0a7fbbd3d646920d573c990cdc52f302659ac419 Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Mon, 15 Apr 2024 10:31:34 -0400 Subject: [PATCH 6/9] Updates for podman --- .../dev/snowdrop/buildpack/BuilderImage.java | 15 +- .../snowdrop/buildpack/BuildpackBuild.java | 5 +- .../buildpack/config/DockerConfig.java | 7 + .../buildpack/docker/BuildContainerUtils.java | 57 ++-- .../buildpack/docker/ContainerEntry.java | 2 + .../buildpack/docker/ContainerUtils.java | 44 ++- .../buildpack/docker/DockerClientUtils.java | 42 ++- .../buildpack/docker/FileContent.java | 8 + .../buildpack/docker/StreamContent.java | 9 +- .../buildpack/docker/StringContent.java | 9 +- .../buildpack/docker/VolumeUtils.java | 22 +- .../lifecycle/LifecycleExecutor.java | 2 - .../lifecycle/LifecyclePhaseFactory.java | 5 +- .../buildpack/lifecycle/phases/Analyzer.java | 56 ++-- .../buildpack/lifecycle/phases/Builder.java | 50 +-- .../buildpack/lifecycle/phases/Creator.java | 50 +-- .../buildpack/lifecycle/phases/Detector.java | 58 ++-- .../buildpack/lifecycle/phases/Exporter.java | 50 +-- .../buildpack/lifecycle/phases/Extender.java | 50 +-- .../buildpack/lifecycle/phases/Restorer.java | 52 +-- .../buildpack/utils/FilePermissions.java | 23 ++ .../buildpack/config/DockerConfigTest.java | 21 +- .../buildpack/docker/ContainerEntryTest.java | 7 + .../buildpack/docker/ContainerUtilsTest.java | 88 +++-- .../buildpack/docker/ContentTest.java | 2 +- .../buildpack/docker/VolumeUtilsTest.java | 2 +- .../pack.java | 0 samples/hello-quarkus/.dockerignore | 5 + samples/hello-quarkus/.gitignore | 43 +++ samples/hello-quarkus/.mvn/wrapper/.gitignore | 1 + .../.mvn/wrapper/MavenWrapperDownloader.java | 98 ++++++ .../.mvn/wrapper/maven-wrapper.properties | 18 + samples/hello-quarkus/README.md | 61 ++++ samples/hello-quarkus/id-debug.sh | 11 + samples/hello-quarkus/mvnw | 308 ++++++++++++++++++ samples/hello-quarkus/mvnw.cmd | 205 ++++++++++++ samples/hello-quarkus/pom.xml | 146 +++++++++ .../src/main/docker/Dockerfile.jvm | 97 ++++++ .../src/main/docker/Dockerfile.legacy-jar | 93 ++++++ .../src/main/docker/Dockerfile.native | 27 ++ .../src/main/docker/Dockerfile.native-micro | 30 ++ .../java/dev/snowdrop/GreetingResource.java | 16 + .../resources/META-INF/resources/index.html | 285 ++++++++++++++++ .../src/main/resources/application.properties | 0 .../java/dev/snowdrop/GreetingResourceIT.java | 8 + .../dev/snowdrop/GreetingResourceTest.java | 20 ++ 46 files changed, 1962 insertions(+), 246 deletions(-) create mode 100644 client/src/main/java/dev/snowdrop/buildpack/utils/FilePermissions.java rename samples/{hello-quarkus => hello-quarkus-jbang}/pack.java (100%) create mode 100755 samples/hello-quarkus/.dockerignore create mode 100755 samples/hello-quarkus/.gitignore create mode 100755 samples/hello-quarkus/.mvn/wrapper/.gitignore create mode 100755 samples/hello-quarkus/.mvn/wrapper/MavenWrapperDownloader.java create mode 100755 samples/hello-quarkus/.mvn/wrapper/maven-wrapper.properties create mode 100755 samples/hello-quarkus/README.md create mode 100755 samples/hello-quarkus/id-debug.sh create mode 100755 samples/hello-quarkus/mvnw create mode 100755 samples/hello-quarkus/mvnw.cmd create mode 100755 samples/hello-quarkus/pom.xml create mode 100755 samples/hello-quarkus/src/main/docker/Dockerfile.jvm create mode 100755 samples/hello-quarkus/src/main/docker/Dockerfile.legacy-jar create mode 100755 samples/hello-quarkus/src/main/docker/Dockerfile.native create mode 100755 samples/hello-quarkus/src/main/docker/Dockerfile.native-micro create mode 100755 samples/hello-quarkus/src/main/java/dev/snowdrop/GreetingResource.java create mode 100755 samples/hello-quarkus/src/main/resources/META-INF/resources/index.html create mode 100755 samples/hello-quarkus/src/main/resources/application.properties create mode 100755 samples/hello-quarkus/src/test/java/dev/snowdrop/GreetingResourceIT.java create mode 100755 samples/hello-quarkus/src/test/java/dev/snowdrop/GreetingResourceTest.java diff --git a/client/src/main/java/dev/snowdrop/buildpack/BuilderImage.java b/client/src/main/java/dev/snowdrop/buildpack/BuilderImage.java index edcd00d..b5c7576 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/BuilderImage.java +++ b/client/src/main/java/dev/snowdrop/buildpack/BuilderImage.java @@ -37,7 +37,6 @@ public BuilderImage(BuilderImage original, boolean addedExtensions, ImageReferen this.runImages = original.runImages; this.builderSupportedPlatforms = original.builderSupportedPlatforms; this.hasExtensions = original.hasExtensions || addedExtensions; - System.out.println("Extended builder image hasExtensions? "+hasExtensions+" orig?"+original.hasExtensions+" add?"+addedExtensions); this.image = extended; } @@ -72,15 +71,21 @@ public BuilderImage(DockerConfig dc, PlatformConfig pc, ImageReference runImage, } String xtnLayers = ii.labels.get("io.buildpacks.extension.layers"); - System.out.println("Builder image got xtnLayers label "+xtnLayers); + //if xtnLayers is absent, or is just the empty json {}, there are no extensions. + if(xtnLayers!=null && !xtnLayers.isEmpty()){ + xtnLayers = xtnLayers.trim(); + xtnLayers.replaceAll("\\\\w", ""); + if(xtnLayers.equals("{}")){ + xtnLayers = null; + } + } hasExtensions = (xtnLayers!=null && !xtnLayers.isEmpty()); - System.out.println("BuilderImage hasExtensions? "+hasExtensions); //defer the calculation of run images to the getter, because we //need to know the selected platform level to know how to find the run metadata. - metadataJson = ii.labels.get("io.buildpacks.builder.metadata"); + this.metadataJson = ii.labels.get("io.buildpacks.builder.metadata"); this.runImage = runImage; - runImages = null; + this.runImages = null; builderSupportedPlatforms = BuildpackMetadata.getSupportedPlatformsFromMetadata(metadataJson); } diff --git a/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java b/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java index 7b97360..b4e5f0c 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java +++ b/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java @@ -9,6 +9,7 @@ import dev.snowdrop.buildpack.config.PlatformConfig; import dev.snowdrop.buildpack.docker.BuildContainerUtils; import dev.snowdrop.buildpack.lifecycle.LifecycleExecutor; +import dev.snowdrop.buildpack.lifecycle.Version; import dev.snowdrop.buildpack.utils.LifecycleMetadata; public class BuildpackBuild { @@ -72,8 +73,8 @@ public int build(){ config.getPlatformConfig(), builder); - System.out.println("Selected platform level "+activePlatformLevel); - + //precache the runimages in the orig builder before extending it. + builder.getRunImages(new Version(activePlatformLevel)); //create the extended builder image. BuilderImage extendedBuilder = BuildContainerUtils.createBuildImage(config.getDockerConfig().getDockerClient(), builder, diff --git a/client/src/main/java/dev/snowdrop/buildpack/config/DockerConfig.java b/client/src/main/java/dev/snowdrop/buildpack/config/DockerConfig.java index eb6d04c..86fe794 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/config/DockerConfig.java +++ b/client/src/main/java/dev/snowdrop/buildpack/config/DockerConfig.java @@ -2,6 +2,7 @@ import com.github.dockerjava.api.DockerClient; +import dev.snowdrop.buildpack.BuildpackException; import dev.snowdrop.buildpack.docker.DockerClientUtils; import io.sundr.builder.annotations.Buildable; @@ -34,6 +35,12 @@ public DockerConfig( this.dockerNetwork = dockerNetwork; this.useDaemon = useDaemon != null ? useDaemon : Boolean.TRUE; //default daemon to true for back compat. this.dockerClient = dockerClient != null ? dockerClient : DockerClientUtils.getDockerClient(this.dockerHost); + + try{ + this.dockerClient.pingCmd().exec(); + }catch(Exception e){ + throw new BuildpackException("Unable to verify docker settings", e); + } } public Integer getPullTimeout(){ diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java index 2b17a83..f2d5dc4 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java @@ -67,6 +67,7 @@ public void run() { tae.setSize(0); tae.setUserId(uid); tae.setGroupId(gid); + tae.setMode(TarArchiveEntry.DEFAULT_DIR_MODE); tout.putArchiveEntry(tae); tout.closeArchiveEntry(); } @@ -121,29 +122,37 @@ public static BuilderImage createBuildImage(DockerClient dc, BuilderImage baseBu List command = Stream.of("").collect(Collectors.toList()); String builderContainerId = ContainerUtils.createContainer(dc, baseBuilder.getImage().getReference(), command); - if(lifecycle!=null) - processBuildModule(dc, builderContainerId, lifecycle.getReference(), "/cnb/lifecycle", "/cnb"); - - if(extensions!=null) - for(ImageReference extension: extensions) - processBuildModule(dc, builderContainerId, extension.getReference(), "/cnb/extensions", "/cnb"); - - if(buildpacks!=null) - for(ImageReference buildpack: buildpacks) - processBuildModule(dc, builderContainerId, buildpack.getReference(), "/cnb/buildpacks", "/cnb"); - - populateMountPointDirs(dc, builderContainerId, baseBuilder.getUserId(), baseBuilder.getGroupId(), - Stream.of(LifecyclePhaseFactory.KANIKO_VOL_PATH, - LifecyclePhaseFactory.WORKSPACE_VOL_PATH, - LifecyclePhaseFactory.LAYERS_VOL_PATH, - LifecyclePhaseFactory.CACHE_VOL_PATH, - LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH, - LifecyclePhaseFactory.PLATFORM_VOL_PATH, - LifecyclePhaseFactory.PLATFORM_VOL_PATH+LifecyclePhaseFactory.ENV_PATH_PREFIX) - .collect(Collectors.toList())); - - return new BuilderImage(baseBuilder, - (extensions!=null && !extensions.isEmpty()), - new ImageReference(ContainerUtils.commitContainer(dc, builderContainerId))); + try{ + if(lifecycle!=null) + processBuildModule(dc, builderContainerId, lifecycle.getReference(), "/cnb/lifecycle", "/cnb"); + + if(extensions!=null) + for(ImageReference extension: extensions) + processBuildModule(dc, builderContainerId, extension.getReference(), "/cnb/extensions", "/cnb"); + + if(buildpacks!=null) + for(ImageReference buildpack: buildpacks) + processBuildModule(dc, builderContainerId, buildpack.getReference(), "/cnb/buildpacks", "/cnb"); + + populateMountPointDirs(dc, builderContainerId, baseBuilder.getUserId(), baseBuilder.getGroupId(), + Stream.of(LifecyclePhaseFactory.KANIKO_VOL_PATH, + LifecyclePhaseFactory.WORKSPACE_VOL_PATH, + LifecyclePhaseFactory.LAYERS_VOL_PATH, + LifecyclePhaseFactory.CACHE_VOL_PATH, + LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH, + LifecyclePhaseFactory.DOCKER_SOCKET_PATH, + LifecyclePhaseFactory.PLATFORM_VOL_PATH, + LifecyclePhaseFactory.PLATFORM_VOL_PATH+LifecyclePhaseFactory.ENV_PATH_PREFIX) + .collect(Collectors.toList())); + + //commit the live container with the modifications and return it as the new Builder Image. + return new BuilderImage(baseBuilder, + (extensions!=null && !extensions.isEmpty()), + new ImageReference(ContainerUtils.commitContainer(dc, builderContainerId))); + }finally{ + if(builderContainerId!=null){ + ContainerUtils.removeContainer(dc, builderContainerId); + } + } } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java b/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java index df60d3a..1ee7738 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerEntry.java @@ -14,6 +14,8 @@ public interface ContainerEntry { public long getSize(); + public Integer getMode(); + public DataSupplier getDataSupplier(); @FunctionalInterface diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerUtils.java index 384d1fc..da8795a 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/ContainerUtils.java @@ -26,6 +26,7 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; +import com.github.dockerjava.api.command.CopyArchiveToContainerCmd; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.exception.NotFoundException; @@ -113,6 +114,7 @@ private static String createContainerInternal(DockerClient dc, String imageRefer } CreateContainerResponse ccr = ccc.exec(); + return ccr.getId(); } @@ -121,7 +123,7 @@ public static String commitContainer(DockerClient dc, String containerId) { } public static void removeContainer(DockerClient dc, String containerId) { - dc.removeContainerCmd(containerId).exec(); + dc.removeContainerCmd(containerId).withForce(true).exec(); } public static void addContentToContainer(DockerClient dc, String containerId, List entries) { @@ -129,7 +131,7 @@ public static void addContentToContainer(DockerClient dc, String containerId, Li } public static void addContentToContainer(DockerClient dc, String containerId, ContainerEntry... entries) { - addContentToContainer(dc, containerId, "", 0, 0, entries); + addContentToContainer(dc, containerId, "", null, null, entries); } public static void addContentToContainer(DockerClient dc, String containerId, String pathInContainer, Integer userId, @@ -138,8 +140,8 @@ public static void addContentToContainer(DockerClient dc, String containerId, St } public static void addContentToContainer(DockerClient dc, String containerId, String pathInContainer, Integer userId, - Integer groupId, String name, String content) { - addContentToContainer(dc, containerId, pathInContainer, userId, groupId, new StringContent(name, content).getContainerEntries()); + Integer groupId, String name, Integer mode, String content) { + addContentToContainer(dc, containerId, pathInContainer, userId, groupId, new StringContent(name, mode, content).getContainerEntries()); } /** @@ -158,12 +160,13 @@ private static void addParents(TarArchiveOutputStream tout, Set seenDirs // add parents of this FIRST addParents(tout, seenDirs, uid, gid, parent); - log.debug("adding "+parent+"/"); + log.debug("adding "+parent+"/ to tar"); // and then add this =) TarArchiveEntry tae = new TarArchiveEntry(parent + "/"); tae.setSize(0); tae.setUserId(uid); - tae.setGroupId(gid); + tae.setGroupId(gid); + tae.setMode(TarArchiveEntry.DEFAULT_DIR_MODE); tout.putArchiveEntry(tae); tout.closeArchiveEntry(); } @@ -182,14 +185,17 @@ public static void addContentToContainer(DockerClient dc, String containerId, St public static void addContentToContainer(DockerClient dc, String containerId, String pathInContainer, Integer userId, Integer groupId, ContainerEntry... entries) { + log.info("Adding to container "+containerId+" pathInContainer "+pathInContainer); + Set seenDirs = new HashSet<>(); // Don't add entry for "/", causes issues with tar format. seenDirs.add(""); // use supplied pathInContainer, trim off trailing "/" where required. - final String path = (!pathInContainer.isEmpty() && pathInContainer.endsWith("/")) + final String containerPath = (!pathInContainer.isEmpty() && pathInContainer.endsWith("/")) ? pathInContainer.substring(0, pathInContainer.length() - 1) : pathInContainer; + // set uid/gid to the supplied values, or 0 if not supplied. final int uid = (userId != null) ? userId : 0; final int gid = (groupId != null) ? groupId : 0; @@ -212,18 +218,18 @@ public void run() { if (entryPath.startsWith("/")) entryPath = entryPath.substring(1); - String pathWithEntry = path + "/" + entryPath; // important! adds the parent dirs for the entries with the correct uid/gid. // (otherwise various buildpack tasks won't be able to write to them!) - addParents(tout, seenDirs, uid, gid, pathWithEntry); + addParents(tout, seenDirs, uid, gid, entryPath); - log.debug("adding "+pathWithEntry); + log.debug("adding "+entryPath+" to tar"); // add this file entry. - TarArchiveEntry tae = new TarArchiveEntry(pathWithEntry); + TarArchiveEntry tae = new TarArchiveEntry(entryPath); tae.setSize(ve.getSize()); tae.setUserId(uid); - tae.setGroupId(gid); + tae.setGroupId(gid); + tae.setMode(0100000 + ve.getMode()); //0100000 means 'regular file' tout.putArchiveEntry(tae); DataSupplier cs = ve.getDataSupplier(); if(cs==null) { @@ -245,10 +251,15 @@ public void run() { } }; + log.info("Copying archive to container at "+containerPath); + Runnable reader = new Runnable() { @Override public void run() { - dc.copyArchiveToContainerCmd(containerId).withRemotePath("/").withTarInputStream(in).exec(); + CopyArchiveToContainerCmd c = dc.copyArchiveToContainerCmd(containerId) + .withRemotePath(containerPath) + .withTarInputStream(in); + c.exec(); } }; @@ -284,11 +295,8 @@ public static byte[] getFileFromContainer(DockerClient dc, String id, String pat try{ TarArchiveEntry tarEntry = tarInput.getNextTarEntry(); while(tarEntry!=null){ - //tarEntry.getName does not include path.. - //if(path.equals(tarEntry.getName())){ - copy(tarInput, file); - file.close(); - //} + copy(tarInput, file); + file.close(); tarEntry = tarInput.getNextTarEntry(); } return file.toByteArray(); diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/DockerClientUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/DockerClientUtils.java index 3a0df96..576b69b 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/DockerClientUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/DockerClientUtils.java @@ -1,5 +1,10 @@ package dev.snowdrop.buildpack.docker; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,7 +17,6 @@ import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; - public class DockerClientUtils { private static final Logger log = LoggerFactory.getLogger(DockerClientUtils.class); @@ -46,15 +50,41 @@ public static DockerClient getDockerClient(String dockerHost) { public static String getDockerHost() { String dockerHost = System.getenv("DOCKER_HOST"); - if (dockerHost != null && dockerHost.isEmpty()) { + if (dockerHost != null && dockerHost.isEmpty()) { return dockerHost; } switch (OperatingSytem.getOperationSystem()) { - case WIN: - return "npipe:////./pipe/docker_engine"; - default: - return "unix:///var/run/docker.sock"; + case WIN: + return "npipe:////./pipe/docker_engine"; + case LINUX: { + //test for presence of docker. + File dockerSock = new File("/var/run/docker.sock"); + if(dockerSock.exists()){ + return "unix:///var/run/docker.sock"; + } + + File podmanSock = new File("/var/run/podman.sock"); + if(podmanSock.exists()){ + return "unix:///var/run/podman.sock"; + } + + try{ + int uid = (Integer)Files.getAttribute(Paths.get("/proc/self"), "unix:uid"); + File podmanUserSock = new File("/var/run/user/"+uid+"/podman/podman.sock"); + if(podmanUserSock.exists()){ + return "unix:///var/run/user/1000/podman/podman.sock"; + } + }catch(IOException io){ + //ignore. + } + + //none of the known linux filesystem locations had socket files, default to docker + //and assume the user has a plan we don't know about =) + return "unix:///var/run/docker.sock"; + } + default: + return "unix:///var/run/docker.sock"; } } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/FileContent.java b/client/src/main/java/dev/snowdrop/buildpack/docker/FileContent.java index 129f2ea..c94f8d8 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/FileContent.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/FileContent.java @@ -13,6 +13,7 @@ import java.util.stream.Collectors; import dev.snowdrop.buildpack.BuildpackException; +import dev.snowdrop.buildpack.utils.FilePermissions; import io.sundr.builder.annotations.Buildable; @Buildable(generateBuilderPackage = true, builderPackage = "dev.snowdrop.buildpack.builder") @@ -25,6 +26,8 @@ public class FileContent implements Content { private final String prefix; private final File file; private final File root; + //delegate calls for permissions thru static, to enable testing. + private static FilePermissions filePermissions = new FilePermissions(); public FileContent(File file) { this(DEFAULT_PREFIX, file); @@ -83,6 +86,11 @@ public String getPath() { UNIX_FILE_SEPARATOR); } + @Override + public Integer getMode() { + return filePermissions.getPermissions(file); + } + @Override public DataSupplier getDataSupplier() { Path p = file.toPath(); diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/StreamContent.java b/client/src/main/java/dev/snowdrop/buildpack/docker/StreamContent.java index 5f381e6..dffa52e 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/StreamContent.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/StreamContent.java @@ -12,11 +12,13 @@ public class StreamContent implements Content { private final String path; private final Long size; + private final Integer mode; private final DataSupplier dataSupplier; - public StreamContent(String path, Long size, DataSupplier dataSupplier) { + public StreamContent(String path, Long size, Integer mode, DataSupplier dataSupplier) { this.path = path; this.size = size; + this.mode = mode; this.dataSupplier = dataSupplier; } @@ -36,6 +38,11 @@ public String getPath() { public long getSize() { return size; } + + @Override + public Integer getMode() { + return mode; + } }); } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/StringContent.java b/client/src/main/java/dev/snowdrop/buildpack/docker/StringContent.java index 00936e4..7d28c91 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/StringContent.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/StringContent.java @@ -11,10 +11,12 @@ public class StringContent implements Content { private final String path; + private final Integer mode; private final String content; - public StringContent(String path, String content) { + public StringContent(String path, Integer mode, String content) { this.path = path; + this.mode = mode; this.content = content; } @@ -39,6 +41,11 @@ public String getPath() { return path; } + @Override + public Integer getMode() { + return mode; + } + @Override public DataSupplier getDataSupplier() { return () -> new ByteArrayInputStream(content.getBytes()); diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java index 790861e..de65526 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java @@ -2,14 +2,19 @@ import java.io.File; import java.util.List; +import org.slf4j.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.slf4j.LoggerFactory; + import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.exception.NotFoundException; public class VolumeUtils { + private static final Logger log = LoggerFactory.getLogger(VolumeUtils.class); + final static String mountPrefix = "/volumecontent"; public static boolean createVolumeIfRequired(DockerClient dc, String volumeName) { @@ -37,8 +42,8 @@ public static boolean addContentToVolume(DockerClient dc, String volumeName, Str return internalAddContentToVolume(dc, volumeName, useImage, mountPrefix, 0,0, new FileContent(content).getContainerEntries()); } - public static boolean addContentToVolume(DockerClient dc, String volumeName, String useImage, String name, String content) { - return internalAddContentToVolume(dc, volumeName, useImage, mountPrefix, 0,0, new StringContent(name, content).getContainerEntries()); + public static boolean addContentToVolume(DockerClient dc, String volumeName, String useImage, String name, Integer mode, String content) { + return internalAddContentToVolume(dc, volumeName, useImage, mountPrefix, 0,0, new StringContent(name, mode, content).getContainerEntries()); } public static boolean addContentToVolume(DockerClient dc, String volumeName, String useImage, String prefix, int uid, int gid, List entries) { @@ -56,11 +61,16 @@ private static boolean internalAddContentToVolume(DockerClient dc, String volume } private static boolean internalAddContentToVolume(DockerClient dc, String volumeName, String useImage, String prefix, int uid, int gid, ContainerEntry... entries) { - List command = Stream.of("").collect(Collectors.toList()); String dummyId = ContainerUtils.createContainer(dc, useImage, command, new VolumeBind(volumeName, mountPrefix)); - ContainerUtils.addContentToContainer(dc, dummyId, prefix, uid, gid, entries); - ContainerUtils.removeContainer(dc, dummyId); - return true; + try{ + log.info("Adding content to volume "+volumeName+" under prefix "+prefix+" using image "+useImage+" with volume bound at "+mountPrefix+" temp container id "+dummyId); + ContainerUtils.addContentToContainer(dc, dummyId, prefix, uid, gid, entries); + return true; + }finally{ + if(dummyId!=null){ + ContainerUtils.removeContainer(dc, dummyId); + } + } } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java index d9cf106..e9a3b7c 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java @@ -111,8 +111,6 @@ public int execute() { rc=runPhase(factory.getRestorer()); if(rc!=0) break; - System.out.println("Lifecycle Executor buildimage has extensions? "+factory.getBuilderImage().hasExtensions()); - if(activePlatformLevel.atLeast("0.10") && factory.getBuilderImage().hasExtensions()){ rc=runPhase(factory.getBuildImageExtender()); if(rc!=0) break; diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java index b367f62..1d95d0a 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java @@ -164,6 +164,7 @@ public void createVolumes(List content){ .flatMap(c -> c.getContainerEntries().stream()) .collect(Collectors.toList()); + log.info("Adding aplication to volume "+applicationVolume); VolumeUtils.addContentToVolume(dockerConfig.getDockerClient(), applicationVolume, originalBuilder.getImage().getReference(), @@ -188,9 +189,10 @@ public void createVolumes(List content){ //add the environment entries to the platform volume. List envEntries = platformConfig.getEnvironment().entrySet() .stream() - .flatMap(e -> new StringContent(e.getKey(), e.getValue()).getContainerEntries().stream()) + .flatMap(e -> new StringContent(e.getKey(), 0777, e.getValue()).getContainerEntries().stream()) .collect(Collectors.toList()); + log.info("Adding platform entries to platform volume "+platformVolume); VolumeUtils.addContentToVolume(dockerConfig.getDockerClient(), platformVolume, originalBuilder.getImage().getReference(), @@ -201,6 +203,7 @@ public void createVolumes(List content){ } public void tidyUp(){ + log.info("- tidying up the build volumes"); // remove volumes // (note when/if we persist the cache between builds, we'll be more selective here over what we remove) if (buildCacheConfig.getDeleteCacheAfterBuild()) { diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java index 26e28b8..64e9352 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java @@ -6,6 +6,7 @@ import com.github.dockerjava.api.command.WaitContainerResultCallback; import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.docker.ContainerUtils; import dev.snowdrop.buildpack.lifecycle.ContainerStatus; import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; @@ -59,30 +60,37 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us args.addArg("-daemon"); } + int runAsId = factory.getBuilderImage().getUserId(); String id = factory.getContainerForPhase(args.toArray(), runAsId); - log.info("- analyze container id " + id+ " will be run with uid "+runAsId); - - // launch the container! - log.info("- launching analyze container"); - factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); - - log.info("- attaching log relay"); - // grab the logs to stdout. - factory.getDockerConfig().getDockerClient().logContainerCmd(id) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withTimestamps(useTimestamps) - .exec(new ContainerLogReader(logger)); - - // wait for the container to complete, and retrieve the exit code. - int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); - log.info("Buildpack analyze container complete, with exit code " + rc); - - return ContainerStatus.of(rc,id); - } - - - + try{ + log.info("- analyze container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching analyze container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack analyze container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + }catch(Exception e){ + if(id!=null){ + log.info("Exception during analyzer, removing container "+id); + ContainerUtils.removeContainer(factory.getDockerConfig().getDockerClient(), id); + log.info("remove complete"); + } + throw e; + } + } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Builder.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Builder.java index 68161b5..b4e9d32 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Builder.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Builder.java @@ -6,6 +6,7 @@ import com.github.dockerjava.api.command.WaitContainerResultCallback; import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.docker.ContainerUtils; import dev.snowdrop.buildpack.lifecycle.ContainerStatus; import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; @@ -36,26 +37,35 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us //builder process has to run as root. int runAsId = factory.getBuilderImage().getUserId(); String id = factory.getContainerForPhase(args.toArray(), runAsId); - log.info("- extender container id " + id+ " will be run with uid "+runAsId); - - // launch the container! - log.info("- launching builder container"); - factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); - - log.info("- attaching log relay"); - // grab the logs to stdout. - factory.getDockerConfig().getDockerClient().logContainerCmd(id) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withTimestamps(useTimestamps) - .exec(new ContainerLogReader(logger)); - - // wait for the container to complete, and retrieve the exit code. - int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); - log.info("Buildpack builder container complete, with exit code " + rc); - - return ContainerStatus.of(rc,id); + try{ + log.info("- extender container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching builder container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack builder container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + }catch(Exception e){ + if(id!=null){ + log.info("Exception during builder, removing container "+id); + ContainerUtils.removeContainer(factory.getDockerConfig().getDockerClient(), id); + log.info("remove complete"); + } + throw e; + } } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java index ff7e4c8..77752ba 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java @@ -6,6 +6,7 @@ import com.github.dockerjava.api.command.WaitContainerResultCallback; import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.docker.ContainerUtils; import dev.snowdrop.buildpack.lifecycle.ContainerStatus; import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; @@ -51,26 +52,35 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us //creator process always has to run as root. int runAsId = 0; String id = factory.getContainerForPhase(args.toArray(), runAsId); - log.info("- creator container id " + id+ " will be run with uid "+runAsId); - - // launch the container! - log.info("- launching build container"); - factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); - - log.info("- attaching log relay"); - // grab the logs to stdout. - factory.getDockerConfig().getDockerClient().logContainerCmd(id) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withTimestamps(useTimestamps) - .exec(new ContainerLogReader(logger)); - - // wait for the container to complete, and retrieve the exit code. - int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); - log.info("Buildpack container complete, with exit code " + rc); - - return ContainerStatus.of(rc,id); + try{ + log.info("- creator container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching build container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + }catch(Exception e){ + if(id!=null){ + log.info("Exception during creator, removing container "+id); + ContainerUtils.removeContainer(factory.getDockerConfig().getDockerClient(), id); + log.info("remove complete"); + } + throw e; + } } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java index 869b2f2..cfc9d72 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java @@ -60,31 +60,39 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us // detector phase must run as non-root int runAsId = factory.getBuilderImage().getUserId(); String id = factory.getContainerForPhase(args.toArray(), runAsId); - log.info("- detect container id " + id+ " will be run with uid "+runAsId); - - // launch the container! - log.info("- launching detect container"); - factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); - - - log.info("- attaching log relay"); - // grab the logs to stdout. - factory.getDockerConfig().getDockerClient().logContainerCmd(id) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withTimestamps(useTimestamps) - .exec(new ContainerLogReader(logger)); - - // wait for the container to complete, and retrieve the exit code. - int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); - log.info("Buildpack detect container complete, with exit code " + rc); - - analyzedToml = ContainerUtils.getFileFromContainer(factory.getDockerConfig().getDockerClient(), - id, - LifecyclePhaseFactory.LAYERS_VOL_PATH + "/analyzed.toml"); - - return ContainerStatus.of(rc,id); + try{ + log.info("- detect container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching detect container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack detect container complete, with exit code " + rc); + + analyzedToml = ContainerUtils.getFileFromContainer(factory.getDockerConfig().getDockerClient(), + id, + LifecyclePhaseFactory.LAYERS_VOL_PATH + "/analyzed.toml"); + + return ContainerStatus.of(rc,id); + }catch(Exception e){ + if(id!=null){ + log.info("Exception during detect, removing container "+id); + ContainerUtils.removeContainer(factory.getDockerConfig().getDockerClient(), id); + log.info("remove complete"); + } + throw e; + } } public byte[] getAnalyzedToml(){ diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java index 6603fb8..3be2d60 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java @@ -6,6 +6,7 @@ import com.github.dockerjava.api.command.WaitContainerResultCallback; import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.docker.ContainerUtils; import dev.snowdrop.buildpack.lifecycle.ContainerStatus; import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; @@ -56,26 +57,35 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us int runAsId = factory.getBuilderImage().getUserId(); String id = factory.getContainerForPhase(args.toArray(), runAsId); - log.info("- export container id " + id+ " will be run with uid "+runAsId); - - // launch the container! - log.info("- launching export container"); - factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); - - log.info("- attaching log relay"); - // grab the logs to stdout. - factory.getDockerConfig().getDockerClient().logContainerCmd(id) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withTimestamps(useTimestamps) - .exec(new ContainerLogReader(logger)); - - // wait for the container to complete, and retrieve the exit code. - int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); - log.info("Buildpack export container complete, with exit code " + rc); - - return ContainerStatus.of(rc,id); + try{ + log.info("- export container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching export container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack export container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + }catch(Exception e){ + if(id!=null){ + log.info("Exception during export, removing container "+id); + ContainerUtils.removeContainer(factory.getDockerConfig().getDockerClient(), id); + log.info("remove complete"); + } + throw e; + } } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Extender.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Extender.java index 8a1d438..fb8c5e1 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Extender.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Extender.java @@ -6,6 +6,7 @@ import com.github.dockerjava.api.command.WaitContainerResultCallback; import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.docker.ContainerUtils; import dev.snowdrop.buildpack.lifecycle.ContainerStatus; import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; @@ -47,26 +48,35 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us // typically it should run as root. int runAsId = 0; String id = factory.getContainerForPhase(args.toArray(), runAsId); - log.info("- extender container id " + id+ " will be run with uid "+runAsId); - - // launch the container! - log.info("- launching extender container"); - factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); - - log.info("- attaching log relay"); - // grab the logs to stdout. - factory.getDockerConfig().getDockerClient().logContainerCmd(id) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withTimestamps(useTimestamps) - .exec(new ContainerLogReader(logger)); - - // wait for the container to complete, and retrieve the exit code. - int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); - log.info("Buildpack extender container complete, with exit code " + rc); - - return ContainerStatus.of(rc,id); + try{ + log.info("- extender container id " + id+ " will be run with uid "+runAsId); + + // launch the container! + log.info("- launching extender container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack extender container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + }catch(Exception e){ + if(id!=null){ + log.info("Exception during extender, removing container "+id); + ContainerUtils.removeContainer(factory.getDockerConfig().getDockerClient(), id); + log.info("remove complete"); + } + throw e; + } } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java index c0f3dad..3979205 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java @@ -7,6 +7,7 @@ import dev.snowdrop.buildpack.BuilderImage; import dev.snowdrop.buildpack.ContainerLogReader; +import dev.snowdrop.buildpack.docker.ContainerUtils; import dev.snowdrop.buildpack.lifecycle.ContainerStatus; import dev.snowdrop.buildpack.lifecycle.LifecyclePhase; import dev.snowdrop.buildpack.lifecycle.LifecyclePhaseFactory; @@ -56,27 +57,36 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us int runAsId = factory.getBuilderImage().getUserId(); String id = factory.getContainerForPhase(args.toArray(), runAsId); - log.info("- restorer container id " + id+ " will be run with uid "+runAsId+" and args "+args); - System.out.println("- restorer container id " + id+ " will be run with uid "+runAsId+" and args "+args); - - // launch the container! - log.info("- launching restorer container"); - factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); - - log.info("- attaching log relay"); - // grab the logs to stdout. - factory.getDockerConfig().getDockerClient().logContainerCmd(id) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withTimestamps(useTimestamps) - .exec(new ContainerLogReader(logger)); - - // wait for the container to complete, and retrieve the exit code. - int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); - log.info("Buildpack restorer container complete, with exit code " + rc); - - return ContainerStatus.of(rc,id); + try{ + log.info("- restorer container id " + id+ " will be run with uid "+runAsId+" and args "+args); + System.out.println("- restorer container id " + id+ " will be run with uid "+runAsId+" and args "+args); + + // launch the container! + log.info("- launching restorer container"); + factory.getDockerConfig().getDockerClient().startContainerCmd(id).exec(); + + log.info("- attaching log relay"); + // grab the logs to stdout. + factory.getDockerConfig().getDockerClient().logContainerCmd(id) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTimestamps(useTimestamps) + .exec(new ContainerLogReader(logger)); + + // wait for the container to complete, and retrieve the exit code. + int rc = factory.getDockerConfig().getDockerClient().waitContainerCmd(id).exec(new WaitContainerResultCallback()).awaitStatusCode(); + log.info("Buildpack restorer container complete, with exit code " + rc); + + return ContainerStatus.of(rc,id); + }catch(Exception e){ + if(id!=null){ + log.info("Exception during restorer, removing container "+id); + ContainerUtils.removeContainer(factory.getDockerConfig().getDockerClient(), id); + log.info("remove complete"); + } + throw e; + } } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/utils/FilePermissions.java b/client/src/main/java/dev/snowdrop/buildpack/utils/FilePermissions.java new file mode 100644 index 0000000..90ce75f --- /dev/null +++ b/client/src/main/java/dev/snowdrop/buildpack/utils/FilePermissions.java @@ -0,0 +1,23 @@ +package dev.snowdrop.buildpack.utils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; + +public class FilePermissions { + public Integer getPermissions(File file){ + try{ + Set fp = Files.getPosixFilePermissions(file.toPath()); + int mode = (fp.contains(PosixFilePermission.OWNER_READ)?0400:0) + (fp.contains(PosixFilePermission.OWNER_WRITE)?0200:0) + (fp.contains(PosixFilePermission.OWNER_EXECUTE)?0100:0) + + (fp.contains(PosixFilePermission.GROUP_READ)?040:0) + (fp.contains(PosixFilePermission.GROUP_WRITE)?020:0) + (fp.contains(PosixFilePermission.GROUP_EXECUTE)?010:0) + + (fp.contains(PosixFilePermission.OTHERS_READ)?04:0) + (fp.contains(PosixFilePermission.OTHERS_WRITE)?02:0) + (fp.contains(PosixFilePermission.OTHERS_EXECUTE)?01:0); + return mode; + }catch(IOException io){ + //may not be able to process posixfileperms on all platforms, fall back to java io File perms, and set as owner & group + return ( (file.canRead()?0400:0) + (file.canWrite()?0200:0) + (file.canExecute()?0100:0) )+ + ( (file.canRead()?040:0) + (file.canWrite()?020:0) + (file.canExecute()?010:0) ); + } + } +} diff --git a/client/src/test/java/dev/snowdrop/buildpack/config/DockerConfigTest.java b/client/src/test/java/dev/snowdrop/buildpack/config/DockerConfigTest.java index d7da285..100bc32 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/config/DockerConfigTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/config/DockerConfigTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,6 +14,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.PingCmd; @ExtendWith(MockitoExtension.class) public class DockerConfigTest { @@ -25,23 +28,27 @@ void checkTimeout() { } @Test - void checkDockerHost() { + void checkDockerHost(@Mock DockerClient dockerClient, @Mock PingCmd pingCmd) { DockerConfig dc1 = new DockerConfig(null, null, null, null, null, null); assertNotNull(dc1.getDockerHost()); - DockerConfig dc2 = new DockerConfig(null, "tcp://stilettos", null, null, null, null); + when(dockerClient.pingCmd()).thenReturn(pingCmd); + DockerConfig dc2 = new DockerConfig(null, "tcp://stilettos", null, null, null, dockerClient); assertEquals("tcp://stilettos", dc2.getDockerHost()); } @Test - void checkDockerSocket() { + void checkDockerSocket(@Mock DockerClient dockerClient, @Mock PingCmd pingCmd) { + + lenient().when(dockerClient.pingCmd()).thenReturn(pingCmd); + DockerConfig dc1 = new DockerConfig(null, null, null, null, null, null); assertNotNull(dc1.getDockerSocket()); - DockerConfig dc2 = new DockerConfig(null, "unix:///stilettos", null, null, null, null); + DockerConfig dc2 = new DockerConfig(null, "unix:///stilettos", null, null, null, dockerClient); assertEquals("/stilettos", dc2.getDockerSocket()); - DockerConfig dc3 = new DockerConfig(null, "tcp://stilettos", null, null, null, null); + DockerConfig dc3 = new DockerConfig(null, "tcp://stilettos", null, null, null, dockerClient); assertEquals("/var/run/docker.sock", dc3.getDockerSocket()); DockerConfig dc4 = new DockerConfig(null, null, "fish", null, null, null); @@ -70,7 +77,9 @@ void checkUseDaemon() { } @Test - void checkDockerClient(@Mock DockerClient dockerClient){ + void checkDockerClient(@Mock DockerClient dockerClient, @Mock PingCmd pingCmd){ + lenient().when(dockerClient.pingCmd()).thenReturn(pingCmd); + DockerConfig dc1 = new DockerConfig(null, null, null, null, null, null); assertNotNull(dc1.getDockerClient()); diff --git a/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerEntryTest.java b/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerEntryTest.java index 7fb2ef8..b6d49a7 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerEntryTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerEntryTest.java @@ -27,6 +27,11 @@ public long getSize() { return 1337; } + @Override + public Integer getMode() { + return 0755; + } + @Override public DataSupplier getDataSupplier() { return new DataSupplier(){ @@ -41,6 +46,8 @@ public InputStream getData() { assertEquals("fish", ce.getPath()); assertEquals(1337, ce.getSize()); + assertEquals(0755, ce.getMode()); + assertNotNull(ce.getDataSupplier().getData()); BufferedReader br = new BufferedReader(new InputStreamReader(ce.getDataSupplier().getData(), StandardCharsets.UTF_8)); diff --git a/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerUtilsTest.java b/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerUtilsTest.java index e87a953..adc9347 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerUtilsTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/docker/ContainerUtilsTest.java @@ -2,6 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -15,6 +17,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; @@ -28,6 +31,7 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.commons.util.ReflectionUtils; import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.mockito.Mock; @@ -42,6 +46,7 @@ import com.github.dockerjava.api.model.HostConfig; import dev.snowdrop.buildpack.BuildpackException; +import dev.snowdrop.buildpack.utils.FilePermissions; @ExtendWith(MockitoExtension.class) class ContainerUtilsTest { @@ -141,7 +146,8 @@ void removeContainer(@Mock DockerClient dc, @Mock RemoveContainerCmd rcc) { String id = "fish"; when(dc.removeContainerCmd(id)).thenReturn(rcc); - + when(rcc.withForce(anyBoolean())).thenReturn(rcc); + ContainerUtils.removeContainer(dc, id); verify(rcc).exec(); @@ -160,7 +166,7 @@ void addContentToContainerViaString(@Mock DockerClient dc, @Mock CopyArchiveToCo String containerId = "id"; when(dc.copyArchiveToContainerCmd(containerId)).thenReturn(catcc); - when(catcc.withRemotePath(anyString())).thenReturn(catcc); + when(catcc.withRemotePath(contentPath)).thenReturn(catcc); when(catcc.withTarInputStream(argThat(x -> { if (x != null) { try (BufferedInputStream bis = new BufferedInputStream(x); @@ -169,7 +175,7 @@ void addContentToContainerViaString(@Mock DockerClient dc, @Mock CopyArchiveToCo TarArchiveEntry entry; while ((entry = (TarArchiveEntry) tais.getNextEntry()) != null) { if (!entry.isDirectory()) { - if (entry.getName().equals(contentPath.substring(1) + "/" + contentName) && entry.getLongUserId() == userid + if (entry.getName().equals(contentName) && entry.getLongUserId() == userid && entry.getLongGroupId() == group) { return true; } @@ -187,7 +193,7 @@ void addContentToContainerViaString(@Mock DockerClient dc, @Mock CopyArchiveToCo }))).thenReturn(catcc); - ContainerUtils.addContentToContainer(dc, containerId, contentPath, userid, group, contentName, content); + ContainerUtils.addContentToContainer(dc, containerId, contentPath, userid, group, contentName, 0777, content); verify(catcc).exec(); } @@ -205,7 +211,7 @@ void addContentToContainerViaList(@Mock DockerClient dc, @Mock CopyArchiveToCont String containerId = "id"; when(dc.copyArchiveToContainerCmd(containerId)).thenReturn(catcc); - when(catcc.withRemotePath(anyString())).thenReturn(catcc); + when(catcc.withRemotePath(contentPath)).thenReturn(catcc); when(catcc.withTarInputStream(argThat(x -> { if (x != null) { try (BufferedInputStream bis = new BufferedInputStream(x); @@ -214,7 +220,7 @@ void addContentToContainerViaList(@Mock DockerClient dc, @Mock CopyArchiveToCont TarArchiveEntry entry; while ((entry = (TarArchiveEntry) tais.getNextEntry()) != null) { if (!entry.isDirectory()) { - if (entry.getName().equals(contentPath.substring(1) + "/" + contentName) && entry.getLongUserId() == userid + if (entry.getName().equals(contentName) && entry.getLongUserId() == userid && entry.getLongGroupId() == group) { return true; } @@ -232,7 +238,7 @@ void addContentToContainerViaList(@Mock DockerClient dc, @Mock CopyArchiveToCont }))).thenReturn(catcc); - ContainerUtils.addContentToContainer(dc, containerId, contentPath, userid, group, new StringContent(contentName, content).getContainerEntries()); + ContainerUtils.addContentToContainer(dc, containerId, contentPath, userid, group, new StringContent(contentName, 0777, content).getContainerEntries()); verify(catcc).exec(); } @@ -250,7 +256,7 @@ void addContentToContainerViaArray(@Mock DockerClient dc, @Mock CopyArchiveToCon String containerId = "id"; when(dc.copyArchiveToContainerCmd(containerId)).thenReturn(catcc); - when(catcc.withRemotePath(anyString())).thenReturn(catcc); + when(catcc.withRemotePath(contentPath)).thenReturn(catcc); when(catcc.withTarInputStream(argThat(x -> { if (x != null) { try (BufferedInputStream bis = new BufferedInputStream(x); @@ -259,7 +265,7 @@ void addContentToContainerViaArray(@Mock DockerClient dc, @Mock CopyArchiveToCon TarArchiveEntry entry; while ((entry = (TarArchiveEntry) tais.getNextEntry()) != null) { if (!entry.isDirectory()) { - if (entry.getName().equals(contentPath.substring(1) + "/" + contentName) && entry.getLongUserId() == userid + if (entry.getName().equals(contentName) && entry.getLongUserId() == userid && entry.getLongGroupId() == group) { return true; } @@ -277,7 +283,7 @@ void addContentToContainerViaArray(@Mock DockerClient dc, @Mock CopyArchiveToCon }))).thenReturn(catcc); - ContainerUtils.addContentToContainer(dc, containerId, contentPath, userid, group, new StringContent(contentName, content).getContainerEntries().toArray(new ContainerEntry[0])); + ContainerUtils.addContentToContainer(dc, containerId, contentPath, userid, group, new StringContent(contentName, 0777, content).getContainerEntries().toArray(new ContainerEntry[0])); verify(catcc).exec(); } @@ -292,7 +298,7 @@ void addContentToContainerViaStringWithoutUserIdAndGroup(@Mock DockerClient dc, String containerId = "id"; when(dc.copyArchiveToContainerCmd(containerId)).thenReturn(catcc); - when(catcc.withRemotePath(anyString())).thenReturn(catcc); + when(catcc.withRemotePath("")).thenReturn(catcc); when(catcc.withTarInputStream(argThat(x -> { if (x != null) { try (BufferedInputStream bis = new BufferedInputStream(x); @@ -301,7 +307,7 @@ void addContentToContainerViaStringWithoutUserIdAndGroup(@Mock DockerClient dc, TarArchiveEntry entry; while ((entry = (TarArchiveEntry) tais.getNextEntry()) != null) { if (!entry.isDirectory()) { - if (entry.getName().equals(contentPath.substring(1) + "/" + contentName) && entry.getLongUserId() == 0 + if (entry.getName().equals(contentPath.substring(1)+"/"+contentName) && entry.getLongUserId() == 0 && entry.getLongGroupId() == 0) { return true; } @@ -319,7 +325,7 @@ void addContentToContainerViaStringWithoutUserIdAndGroup(@Mock DockerClient dc, }))).thenReturn(catcc); - ContainerUtils.addContentToContainer(dc, containerId, new StringContent(contentPath+"/"+contentName, content).getContainerEntries()); + ContainerUtils.addContentToContainer(dc, containerId, new StringContent(contentPath+"/"+contentName, 0777, content).getContainerEntries()); verify(catcc).exec(); } @@ -334,7 +340,7 @@ void addContentToContainerViaStringWithoutUserIdAndGroupViaArray(@Mock DockerCli String containerId = "id"; when(dc.copyArchiveToContainerCmd(containerId)).thenReturn(catcc); - when(catcc.withRemotePath(anyString())).thenReturn(catcc); + when(catcc.withRemotePath("")).thenReturn(catcc); when(catcc.withTarInputStream(argThat(x -> { if (x != null) { try (BufferedInputStream bis = new BufferedInputStream(x); @@ -361,7 +367,7 @@ void addContentToContainerViaStringWithoutUserIdAndGroupViaArray(@Mock DockerCli }))).thenReturn(catcc); - ContainerUtils.addContentToContainer(dc, containerId, new StringContent(contentPath+"/"+contentName, content).getContainerEntries().toArray(new ContainerEntry[0])); + ContainerUtils.addContentToContainer(dc, containerId, new StringContent(contentPath+"/"+contentName, 0777, content).getContainerEntries().toArray(new ContainerEntry[0])); verify(catcc).exec(); } @@ -376,7 +382,7 @@ void addContentToContainerViaStringWithoutUserIdAndGroupViaNulls(@Mock DockerCli String containerId = "id"; when(dc.copyArchiveToContainerCmd(containerId)).thenReturn(catcc); - when(catcc.withRemotePath(anyString())).thenReturn(catcc); + when(catcc.withRemotePath(contentPath)).thenReturn(catcc); when(catcc.withTarInputStream(argThat(x -> { if (x != null) { try (BufferedInputStream bis = new BufferedInputStream(x); @@ -385,7 +391,7 @@ void addContentToContainerViaStringWithoutUserIdAndGroupViaNulls(@Mock DockerCli TarArchiveEntry entry; while ((entry = (TarArchiveEntry) tais.getNextEntry()) != null) { if (!entry.isDirectory()) { - if (entry.getName().equals(contentPath.substring(1) + "/" + contentName) && entry.getLongUserId() == 0 + if (entry.getName().equals(contentName) && entry.getLongUserId() == 0 && entry.getLongGroupId() == 0) { return true; } @@ -403,7 +409,7 @@ void addContentToContainerViaStringWithoutUserIdAndGroupViaNulls(@Mock DockerCli }))).thenReturn(catcc); - ContainerUtils.addContentToContainer(dc, containerId, contentPath, null, null, contentName, content); + ContainerUtils.addContentToContainer(dc, containerId, contentPath, null, null, contentName, 0777, content); verify(catcc).exec(); } @@ -426,6 +432,10 @@ public long getSize() { return 0; } + public Integer getMode() { + return 0644; + } + public DataSupplier getDataSupplier() { return null; } @@ -457,6 +467,10 @@ public long getSize() { throw BuildpackException.launderThrowable(new IOException("Test")); } + public Integer getMode() { + return 0644; + } + public DataSupplier getDataSupplier() { return null; } @@ -489,6 +503,10 @@ public long getSize() { return 4; } + public Integer getMode() { + return 0644; + } + public DataSupplier getDataSupplier() { return null; } @@ -519,6 +537,10 @@ public long getSize() { return 4; } + public Integer getMode() { + return 0644; + } + public DataSupplier getDataSupplier() { return new DataSupplier() { public InputStream getData() { @@ -554,6 +576,10 @@ public long getSize() { return 4; } + public Integer getMode() { + return 0644; + } + public DataSupplier getDataSupplier() { return new DataSupplier() { public InputStream getData() { @@ -579,8 +605,24 @@ void addContentToContainerViaFile(@Mock DockerClient dc, @Mock FileSystemProvider fsp, @Mock BasicFileAttributes bfa) { - String containerId = "id"; + //this test will cause FileContent to obtain permnissions, which ends up at Files.getPosixPermissions + //sadly, the invocation is on a different thread (handled during the tar stream in/out), so we cannot + //mock Files as a static with mockito, instead the call is delegated via an instance of a class that + //we can swap via reflection, still ugly, but at least functional. + try{ + Field perms = ReflectionUtils.findFields(FileContent.class, + field->field.getName().equals("filePermissions"), + ReflectionUtils.HierarchyTraversalMode.TOP_DOWN) + .get(0); + perms.setAccessible(true); + perms.set(null, new FilePermissions(){ + public Integer getPermissions(File file){ return 0777; } + }); + }catch(Exception e){ + fail(); + } + String containerId = "id"; when(dc.copyArchiveToContainerCmd(containerId)).thenReturn(catcc); when(catcc.withRemotePath(anyString())).thenReturn(catcc); when(catcc.withTarInputStream(argThat(x -> { @@ -592,7 +634,8 @@ void addContentToContainerViaFile(@Mock DockerClient dc, while ((entry = (TarArchiveEntry) tais.getNextEntry()) != null) { if (!entry.isDirectory()) { if (entry.getName().equals("wibble") && entry.getLongUserId() == 0 - && entry.getLongGroupId() == 0 && entry.getSize()==4L) { + && entry.getLongGroupId() == 0 && entry.getSize()==4L && entry.getMode()==(0100000 + 0777)) { + //note 0100000 means 'regular file', 0777 are the perms. (0 prefix is octal) return true; } } @@ -612,8 +655,7 @@ void addContentToContainerViaFile(@Mock DockerClient dc, when(f.isFile()).thenReturn(true); when(f.isDirectory()).thenReturn(false); when(f.getName()).thenReturn("wibble"); - when(f.toPath()).thenReturn(p); - + when(f.toPath()).thenReturn(p); when(p.getFileSystem()).thenReturn(fs); when(fs.provider()).thenReturn(fsp); @@ -628,6 +670,7 @@ void addContentToContainerViaFile(@Mock DockerClient dc, ContainerUtils.addContentToContainer(dc, containerId, "/", 0,0, f); verify(catcc).exec(); + } @Test @@ -635,6 +678,7 @@ void testRemoveContainer(@Mock DockerClient dc, @Mock RemoveContainerCmd rcc) { String containerId = "id"; when(dc.removeContainerCmd(containerId)).thenReturn(rcc); + when(rcc.withForce(anyBoolean())).thenReturn(rcc); ContainerUtils.removeContainer(dc, containerId); verify(rcc).exec(); } diff --git a/client/src/test/java/dev/snowdrop/buildpack/docker/ContentTest.java b/client/src/test/java/dev/snowdrop/buildpack/docker/ContentTest.java index 35a0614..5ce7e25 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/docker/ContentTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/docker/ContentTest.java @@ -14,7 +14,7 @@ void apiRegressionTest() throws Exception{ Content c = new Content(){ @Override public List getContainerEntries() { - return new StringContent("/fish", "stiletto").getContainerEntries(); + return new StringContent("/fish", 0777, "stiletto").getContainerEntries(); } }; diff --git a/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java b/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java index 63c94b7..b7c83e2 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/docker/VolumeUtilsTest.java @@ -97,7 +97,7 @@ void addContentToVolumeViaString(@Mock DockerClient dc ) { scu.when(() -> ContainerUtils.createContainer(eq(dc), anyString(), anyList(), ArgumentMatchers.any())).thenReturn(containerId); - boolean result = VolumeUtils.addContentToVolume(dc, volumeName, "tianon/true", entryName, entryContent); + boolean result = VolumeUtils.addContentToVolume(dc, volumeName, "tianon/true", entryName, 0777, entryContent); assertTrue(result); scu.verify(() -> ContainerUtils.addContentToContainer(eq(dc), eq(containerId), anyString(), anyInt(), anyInt(), ArgumentMatchers.argThat(ce -> { diff --git a/samples/hello-quarkus/pack.java b/samples/hello-quarkus-jbang/pack.java similarity index 100% rename from samples/hello-quarkus/pack.java rename to samples/hello-quarkus-jbang/pack.java diff --git a/samples/hello-quarkus/.dockerignore b/samples/hello-quarkus/.dockerignore new file mode 100755 index 0000000..94810d0 --- /dev/null +++ b/samples/hello-quarkus/.dockerignore @@ -0,0 +1,5 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* +!target/quarkus-app/* \ No newline at end of file diff --git a/samples/hello-quarkus/.gitignore b/samples/hello-quarkus/.gitignore new file mode 100755 index 0000000..8c7863e --- /dev/null +++ b/samples/hello-quarkus/.gitignore @@ -0,0 +1,43 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env + +# Plugin directory +/.quarkus/cli/plugins/ diff --git a/samples/hello-quarkus/.mvn/wrapper/.gitignore b/samples/hello-quarkus/.mvn/wrapper/.gitignore new file mode 100755 index 0000000..e72f5e8 --- /dev/null +++ b/samples/hello-quarkus/.mvn/wrapper/.gitignore @@ -0,0 +1 @@ +maven-wrapper.jar diff --git a/samples/hello-quarkus/.mvn/wrapper/MavenWrapperDownloader.java b/samples/hello-quarkus/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 0000000..84d1e60 --- /dev/null +++ b/samples/hello-quarkus/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +public final class MavenWrapperDownloader +{ + private static final String WRAPPER_VERSION = "3.2.0"; + + private static final boolean VERBOSE = Boolean.parseBoolean( System.getenv( "MVNW_VERBOSE" ) ); + + public static void main( String[] args ) + { + log( "Apache Maven Wrapper Downloader " + WRAPPER_VERSION ); + + if ( args.length != 2 ) + { + System.err.println( " - ERROR wrapperUrl or wrapperJarPath parameter missing" ); + System.exit( 1 ); + } + + try + { + log( " - Downloader started" ); + final URL wrapperUrl = new URL( args[0] ); + final String jarPath = args[1].replace( "..", "" ); // Sanitize path + final Path wrapperJarPath = Paths.get( jarPath ).toAbsolutePath().normalize(); + downloadFileFromURL( wrapperUrl, wrapperJarPath ); + log( "Done" ); + } + catch ( IOException e ) + { + System.err.println( "- Error downloading: " + e.getMessage() ); + if ( VERBOSE ) + { + e.printStackTrace(); + } + System.exit( 1 ); + } + } + + private static void downloadFileFromURL( URL wrapperUrl, Path wrapperJarPath ) + throws IOException + { + log( " - Downloading to: " + wrapperJarPath ); + if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null ) + { + final String username = System.getenv( "MVNW_USERNAME" ); + final char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray(); + Authenticator.setDefault( new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication( username, password ); + } + } ); + } + try ( InputStream inStream = wrapperUrl.openStream() ) + { + Files.copy( inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING ); + } + log( " - Downloader complete" ); + } + + private static void log( String msg ) + { + if ( VERBOSE ) + { + System.out.println( msg ); + } + } + +} diff --git a/samples/hello-quarkus/.mvn/wrapper/maven-wrapper.properties b/samples/hello-quarkus/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 0000000..346d645 --- /dev/null +++ b/samples/hello-quarkus/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/samples/hello-quarkus/README.md b/samples/hello-quarkus/README.md new file mode 100755 index 0000000..798d9d7 --- /dev/null +++ b/samples/hello-quarkus/README.md @@ -0,0 +1,61 @@ +# quarkus-hello + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./mvnw compile quarkus:dev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./mvnw package +``` +It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./mvnw package -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./mvnw package -Dnative +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./mvnw package -Dnative -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./target/quarkus-hello-1.0-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling. + +## Related Guides + +- Kubernetes ([guide](https://quarkus.io/guides/kubernetes)): Generate Kubernetes resources from annotations +- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A Jakarta REST implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + +## Provided Code + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) diff --git a/samples/hello-quarkus/id-debug.sh b/samples/hello-quarkus/id-debug.sh new file mode 100755 index 0000000..0d5bfe3 --- /dev/null +++ b/samples/hello-quarkus/id-debug.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# A simple script to dump uid/gid & /workspace permissions during a build. +# +# This can be very useful when implementing a platform, to determine if /workspace +# has permissions for the executing uid/gid. + +id +ls -aln /workspace + +exit 0 diff --git a/samples/hello-quarkus/mvnw b/samples/hello-quarkus/mvnw new file mode 100755 index 0000000..8d937f4 --- /dev/null +++ b/samples/hello-quarkus/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/samples/hello-quarkus/mvnw.cmd b/samples/hello-quarkus/mvnw.cmd new file mode 100755 index 0000000..c4586b5 --- /dev/null +++ b/samples/hello-quarkus/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/samples/hello-quarkus/pom.xml b/samples/hello-quarkus/pom.xml new file mode 100755 index 0000000..aea752a --- /dev/null +++ b/samples/hello-quarkus/pom.xml @@ -0,0 +1,146 @@ + + + 4.0.0 + dev.snowdrop + quarkus-hello + 1.0 + + 3.12.1 + + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 3.8.3 + true + 3.2.5 + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-kubernetes + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-container-image-buildpack + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + org.codehaus.mojo + exec-maven-plugin + 1.5.0 + + + compile + + exec + + + id-debug.sh + ${basedir} + + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + native + + + native + + + + false + native + + + + diff --git a/samples/hello-quarkus/src/main/docker/Dockerfile.jvm b/samples/hello-quarkus/src/main/docker/Dockerfile.jvm new file mode 100755 index 0000000..d917461 --- /dev/null +++ b/samples/hello-quarkus/src/main/docker/Dockerfile.jvm @@ -0,0 +1,97 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/quarkus-hello-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/quarkus-hello-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/quarkus-hello-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + +ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] + diff --git a/samples/hello-quarkus/src/main/docker/Dockerfile.legacy-jar b/samples/hello-quarkus/src/main/docker/Dockerfile.legacy-jar new file mode 100755 index 0000000..bd6d4bc --- /dev/null +++ b/samples/hello-quarkus/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,93 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/quarkus-hello-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/quarkus-hello-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/quarkus-hello-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 + +ENV LANGUAGE='en_US:en' + + +COPY target/lib/* /deployments/lib/ +COPY target/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + +ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] diff --git a/samples/hello-quarkus/src/main/docker/Dockerfile.native b/samples/hello-quarkus/src/main/docker/Dockerfile.native new file mode 100755 index 0000000..ae66066 --- /dev/null +++ b/samples/hello-quarkus/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./mvnw package -Dnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/quarkus-hello . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/quarkus-hello +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/samples/hello-quarkus/src/main/docker/Dockerfile.native-micro b/samples/hello-quarkus/src/main/docker/Dockerfile.native-micro new file mode 100755 index 0000000..6cd684e --- /dev/null +++ b/samples/hello-quarkus/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./mvnw package -Dnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/quarkus-hello . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/quarkus-hello +# +### +FROM quay.io/quarkus/quarkus-micro-image:2.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/samples/hello-quarkus/src/main/java/dev/snowdrop/GreetingResource.java b/samples/hello-quarkus/src/main/java/dev/snowdrop/GreetingResource.java new file mode 100755 index 0000000..9e610a8 --- /dev/null +++ b/samples/hello-quarkus/src/main/java/dev/snowdrop/GreetingResource.java @@ -0,0 +1,16 @@ +package dev.snowdrop; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello from RESTEasy Reactive"; + } +} diff --git a/samples/hello-quarkus/src/main/resources/META-INF/resources/index.html b/samples/hello-quarkus/src/main/resources/META-INF/resources/index.html new file mode 100755 index 0000000..bbd054a --- /dev/null +++ b/samples/hello-quarkus/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,285 @@ + + + + + quarkus-hello - 1.0 + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.properties

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • Kubernetes (guide)
  • +
  • RESTEasy Reactive (guide)
  • +
  • Container Image Buildpack
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/samples/hello-quarkus/src/main/resources/application.properties b/samples/hello-quarkus/src/main/resources/application.properties new file mode 100755 index 0000000..e69de29 diff --git a/samples/hello-quarkus/src/test/java/dev/snowdrop/GreetingResourceIT.java b/samples/hello-quarkus/src/test/java/dev/snowdrop/GreetingResourceIT.java new file mode 100755 index 0000000..27f65ae --- /dev/null +++ b/samples/hello-quarkus/src/test/java/dev/snowdrop/GreetingResourceIT.java @@ -0,0 +1,8 @@ +package dev.snowdrop; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class GreetingResourceIT extends GreetingResourceTest { + // Execute the same tests but in packaged mode. +} diff --git a/samples/hello-quarkus/src/test/java/dev/snowdrop/GreetingResourceTest.java b/samples/hello-quarkus/src/test/java/dev/snowdrop/GreetingResourceTest.java new file mode 100755 index 0000000..3eaf134 --- /dev/null +++ b/samples/hello-quarkus/src/test/java/dev/snowdrop/GreetingResourceTest.java @@ -0,0 +1,20 @@ +package dev.snowdrop; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +class GreetingResourceTest { + @Test + void testHelloEndpoint() { + given() + .when().get("/hello") + .then() + .statusCode(200) + .body(is("Hello from RESTEasy Reactive")); + } + +} \ No newline at end of file From 8607fe8816bb34d97be99168b6d1634220b8bd1d Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Mon, 15 Apr 2024 13:39:47 -0400 Subject: [PATCH 7/9] Update samples --- samples/{hello-quarkus-jbang => hello-quarkus}/pack.java | 0 samples/hello-quarkus/pom.xml | 5 ++++- samples/hello-spring/pack.java | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) rename samples/{hello-quarkus-jbang => hello-quarkus}/pack.java (100%) diff --git a/samples/hello-quarkus-jbang/pack.java b/samples/hello-quarkus/pack.java similarity index 100% rename from samples/hello-quarkus-jbang/pack.java rename to samples/hello-quarkus/pack.java diff --git a/samples/hello-quarkus/pom.xml b/samples/hello-quarkus/pom.xml index aea752a..1d8e104 100755 --- a/samples/hello-quarkus/pom.xml +++ b/samples/hello-quarkus/pom.xml @@ -92,8 +92,11 @@ exec - id-debug.sh + /bin/sh ${basedir} + + ${basedir}/id-debug.sh + diff --git a/samples/hello-spring/pack.java b/samples/hello-spring/pack.java index 67f1760..0962062 100755 --- a/samples/hello-spring/pack.java +++ b/samples/hello-spring/pack.java @@ -9,7 +9,8 @@ public class pack { public static void main(String... args) { int exitCode = BuildConfig.builder() - .withOutputImage(new ImageReference("snowdrop/hello-quarkus:latest")) + .withBuilderImage(new ImageReference("paketocommunity/builder-ubi-base")) + .withOutputImage(new ImageReference("snowdrop/hello-spring:latest")) .addNewFileContentApplication(new File(".")) .build() .getExitCode(); From da0df461eb9ab3063d980972869aaa892ba0f3b6 Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Mon, 29 Apr 2024 11:10:44 -0400 Subject: [PATCH 8/9] Update integration tests, fixes for docker volume ownership Signed-off-by: Ozzy Osborne --- .github/workflows/build.yml | 2 +- .github/workflows/integration-tests.yml | 17 +++- README.md | 4 +- .../snowdrop/buildpack/BuildpackBuild.java | 11 +++ .../buildpack/docker/BuildContainerUtils.java | 7 +- .../buildpack/docker/VolumeUtils.java | 5 +- .../lifecycle/LifecycleExecutor.java | 16 +++- .../lifecycle/LifecyclePhaseFactory.java | 11 ++- .../buildpack/lifecycle/phases/Analyzer.java | 9 +- .../buildpack/lifecycle/phases/Creator.java | 5 +- .../buildpack/lifecycle/phases/Detector.java | 5 - .../buildpack/lifecycle/phases/Exporter.java | 7 +- .../buildpack/lifecycle/phases/Restorer.java | 5 +- .../lifecycle/phases/AnalzyerTest.java | 2 +- .../lifecycle/phases/DetectorTest.java | 96 +------------------ .../lifecycle/phases/ExporterTest.java | 2 +- pom.xml | 10 +- samples/hello-quarkus/pack.java | 20 +++- samples/hello-quarkus/pom.xml | 2 +- samples/hello-spring/pack.java | 26 ++++- 20 files changed, 125 insertions(+), 137 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 855fce6..93dc6d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [8, 11] + java: [8, 11, 17] steps: - name: Checkout uses: actions/checkout@v2.3.4 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 36bc8d1..b3aa744 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [8, 11] + java: [8, 11, 17] jbang: [0.84.2] steps: - name: Checkout @@ -68,6 +68,21 @@ jobs: - name: Integration Tests run: | + if [ ${{ github.event_name }} = 'pull_request' ]; then + REPO="${{ github.event.pull_request.head.repo.full_name }}" + REPO_NAME=`echo $REPO | cut -d'/' -f2` + REPO_OWNER=`echo $REPO | cut -d'/' -f1` + SHA="${{ github.event.pull_request.head.sha }}" + else + REPO="$GITHUB_REPOSITORY" + REPO_NAME=`echo $REPO | cut -d'/' -f2` + REPO_OWNER=`echo $REPO | cut -d'/' -f1` + SHA=$GITHUB_SHA + fi + export CURRENT_WORKFLOW_DEP=com.github.$REPO_OWNER.$REPO_NAME:buildpack-client:$SHA + echo Tests using dependency $CURRENT_WORKFLOW_DEP cd samples/hello-spring ./pack.java + cd ../../samples/hello-quarkus + ./pack.java diff --git a/README.md b/README.md index e95c297..bd6a536 100644 --- a/README.md +++ b/README.md @@ -245,13 +245,13 @@ public class pack { ./pack.java ``` -See how it's used in the included [samples](./samples). +The samples use jbang too, but allow the version of the library to be set via an env var for use in our tests! [samples](./samples). ## FAQ: **Will this work with Podman?:** -It should do =) I've got some testing to run still. +Yes, tested with podman on fedora, rootless and rootful. **Does this work on Windows?:** diff --git a/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java b/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java index b4e5f0c..f5bacfa 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java +++ b/client/src/main/java/dev/snowdrop/buildpack/BuildpackBuild.java @@ -5,6 +5,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import dev.snowdrop.buildpack.config.DockerConfig; import dev.snowdrop.buildpack.config.PlatformConfig; import dev.snowdrop.buildpack.docker.BuildContainerUtils; @@ -13,6 +16,7 @@ import dev.snowdrop.buildpack.utils.LifecycleMetadata; public class BuildpackBuild { + private static final Logger log = LoggerFactory.getLogger(BuildpackBuild.class); //platforms stored in order of preference. private final List supportedPlatformLevels =Stream.of("0.12", "0.11", "0.10", "0.9", "0.8", "0.7", "0.6", "0.5", "0.4").collect(Collectors.toList()); @@ -82,6 +86,13 @@ public int build(){ null, null); try{ + log.info("Initiating buildpack build with config \n"+ + " - ephemeralBuilder "+extendedBuilder.getImage().getReference()+"\n"+ + " - baseBuilder "+builder.getImage().getReference()+"\n"+ + " - activePlatformLevel "+activePlatformLevel+"\n"+ + " - build uid:gid "+extendedBuilder.getUserId()+":"+extendedBuilder.getGroupId()+"\n"+ + " - withExtensions "+extendedBuilder.hasExtensions()); + // execute the build using the lifecycle. LifecycleExecutor le = new LifecycleExecutor(config, builder, extendedBuilder, activePlatformLevel); return le.execute(); diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java index f2d5dc4..0af83fa 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/BuildContainerUtils.java @@ -13,6 +13,8 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; @@ -26,6 +28,8 @@ //used to create ephemeral builder, streaming tarball content from one image to another. public class BuildContainerUtils { + private static final Logger log = LoggerFactory.getLogger(BuildContainerUtils.class); + private static InputStream getArchiveStreamFromContainer(DockerClient dc, String containerId, String path){ CopyArchiveFromContainerCmd copyLifecyleFromImageCmd = dc.copyArchiveFromContainerCmd(containerId, path); try{ @@ -119,6 +123,8 @@ public void run() { */ public static BuilderImage createBuildImage(DockerClient dc, BuilderImage baseBuilder, ImageReference lifecycle, List extensions, List buildpacks) { + log.debug("Creating Ephemeral image, from "+baseBuilder.getImage().getReference()+" with uid:gid "+baseBuilder.getUserId()+":"+baseBuilder.getGroupId()); + List command = Stream.of("").collect(Collectors.toList()); String builderContainerId = ContainerUtils.createContainer(dc, baseBuilder.getImage().getReference(), command); @@ -140,7 +146,6 @@ public static BuilderImage createBuildImage(DockerClient dc, BuilderImage baseBu LifecyclePhaseFactory.LAYERS_VOL_PATH, LifecyclePhaseFactory.CACHE_VOL_PATH, LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH, - LifecyclePhaseFactory.DOCKER_SOCKET_PATH, LifecyclePhaseFactory.PLATFORM_VOL_PATH, LifecyclePhaseFactory.PLATFORM_VOL_PATH+LifecyclePhaseFactory.ENV_PATH_PREFIX) .collect(Collectors.toList())); diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java index de65526..e277a44 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/VolumeUtils.java @@ -15,7 +15,10 @@ public class VolumeUtils { private static final Logger log = LoggerFactory.getLogger(VolumeUtils.class); - final static String mountPrefix = "/volumecontent"; + //It is critical that we use a mountpoint that exists and is not owned by root, otherwise the volume can gain + //'sticky' root ownership that cannot be undone. As such, we use the /workspace dir, as we know the ephemeral builder + //will have this present as owned by build uid/gid. + final static String mountPrefix = "/workspace"; public static boolean createVolumeIfRequired(DockerClient dc, String volumeName) { if (!exists(dc, volumeName)) { diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java index e9a3b7c..823e3e5 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecycleExecutor.java @@ -1,5 +1,7 @@ package dev.snowdrop.buildpack.lifecycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.tomlj.Toml; import org.tomlj.TomlParseResult; @@ -12,6 +14,8 @@ public class LifecycleExecutor { + private static final Logger log = LoggerFactory.getLogger(LifecycleExecutor.class); + private final BuildConfig config; private final LifecyclePhaseFactory factory; private final Version activePlatformLevel; @@ -19,17 +23,19 @@ public class LifecycleExecutor { private boolean useCreator(boolean extensionsPresent, Boolean trustBuilder) { if(trustBuilder==null){ - return extensionsPresent; + log.debug("Trusted Builder not requested, extensions are present? "+extensionsPresent); + return !extensionsPresent; } - if(!trustBuilder) + if(!trustBuilder){ + log.debug("Trusted builder explicitly set to false"); return false; - else{ + }else{ if(extensionsPresent) { - config.getLogConfig().getLogger().stdout("request to trust builder ignored, as extensions are present"); + log.info("request to trust builder ignored, as extensions are present"); return false; } - + log.debug("request to trust builder honored, will use creator"); return true; } } diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java index 1d95d0a..7bc04f2 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/LifecyclePhaseFactory.java @@ -43,7 +43,7 @@ public class LifecyclePhaseFactory { public final static String DOCKER_SOCKET_PATH = "/var/run/docker.sock"; public final static String APP_PATH_PREFIX = ""; //previously /content to avoid permissions, should not be issue with extended builder. - public final static String ENV_PATH_PREFIX = "/env"; + public final static String ENV_PATH_PREFIX = ""; private final DockerConfig dockerConfig; private final CacheConfig buildCacheConfig; @@ -112,7 +112,8 @@ public String getContainerForPhase(String args[], Integer runAsId){ if(dockerConfig.getUseDaemon()) log.info("- mounted " + dockerConfig.getDockerSocket() + " at " + LifecyclePhaseFactory.DOCKER_SOCKET_PATH); log.info("- mounted " + outputVolume + " at " + LAYERS_VOL_PATH); - log.info("- build container id " + id); + log.info("- container id " + id); + log.info("- image reference "+builder.getImage().getReference()); return id; } @@ -167,7 +168,7 @@ public void createVolumes(List content){ log.info("Adding aplication to volume "+applicationVolume); VolumeUtils.addContentToVolume(dockerConfig.getDockerClient(), applicationVolume, - originalBuilder.getImage().getReference(), + builder.getImage().getReference(), LifecyclePhaseFactory.APP_PATH_PREFIX, builder.getUserId(), builder.getGroupId(), @@ -189,13 +190,13 @@ public void createVolumes(List content){ //add the environment entries to the platform volume. List envEntries = platformConfig.getEnvironment().entrySet() .stream() - .flatMap(e -> new StringContent(e.getKey(), 0777, e.getValue()).getContainerEntries().stream()) + .flatMap(e -> new StringContent("env/"+e.getKey(), 0777, e.getValue()).getContainerEntries().stream()) .collect(Collectors.toList()); log.info("Adding platform entries to platform volume "+platformVolume); VolumeUtils.addContentToVolume(dockerConfig.getDockerClient(), platformVolume, - originalBuilder.getImage().getReference(), + builder.getImage().getReference(), LifecyclePhaseFactory.ENV_PATH_PREFIX, builder.getUserId(), builder.getGroupId(), diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java index 64e9352..0b6e65c 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Analyzer.java @@ -55,13 +55,14 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us args.addArg("-launch-cache", LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH); } - //if using daemon, add daemon arg + int runAsId = factory.getBuilderImage().getUserId(); + + //if using daemon, add daemon arg, run as root if(factory.getDockerConfig().getUseDaemon()){ - args.addArg("-daemon"); + args.addArg("-daemon"); + runAsId = 0; } - - int runAsId = factory.getBuilderImage().getUserId(); String id = factory.getContainerForPhase(args.toArray(), runAsId); try{ log.info("- analyze container id " + id+ " will be run with uid "+runAsId); diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java index 77752ba..9e2cd6b 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Creator.java @@ -36,7 +36,6 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us args.addArg("-platform", LifecyclePhaseFactory.PLATFORM_VOL_PATH); args.addArg("-run-image", factory.getBuilderImage().getRunImages(factory.getPlatformLevel()).get(0)); args.addArg("-log-level", factory.getLogConfig().getLogLevel()); - args.addArg("-skip-restore", "false"); if(factory.getPlatformLevel().atLeast("0.9") && factory.getDockerConfig().getUseDaemon()){ args.addArg("-launch-cache", LifecyclePhaseFactory.LAUNCH_CACHE_VOL_PATH); @@ -46,14 +45,12 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us if(factory.getDockerConfig().getUseDaemon()){ args.addArg("-daemon"); } - - // TODO: add labels for container for creator etc (as per spec) //creator process always has to run as root. int runAsId = 0; String id = factory.getContainerForPhase(args.toArray(), runAsId); try{ - log.info("- creator container id " + id+ " will be run with uid "+runAsId); + log.info("- creator container id " + id+ " will be run with uid "+runAsId+" and args "+args); // launch the container! log.info("- launching build container"); diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java index cfc9d72..6a7a2b6 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Detector.java @@ -52,11 +52,6 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us args.addArg("-run", "/cnb/run.toml"); } - //if using daemon, add daemon arg - if(factory.getDockerConfig().getUseDaemon()){ - args.addArg("-daemon"); - } - // detector phase must run as non-root int runAsId = factory.getBuilderImage().getUserId(); String id = factory.getContainerForPhase(args.toArray(), runAsId); diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java index 3be2d60..6f23afd 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Exporter.java @@ -50,12 +50,15 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us args.addArg("-run", "/cnb/run.toml"); } - //if using daemon, add daemon arg + int runAsId = factory.getBuilderImage().getUserId(); + + //if using daemon, add daemon arg, run as root if(factory.getDockerConfig().getUseDaemon()){ args.addArg("-daemon"); + runAsId = 0; } - int runAsId = factory.getBuilderImage().getUserId(); + String id = factory.getContainerForPhase(args.toArray(), runAsId); try{ log.info("- export container id " + id+ " will be run with uid "+runAsId); diff --git a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java index 3979205..b2496a9 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java +++ b/client/src/main/java/dev/snowdrop/buildpack/lifecycle/phases/Restorer.java @@ -51,11 +51,14 @@ public ContainerStatus runPhase(dev.snowdrop.buildpack.Logger logger, boolean us args.addArg("-build-image", originalBuilder.getImage().getReference()); } + int runAsId = factory.getBuilderImage().getUserId(); + if(factory.getPlatformLevel().atLeast("0.12") && factory.getDockerConfig().getUseDaemon()){ args.addArg("-daemon"); + runAsId = 0; } - int runAsId = factory.getBuilderImage().getUserId(); + String id = factory.getContainerForPhase(args.toArray(), runAsId); try{ log.info("- restorer container id " + id+ " will be run with uid "+runAsId+" and args "+args); diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/AnalzyerTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/AnalzyerTest.java index b1d49d9..0c43695 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/AnalzyerTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/AnalzyerTest.java @@ -368,6 +368,6 @@ void test9OnwardsWithDaemon(@Mock LifecyclePhaseFactory factory, verify(logCmd).withTimestamps(true); verify(dockerClient).logContainerCmd(CONTAINER_ID); - verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + verify(factory).getContainerForPhase(any(String[].class), eq(0)); //running as root with daemon } } diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/DetectorTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/DetectorTest.java index 878a76c..66a800a 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/DetectorTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/DetectorTest.java @@ -497,97 +497,7 @@ void test10WithoutExtensions(@Mock LifecyclePhaseFactory factory, verify(logCmd).withTimestamps(true); verify(dockerClient).logContainerCmd(CONTAINER_ID); verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); - } - - @SuppressWarnings("resource") - @Test - void test10WithDaemon(@Mock LifecyclePhaseFactory factory, - @Mock BuilderImage builder, - @Mock LogConfig logConfig, - @Mock DockerConfig dockerConfig, - @Mock DockerClient dockerClient, - @Mock StartContainerCmd startCmd, - @Mock LogContainerCmd logCmd, - @Mock WaitContainerCmd waitCmd, - @Mock WaitContainerResultCallback waitResult, - @Mock Logger logger - ) { - - String PLATFORM_LEVEL="0.10"; - String LOG_LEVEL="debug"; - String CONTAINER_ID="999"; - int CONTAINER_RC=99; - int USER_ID=77; - int GROUP_ID=88; - String OUTPUT_IMAGE="stiletto"; - boolean USE_DAEMON=true; - boolean HAS_EXTENSIONS=true; - - lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); - lenient().when(dockerConfig.getDockerClient()).thenReturn(dockerClient); - lenient().when(factory.getDockerConfig()).thenReturn(dockerConfig); - - lenient().doNothing().when(startCmd).exec(); - lenient().when(dockerClient.startContainerCmd(any())).thenReturn(startCmd); - - lenient().when(logCmd.withFollowStream(any())).thenReturn(logCmd); - lenient().when(logCmd.withStdOut(any())).thenReturn(logCmd); - lenient().when(logCmd.withStdErr(any())).thenReturn(logCmd); - lenient().when(logCmd.withTimestamps(any())).thenReturn(logCmd); - lenient().when(logCmd.exec(any())).thenReturn(null); - lenient().when(dockerClient.logContainerCmd(any())).thenReturn(logCmd); - - lenient().when(waitCmd.exec(any())).thenReturn(waitResult); - lenient().when(waitResult.awaitStatusCode()).thenReturn(CONTAINER_RC); - lenient().when(dockerClient.waitContainerCmd(any())).thenReturn(waitCmd); - - lenient().when(logConfig.getLogLevel()).thenReturn(LOG_LEVEL); - lenient().when(factory.getLogConfig()).thenReturn(logConfig); - - lenient().when(builder.getUserId()).thenReturn(USER_ID); - lenient().when(builder.getGroupId()).thenReturn(GROUP_ID); - lenient().when(builder.getRunImages(any())).thenReturn(Stream.of("runImage1", "runImage2").collect(Collectors.toList())); - lenient().when(builder.hasExtensions()).thenReturn(HAS_EXTENSIONS); - lenient().when(factory.getBuilderImage()).thenReturn(builder); - - lenient().when(factory.getPlatformLevel()).thenReturn(new Version(PLATFORM_LEVEL)); - - lenient().when(factory.getContainerForPhase(argsCaptor.capture(), any())).thenReturn(CONTAINER_ID); - - lenient().when(factory.getOutputImage()).thenReturn(new ImageReference(OUTPUT_IMAGE)); - - try (MockedStatic containerUtils = mockStatic(ContainerUtils.class)) { - Detector d = new Detector(factory); - - containerUtils.when(() -> ContainerUtils.getFileFromContainer(eq(dockerClient), any(), any())).thenReturn("EmptyTOML".getBytes()); - - ContainerStatus cs = d.runPhase(logger, true); - - assertNotNull(cs); - assertEquals(CONTAINER_ID, cs.getContainerId()); - assertEquals(CONTAINER_RC, cs.getRc()); - - assertEquals("EmptyTOML", new String(d.getAnalyzedToml())); - } - - String[] args = argsCaptor.getValue(); - assertNotNull(args); - //verify 1st & last elements - assertEquals("/cnb/lifecycle/detector", args[0]); - assertNotEquals(args[args.length-1], OUTPUT_IMAGE); - - List argList = Arrays.asList(args); - //verify generated for a 0.10 run with extensions - assertTrue(argList.contains("-generated")); - //verify dameon - assertTrue(argList.contains("-daemon")); - //verify log as expected - assertTrue(argList.contains(LOG_LEVEL)); - - verify(logCmd).withTimestamps(true); - verify(dockerClient).logContainerCmd(CONTAINER_ID); - verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); - } + } @SuppressWarnings("resource") @Test @@ -610,7 +520,7 @@ void testDisableTimestamps(@Mock LifecyclePhaseFactory factory, int USER_ID=77; int GROUP_ID=88; String OUTPUT_IMAGE="stiletto"; - boolean USE_DAEMON=true; + boolean USE_DAEMON=false; boolean HAS_EXTENSIONS=true; lenient().when(dockerConfig.getUseDaemon()).thenReturn(USE_DAEMON); @@ -670,7 +580,7 @@ void testDisableTimestamps(@Mock LifecyclePhaseFactory factory, //verify generated for a 0.10 run with extensions assertTrue(argList.contains("-generated")); //verify dameon - assertTrue(argList.contains("-daemon")); + assertFalse(argList.contains("-daemon")); //verify log as expected assertTrue(argList.contains(LOG_LEVEL)); diff --git a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExporterTest.java b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExporterTest.java index 7f59240..531cf87 100644 --- a/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExporterTest.java +++ b/client/src/test/java/dev/snowdrop/buildpack/lifecycle/phases/ExporterTest.java @@ -528,7 +528,7 @@ void testWithDaemon(@Mock LifecyclePhaseFactory factory, verify(logCmd).withTimestamps(true); verify(dockerClient).logContainerCmd(CONTAINER_ID); - verify(factory).getContainerForPhase(any(String[].class), eq(USER_ID)); + verify(factory).getContainerForPhase(any(String[].class), eq(0)); } } diff --git a/pom.xml b/pom.xml index b9daf67..c30ae07 100644 --- a/pom.xml +++ b/pom.xml @@ -101,11 +101,11 @@
- - org.tomlj - tomlj - 1.1.1 - + + org.tomlj + tomlj + 1.1.1 + org.apache.commons commons-compress diff --git a/samples/hello-quarkus/pack.java b/samples/hello-quarkus/pack.java index 520cf5b..ee149c0 100755 --- a/samples/hello-quarkus/pack.java +++ b/samples/hello-quarkus/pack.java @@ -1,19 +1,35 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS dev.snowdrop:buildpack-client:0.0.6 +//REPOS mavencentral,jitpack +//DEPS org.slf4j:slf4j-simple:1.7.30 +//DEPS ${env.CURRENT_WORKFLOW_DEP:dev.snowdrop:buildpack-client:0.0.7-SNAPSHOT} + + import java.io.File; import dev.snowdrop.buildpack.*; +import dev.snowdrop.buildpack.config.*; import dev.snowdrop.buildpack.docker.*; public class pack { public static void main(String... args) { + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack","debug"); + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack.docker","debug"); + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack.lifecycle","debug"); + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack.lifecycle.phases","debug"); + int exitCode = BuildConfig.builder() - .withBuilderImage(new ImageReference("paketocommunity/builder-ubi-base")) + .withBuilderImage(new ImageReference("paketocommunity/builder-ubi-base:latest")) .withOutputImage(new ImageReference("snowdrop/hello-quarkus:latest")) + .withNewLogConfig() + .withLogger(new SystemLogger()) + .withLogLevel("debug") + .and() .addNewFileContentApplication(new File(".")) .build() .getExitCode(); + + System.exit(exitCode); } } diff --git a/samples/hello-quarkus/pom.xml b/samples/hello-quarkus/pom.xml index 1d8e104..5a48098 100755 --- a/samples/hello-quarkus/pom.xml +++ b/samples/hello-quarkus/pom.xml @@ -4,10 +4,10 @@ 4.0.0 dev.snowdrop quarkus-hello + Snowdrop :: Java Buildpack Client :: Samples :: Hello Quarkus 1.0 3.12.1 - UTF-8 UTF-8 quarkus-bom diff --git a/samples/hello-spring/pack.java b/samples/hello-spring/pack.java index 0962062..30c2a44 100755 --- a/samples/hello-spring/pack.java +++ b/samples/hello-spring/pack.java @@ -1,18 +1,40 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS dev.snowdrop:buildpack-client:0.0.6 +//REPOS mavencentral,jitpack +//DEPS org.slf4j:slf4j-simple:1.7.30 +//DEPS ${env.CURRENT_WORKFLOW_DEP:dev.snowdrop:buildpack-client:0.0.7-SNAPSHOT} + import java.io.File; +import java.util.HashMap; import dev.snowdrop.buildpack.*; +import dev.snowdrop.buildpack.config.*; import dev.snowdrop.buildpack.docker.*; public class pack { public static void main(String... args) { + + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack","debug"); + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack.docker","debug"); + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack.lifecycle","debug"); + System.setProperty("org.slf4j.simpleLogger.log.dev.snowdrop.buildpack.lifecycle.phases","debug"); + + HashMap env = new HashMap<>(); + int exitCode = BuildConfig.builder() - .withBuilderImage(new ImageReference("paketocommunity/builder-ubi-base")) + .withBuilderImage(new ImageReference("paketocommunity/builder-ubi-base:latest")) .withOutputImage(new ImageReference("snowdrop/hello-spring:latest")) + .withNewLogConfig() + .withLogger(new SystemLogger()) + .withLogLevel("debug") + .and() + .withNewPlatformConfig() + .withEnvironment(env) + .and() .addNewFileContentApplication(new File(".")) .build() .getExitCode(); + + System.exit(exitCode); } } From 4b218a37d21b3af2d8c6ceb74c432ed398b5e84e Mon Sep 17 00:00:00 2001 From: Ozzy Osborne Date: Wed, 5 Jun 2024 13:30:16 -0400 Subject: [PATCH 9/9] Review updates --- README.md | 13 +++++++------ .../buildpack/docker/DockerClientUtils.java | 12 +++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index bd6a536..adf969b 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ or ```java int exitCode = BuildConfig.builder() .withNewLogConfig() - .withLogger(new SystemLogger()) + .withLogger(new Slf4jLogger()) .withLogLevel("debug") .and() .withOutputImage(new ImageReference("test/testimage:latest")) @@ -251,7 +251,7 @@ The samples use jbang too, but allow the version of the library to be set via an **Will this work with Podman?:** -Yes, tested with podman on fedora, rootless and rootful. +Yes, tested with Podman 4.7.0 on Fedora, rootless and rootful. **Does this work on Windows?:** @@ -265,10 +265,11 @@ Tested with Ubuntu & Fedora with Docker **Can I supply buildpacks/extensions to add to a builder like pack?:** -The code should support this now, internally. It hasn't been exposed yet, as -in addition to supplying the buildpacks/extensions, a new order.toml must be -created combining the builder order with supplied content, and work is required -to see how that should look. +The code is structured to allow for this, but the feature is not exposed via the builder api, +as using additional buildpacks/extensions requires updating order.toml in the base builder, +and that would require additional config to either override, or specify the behavior for manipulating +the toml. If you are interested in this kind of functionality, raise an Issue so I can better understand +the use case, or send a PR! diff --git a/client/src/main/java/dev/snowdrop/buildpack/docker/DockerClientUtils.java b/client/src/main/java/dev/snowdrop/buildpack/docker/DockerClientUtils.java index 576b69b..ead953e 100644 --- a/client/src/main/java/dev/snowdrop/buildpack/docker/DockerClientUtils.java +++ b/client/src/main/java/dev/snowdrop/buildpack/docker/DockerClientUtils.java @@ -73,18 +73,16 @@ public static String getDockerHost() { int uid = (Integer)Files.getAttribute(Paths.get("/proc/self"), "unix:uid"); File podmanUserSock = new File("/var/run/user/"+uid+"/podman/podman.sock"); if(podmanUserSock.exists()){ - return "unix:///var/run/user/1000/podman/podman.sock"; + return "unix:///var/run/user/"+uid+"/podman/podman.sock"; } }catch(IOException io){ //ignore. } - - //none of the known linux filesystem locations had socket files, default to docker - //and assume the user has a plan we don't know about =) - return "unix:///var/run/docker.sock"; } - default: - return "unix:///var/run/docker.sock"; } + + //none of the known locations had socket files, default to docker + //and assume the user has a plan we don't know about =) + return "unix:///var/run/docker.sock"; } }