From bf8dec2fbb17b98bbeda7c87cf7f27eef36f4d96 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Fri, 8 Sep 2023 19:35:17 +0530 Subject: [PATCH] feat (jkube-kit/config/service) : Add support for Buildpacks + Add new build strategy called `buildpacks` + Add new BuildpacksBuildService that downloads pack cli from GitHub and executes a command to create a container image Signed-off-by: Rohan Kumar --- CHANGELOG.md | 1 + jkube-kit/build/service/buildpacks/pom.xml | 59 ++++++ .../BuildPackBuildImageCommand.java | 131 +++++++++++++ .../buildpacks/BuildPackBuildUtil.java | 106 +++++++++++ .../buildpacks/BuildPackVersionCommand.java | 40 ++++ .../BuildPackBuildImageCommandTest.java | 130 +++++++++++++ .../buildpacks/BuildPackBuildUtilTest.java | 178 +++++++++++++++++ .../BuildPackVersionCommandTest.java | 49 +++++ .../org.mockito.plugins.MockMaker | 1 + .../common/util/GitHubCliDownloaderUtil.java | 129 +++++++++++++ .../util/GitHubCliDownloaderUtilTest.java | 81 ++++++++ .../pack-v0.31.0-linux.tgz | Bin 0 -> 112 bytes .../pack-v0.31.0-windows.zip | Bin 0 -> 166 bytes .../image/build/BuildConfiguration.java | 17 ++ .../image/build/BuildPackConfiguration.java | 116 +++++++++++ .../image/build/JKubeBuildStrategy.java | 3 +- .../build/BuildPackConfigurationTest.java | 42 ++++ jkube-kit/config/service/pom.xml | 4 + .../kubernetes/BuildPackBuildService.java | 79 ++++++++ .../resources/META-INF/jkube/build-service | 1 + .../JKubeServiceHubBuildServiceTest.java | 2 + .../kubernetes/BuildPackBuildServiceTest.java | 180 ++++++++++++++++++ jkube-kit/parent/pom.xml | 8 +- jkube-kit/pom.xml | 1 + 24 files changed, 1356 insertions(+), 2 deletions(-) create mode 100644 jkube-kit/build/service/buildpacks/pom.xml create mode 100644 jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildImageCommand.java create mode 100644 jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildUtil.java create mode 100644 jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackVersionCommand.java create mode 100644 jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildImageCommandTest.java create mode 100644 jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildUtilTest.java create mode 100644 jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackVersionCommandTest.java create mode 100644 jkube-kit/build/service/buildpacks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/util/GitHubCliDownloaderUtil.java create mode 100644 jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/util/GitHubCliDownloaderUtilTest.java create mode 100644 jkube-kit/common/src/test/resources/downloadable-artifacts/pack-v0.31.0-linux.tgz create mode 100644 jkube-kit/common/src/test/resources/downloadable-artifacts/pack-v0.31.0-windows.zip create mode 100644 jkube-kit/config/image/src/main/java/org/eclipse/jkube/kit/config/image/build/BuildPackConfiguration.java create mode 100644 jkube-kit/config/image/src/test/java/org/eclipse/jkube/kit/config/image/build/BuildPackConfigurationTest.java create mode 100644 jkube-kit/config/service/src/main/java/org/eclipse/jkube/kit/config/service/kubernetes/BuildPackBuildService.java create mode 100644 jkube-kit/config/service/src/test/java/org/eclipse/jkube/kit/config/service/kubernetes/BuildPackBuildServiceTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6483991037..63fb5c4f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ _**Note**_: - Container Images generated using jkube opinionated defaults no longer contain full timestamp in `org.label-schema.build-date` label. The label contains the build date in the format `yyyy-MM-dd`. ### 1.14.0 (2023-08-31) +* Fix #439: Add new build strategy for Buildpacks * Fix #1674: SpringBootGenerator utilizes the layered jar if present and use it as Docker layers * Fix #1713: Add HelidonHealthCheckEnricher to add Kubernetes health checks for Helidon applications * Fix #1714: Add HelidonGenerator to add opinionated container image for Helidon applications diff --git a/jkube-kit/build/service/buildpacks/pom.xml b/jkube-kit/build/service/buildpacks/pom.xml new file mode 100644 index 0000000000..6c3453578f --- /dev/null +++ b/jkube-kit/build/service/buildpacks/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + + org.eclipse.jkube + jkube-kit-parent + 1.16-SNAPSHOT + ../../../parent/pom.xml + + + jkube-kit-build-service-buildpacks + + JKube Kit :: Build :: Service :: Buildpacks + + + + org.eclipse.jkube + jkube-kit-build-api + + + org.apache.commons + commons-text + + + org.projectlombok + lombok + + + org.mockito + mockito-core + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + + + + diff --git a/jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildImageCommand.java b/jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildImageCommand.java new file mode 100644 index 0000000000..662b153a01 --- /dev/null +++ b/jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildImageCommand.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.service.buildpacks; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jkube.kit.common.ExternalCommand; +import org.eclipse.jkube.kit.common.KitLogger; +import org.eclipse.jkube.kit.config.image.ImageName; +import org.eclipse.jkube.kit.config.image.build.BuildPackConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class BuildPackBuildImageCommand extends ExternalCommand { + private final String imageName; + private final KitLogger logger; + private final String cliPath; + private final BuildPackConfiguration buildpackConfiguration; + + protected BuildPackBuildImageCommand(KitLogger log, String imageName, String cliPath, BuildPackConfiguration buildpackConfiguration) { + super(log); + this.logger = log; + this.imageName = imageName; + this.cliPath = cliPath; + this.buildpackConfiguration = buildpackConfiguration; + } + + @Override + protected String[] getArgs() { + List args = new ArrayList<>(); + args.add(cliPath); + args.add("build"); + args.add(imageName); + args.addAll(extractStringArg("--builder", buildpackConfiguration.getBuilderImage())); + args.addAll(extractStringArg("--buildpack-registry", buildpackConfiguration.getRegistry())); + args.addAll(extractStringArg("--cache", buildpackConfiguration.getCache())); + args.addAll(extractStringArg("--cache-image", buildpackConfiguration.getCacheImage())); + args.addAll(extractStringArg("--creation-time", buildpackConfiguration.getCreationTime())); + args.addAll(extractStringArg("--default-process", buildpackConfiguration.getDefaultProcess())); + args.addAll(extractStringArg("--descriptor", buildpackConfiguration.getDescriptor())); + args.addAll(extractStringArg("--docker-host", buildpackConfiguration.getDockerHost())); + args.addAll(extractStringArg("--lifecycle-image", buildpackConfiguration.getLifecycleImage())); + args.addAll(extractStringArg("--network", buildpackConfiguration.getNetwork())); + args.addAll(extractStringArg("--path", buildpackConfiguration.getPath())); + args.addAll(extractStringArg("--previous-image", buildpackConfiguration.getPreviousImage())); + args.addAll(extractStringArg("--pull-policy", buildpackConfiguration.getPullPolicy())); + args.addAll(extractStringArg("--report-output-dir", buildpackConfiguration.getReportOutputDir())); + args.addAll(extractStringArg("--run-image", buildpackConfiguration.getRunImage())); + args.addAll(extractStringArg("--sbom-output-dir", buildpackConfiguration.getSbomOutputDir())); + args.addAll(extractStringArg("--workspace", buildpackConfiguration.getWorkspace())); + args.addAll(extractBooleanArg("--clear-cache", buildpackConfiguration.isClearCache())); + args.addAll(extractBooleanArg("--publish", buildpackConfiguration.isPublish())); + args.addAll(extractBooleanArg("--trust-builder", buildpackConfiguration.isTrustBuilder())); + args.addAll(extractRepeatedArgsForListElements("--env-file", buildpackConfiguration.getEnvFiles())); + args.addAll(extractRepeatedArgsForListElements("--post-buildpack", buildpackConfiguration.getPostBuildpacks())); + args.addAll(extractRepeatedArgsForListElements("--pre-buildpack", buildpackConfiguration.getPreBuildpacks())); + args.addAll(extractRepeatedArgsForListElements("--volume", buildpackConfiguration.getVolumes())); + args.addAll(extractRepeatedArgsForListElements("--buildpack", buildpackConfiguration.getBuildpacks())); + args.addAll(extractRepeatedArgsForListElements("--extension", buildpackConfiguration.getExtensions())); + if (buildpackConfiguration.getEnv() != null) { + List keyValueEntryList = buildpackConfiguration.getEnv().entrySet().stream() + .map(this::getSingleEnvArg) + .collect(Collectors.toList()); + args.addAll(extractRepeatedArgsForListElements("--env", keyValueEntryList)); + } + if (buildpackConfiguration.getGid() > 0) { + args.add("--gid"); + args.add(String.valueOf(buildpackConfiguration.getGid())); + } + if (buildpackConfiguration.getTags() != null && !buildpackConfiguration.getTags().isEmpty()) { + ImageName specifiedImageName = new ImageName(imageName); + List imageNameWithAdditionalTags = buildpackConfiguration.getTags().stream() + .map(t -> specifiedImageName.getNameWithoutTag() + ":" + t) + .collect(Collectors.toList()); + args.addAll(extractRepeatedArgsForListElements("--tag", imageNameWithAdditionalTags)); + } + + return args.toArray(new String[0]); + } + + @Override + protected void processLine(String line) { + logger.info("[[s]]%s", line); + } + + private List extractBooleanArg(String flag, boolean flagValue) { + List args = new ArrayList<>(); + if (flagValue) { + args.add(flag); + } + return args; + } + + private List extractStringArg(String flag, String flagValue) { + List args = new ArrayList<>(); + if (StringUtils.isNotBlank(flagValue)) { + args.add(flag); + args.add(flagValue); + } + return args; + } + + private List extractRepeatedArgsForListElements(String flag, List flagValues) { + List args = new ArrayList<>(); + if (flagValues != null && !flagValues.isEmpty()) { + flagValues.forEach(v -> args.addAll(extractStringArg(flag, v))); + } + return args; + } + + private String getSingleEnvArg(Map.Entry e) { + String entryKeyValue = e.getKey(); + if (StringUtils.isNotBlank(e.getValue())) { + entryKeyValue = entryKeyValue + "=" + e.getValue(); + } + return entryKeyValue; + } +} diff --git a/jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildUtil.java b/jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildUtil.java new file mode 100644 index 0000000000..ae810d1fde --- /dev/null +++ b/jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildUtil.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.service.buildpacks; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jkube.kit.common.KitLogger; +import org.eclipse.jkube.kit.common.util.FileUtil; +import org.eclipse.jkube.kit.config.image.build.BuildPackConfiguration; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; + +import static org.eclipse.jkube.kit.common.util.GitHubCliDownloaderUtil.downloadCLIFromGitHub; +import static org.eclipse.jkube.kit.common.util.GitHubCliDownloaderUtil.getApplicableBinary; +import static org.eclipse.jkube.kit.common.util.GitHubCliDownloaderUtil.getDownloadPlatform; + +public class BuildPackBuildUtil { + private static final String DEFAULT_PACK_CLI_VERSION = "v0.31.0"; + private static final String PACK_CLI_DOWNLOADS_LINK = "https://github.com/buildpacks/pack/releases/download/%s"; + private static final String PACK_CLI_ARTIFACT_PREFIX = "pack"; + private static final String PACK_UNIX_TAR_SUFFIX = ".tgz"; + private static final String PACK_WINDOWS_ZIP_SUFFIX = ".zip"; + + private BuildPackBuildUtil() { } + + public static void buildImage(KitLogger kitLogger, File baseDirectory, String outputDirectory, String imageName, BuildPackConfiguration buildPackConfiguration) { + String cliPath = getPackCLIIfPresentOrDownload(kitLogger, baseDirectory, new File(baseDirectory, outputDirectory)); + executeBuildPacksCLIBuildImageTask(kitLogger, cliPath, imageName, buildPackConfiguration); + } + + private static void executeBuildPacksCLIBuildImageTask(KitLogger kitLogger, String cliPath, String imageName, BuildPackConfiguration buildPackConfiguration) { + BuildPackBuildImageCommand buildPackBuildImageCommand = new BuildPackBuildImageCommand(kitLogger, imageName, cliPath, buildPackConfiguration); + try { + kitLogger.info("Using pack CLI %s", cliPath); + buildPackBuildImageCommand.execute(); + } catch (IOException e) { + throw new IllegalStateException("Failure in executing pack command", e); + } + } + + private static String getPackCLIIfPresentOrDownload(KitLogger kitLogger, File baseDir, File outputDirectory) { + try { + String packCliDownloadBaseUrl = String.format(PACK_CLI_DOWNLOADS_LINK, DEFAULT_PACK_CLI_VERSION); + String packCliArtifactSuffix = SystemUtils.IS_OS_WINDOWS ? PACK_WINDOWS_ZIP_SUFFIX : PACK_UNIX_TAR_SUFFIX; + String downloadedPackCliPath = getExpectedPackCliDownloadedPath(outputDirectory); + if (StringUtils.isBlank(downloadedPackCliPath)) { + kitLogger.info("Downloading pack CLI %s", DEFAULT_PACK_CLI_VERSION); + downloadedPackCliPath = downloadCLIFromGitHub(kitLogger, packCliDownloadBaseUrl, getDownloadPlatform(), PACK_CLI_ARTIFACT_PREFIX, DEFAULT_PACK_CLI_VERSION, packCliArtifactSuffix, outputDirectory); + } + return FileUtil.getRelativeFilePath(baseDir.getAbsolutePath(), downloadedPackCliPath); + } catch (IOException ioException) { + kitLogger.info("Not able to download pack CLI : " + ioException.getMessage()); + kitLogger.info("Checking for local pack CLI"); + String packCliFoundLocally = checkPackCLIPresentOnMachine(kitLogger); + if (StringUtils.isNotBlank(packCliFoundLocally)) { + return packCliFoundLocally; + } + throw new IllegalStateException("No local pack binary found, Not able to download pack CLI : " + ioException.getMessage()); + } + } + + private static String getExpectedPackCliDownloadedPath(File outputDirectory) { + if (outputDirectory != null && outputDirectory.exists()) { + File extractedPackDir = new File(outputDirectory, String.format("pack-%s-%s", DEFAULT_PACK_CLI_VERSION, getDownloadPlatform())); + if (extractedPackDir.exists()) { + File[] foundPackCliList = extractedPackDir.listFiles(f -> f.getName().startsWith("pack") && f.canExecute()); + if (foundPackCliList != null && foundPackCliList.length > 0) { + return foundPackCliList[0].getAbsolutePath(); + } + } + } + return null; + } + + private static String checkPackCLIPresentOnMachine(KitLogger kitLogger) { + String packCLIVersion = getLocalPackCLIVersion(kitLogger); + if (StringUtils.isNotBlank(packCLIVersion)) { + return getApplicableBinary(PACK_CLI_ARTIFACT_PREFIX); + } + return null; + } + + private static String getLocalPackCLIVersion(KitLogger kitLogger) { + BuildPackVersionCommand versionCommand = new BuildPackVersionCommand(kitLogger); + try { + versionCommand.execute(); + return versionCommand.getVersion(); + } catch (IOException e) { + kitLogger.info("pack CLI not found"); + } + return null; + } +} diff --git a/jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackVersionCommand.java b/jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackVersionCommand.java new file mode 100644 index 0000000000..25b7fa4c7b --- /dev/null +++ b/jkube-kit/build/service/buildpacks/src/main/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackVersionCommand.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.service.buildpacks; + +import lombok.Getter; +import org.eclipse.jkube.kit.common.ExternalCommand; +import org.eclipse.jkube.kit.common.KitLogger; + +import static org.eclipse.jkube.kit.common.util.GitHubCliDownloaderUtil.getApplicableBinary; + +@Getter +public class BuildPackVersionCommand extends ExternalCommand { + private String version; + private static final String PACK_CLI_ARTIFACT_PREFIX = "pack"; + + protected BuildPackVersionCommand(KitLogger log) { + super(log); + } + + @Override + protected String[] getArgs() { + return new String[] { getApplicableBinary(PACK_CLI_ARTIFACT_PREFIX), "--version" }; + } + + @Override + protected void processLine(String line) { + version = line; + } +} diff --git a/jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildImageCommandTest.java b/jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildImageCommandTest.java new file mode 100644 index 0000000000..f473439f42 --- /dev/null +++ b/jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildImageCommandTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.service.buildpacks; + +import org.eclipse.jkube.kit.common.KitLogger; +import org.eclipse.jkube.kit.config.image.build.BuildPackConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class BuildPackBuildImageCommandTest { + private KitLogger kitLogger; + + @BeforeEach + void setUp() { + kitLogger = spy(new KitLogger.SilentLogger()); + } + + @Test + void getArgs_whenEmptyBuildPackConfiguration_thenGenerateCompleteCommand() { + // Given + String imageName = "test/foo:latest"; + String cliPath = "target/docker/pack-v0.31.0-linux/pack"; + BuildPackConfiguration buildPackConfiguration = BuildPackConfiguration.builder() + .build(); + BuildPackBuildImageCommand buildPackBuildImageCommand = new BuildPackBuildImageCommand(kitLogger, imageName, cliPath, buildPackConfiguration); + + // When + Then + assertThat(buildPackBuildImageCommand.getArgs()) + .containsExactly(cliPath, "build", "test/foo:latest"); + } + + @Test + void getArgs_whenCompleteBuildPackConfiguration_thenGenerateCompleteCommand() { + // Given + String imageName = "test/foo:latest"; + String cliPath = "target/docker/pack-v0.31.0-linux/pack"; + BuildPackConfiguration buildPackConfiguration = BuildPackConfiguration.builder() + .publish(true) + .clearCache(true) + .trustBuilder(true) + .dockerHost("/var/run/docker.sock") + .cacheImage("foo/cache-image:latest") + .cache("type=launch;format=image;source=test/image:latest") + .path("/home/example/basedir") + .registry("example-registry.io") + .network("default") + .pullPolicy("if-not-present") + .descriptor("/home/example/basedir/target/project.toml<") + .defaultProcess("web") + .lifecycleImage("buildpacksio/lifecycle:latest") + .putEnv("BP_SPRING_CLOUD_BINDINGS_DISABLED", "true") + .putEnv("BPL_SPRING_CLOUD_BINDINGS_DISABLED", "true") + .envFile("/home/example/basedir/src/main/resources/application.properties") + .buildpack("samples/java-maven") + .extension("test-extension") + .volume("/tmp/volume:/platform/volume:ro") + .tags(Arrays.asList("t1", "t2", "t3")) + .workspace("/platform/application-overridden-dir") + .gid(1000) + .previousImage("test-previous-image") + .sbomOutputDir("/home/example/basedir/target/sbom-output-dir") + .reportOutputDir("/home/example/basedir/target/buildpack-build-report.toml") + .creationTime("now") + .builderImage("paketobuildpacks/builder:base") + .runImage("paketobuildpacks/run:base-cnb") + .preBuildpack("paketobuildpacks/java-memory-assistant:latest") + .postBuildpack("paketobuildpacks/appdynamics:latest") + .build(); + BuildPackBuildImageCommand buildPackBuildImageCommand = new BuildPackBuildImageCommand(kitLogger, imageName, cliPath, buildPackConfiguration); + + // When + Then + assertThat(buildPackBuildImageCommand.getArgs()) + .containsExactly(cliPath, "build", "test/foo:latest", + "--builder", "paketobuildpacks/builder:base", "--buildpack-registry", "example-registry.io", + "--cache", "type=launch;format=image;source=test/image:latest", "--cache-image", "foo/cache-image:latest", + "--creation-time", "now", "--default-process", "web", + "--descriptor", "/home/example/basedir/target/project.toml<", "--docker-host", "/var/run/docker.sock", + "--lifecycle-image", "buildpacksio/lifecycle:latest", "--network", "default", + "--path", "/home/example/basedir", "--previous-image", "test-previous-image", + "--pull-policy", "if-not-present", "--report-output-dir", "/home/example/basedir/target/buildpack-build-report.toml", + "--run-image", "paketobuildpacks/run:base-cnb", "--sbom-output-dir", "/home/example/basedir/target/sbom-output-dir", + "--workspace", "/platform/application-overridden-dir", "--clear-cache", "--publish", "--trust-builder", + "--env-file", "/home/example/basedir/src/main/resources/application.properties", + "--post-buildpack", "paketobuildpacks/appdynamics:latest", "--pre-buildpack", "paketobuildpacks/java-memory-assistant:latest", + "--volume", "/tmp/volume:/platform/volume:ro", "--buildpack", "samples/java-maven", + "--extension", "test-extension", + "--env", "BP_SPRING_CLOUD_BINDINGS_DISABLED=true", "--env", "BPL_SPRING_CLOUD_BINDINGS_DISABLED=true", + "--gid", "1000", + "--tag", "test/foo:t1", "--tag", "test/foo:t2", "--tag", "test/foo:t3"); + } + + @Test + void processLine_whenInvoked_shouldLogOutput() { + // Given + String imageName = "test/foo:latest"; + String cliPath = "target/docker/pack-v0.31.0-linux/pack"; + BuildPackConfiguration buildPackConfiguration = BuildPackConfiguration.builder() + .build(); + BuildPackBuildImageCommand buildPackBuildImageCommand = new BuildPackBuildImageCommand(kitLogger, imageName, cliPath, buildPackConfiguration); + + // When + buildPackBuildImageCommand.processLine("===> ANALYZING"); + buildPackBuildImageCommand.processLine("===> DETECTING"); + buildPackBuildImageCommand.processLine("===> RESTORING"); + buildPackBuildImageCommand.processLine("===> EXPORTING"); + + // Then + verify(kitLogger).info("[[s]]%s", "===> ANALYZING"); + verify(kitLogger).info("[[s]]%s", "===> DETECTING"); + verify(kitLogger).info("[[s]]%s", "===> RESTORING"); + verify(kitLogger).info("[[s]]%s", "===> EXPORTING"); + } +} diff --git a/jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildUtilTest.java b/jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildUtilTest.java new file mode 100644 index 0000000000..e70c28a4f4 --- /dev/null +++ b/jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackBuildUtilTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.service.buildpacks; + +import org.eclipse.jkube.kit.common.KitLogger; +import org.eclipse.jkube.kit.common.util.GitHubCliDownloaderUtil; +import org.eclipse.jkube.kit.config.image.build.BuildPackConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class BuildPackBuildUtilTest { + private KitLogger kitLogger; + @TempDir + private File temporaryFolder; + private File outputDir; + private String imageName; + private MockedStatic gitHubCliDownloaderUtilMockedStatic; + + @BeforeEach + void setUp() throws IOException { + kitLogger = spy(new KitLogger.SilentLogger()); + outputDir = new File(temporaryFolder, "target"); + Files.createDirectory(outputDir.toPath()); + imageName = "test/foo:latest"; + gitHubCliDownloaderUtilMockedStatic = mockStatic(GitHubCliDownloaderUtil.class); + } + + @AfterEach + void tearDown() { + gitHubCliDownloaderUtilMockedStatic.close(); + } + + @Test + void buildImage_whenPackAbsent_thenDownloadAndRunPackCommand() throws IOException { + try (MockedConstruction buildPackBuildImageCommandMockedConstruction = mockConstruction(BuildPackBuildImageCommand.class)) { + // Given + gitHubCliDownloaderUtilMockedStatic.when(() -> GitHubCliDownloaderUtil.downloadCLIFromGitHub(eq(kitLogger), anyString(), anyString(), anyString(), anyString(), anyString(), any(File.class))) + .thenReturn("target/docker/pack-v0.31.0-linux/pack"); + gitHubCliDownloaderUtilMockedStatic.when(GitHubCliDownloaderUtil::getDownloadPlatform).thenReturn("linux"); + + // When + BuildPackBuildUtil.buildImage(kitLogger, temporaryFolder, "target", imageName, createNewBuildPackConfiguration()); + + // Then + gitHubCliDownloaderUtilMockedStatic.verify(() -> GitHubCliDownloaderUtil.downloadCLIFromGitHub(kitLogger, + "https://github.com/buildpacks/pack/releases/download/v0.31.0", "linux", "pack", "v0.31.0", ".tgz", outputDir)); + assertThat(buildPackBuildImageCommandMockedConstruction.constructed()).hasSize(1); + BuildPackBuildImageCommand buildPackBuildImageCommand = buildPackBuildImageCommandMockedConstruction.constructed().get(0); + verify(buildPackBuildImageCommand).execute(); + } + } + + @Test + void buildImage_whenPackAlreadyPresent_thenDirectlyRunPackCommand() throws IOException { + try (MockedConstruction buildPackBuildImageCommandMockedConstruction = mockConstruction(BuildPackBuildImageCommand.class)) { + // Given + givenPackCliAlreadyDownloaded(); + gitHubCliDownloaderUtilMockedStatic.when(GitHubCliDownloaderUtil::getDownloadPlatform).thenReturn("linux"); + + // When + BuildPackBuildUtil.buildImage(kitLogger, temporaryFolder, "target", imageName, createNewBuildPackConfiguration()); + + // Then + gitHubCliDownloaderUtilMockedStatic.verify(() -> GitHubCliDownloaderUtil.downloadCLIFromGitHub(eq(kitLogger), + anyString(), anyString(), anyString(), anyString(), anyString(), any(File.class)), times(0)); + assertThat(buildPackBuildImageCommandMockedConstruction.constructed()).hasSize(1); + BuildPackBuildImageCommand buildPackBuildImageCommand = buildPackBuildImageCommandMockedConstruction.constructed().get(0); + verify(buildPackBuildImageCommand).execute(); + } + } + + @Test + void buildImage_whenPackCliExecutionFailed_thenThrowException() throws IOException { + try (MockedConstruction buildPackBuildImageCommandMockedConstruction = mockConstruction(BuildPackBuildImageCommand.class, (mock, ctx) -> { + doThrow(new IOException("pack cli execution error")).when(mock).execute(); + })) { + // Given + givenPackCliAlreadyDownloaded(); + gitHubCliDownloaderUtilMockedStatic.when(GitHubCliDownloaderUtil::getDownloadPlatform).thenReturn("linux"); + + // When + Then + assertThatIllegalStateException() + .isThrownBy(() -> BuildPackBuildUtil.buildImage(kitLogger, temporaryFolder, "target", imageName, createNewBuildPackConfiguration())) + .withMessage("Failure in executing pack command"); + assertThat(buildPackBuildImageCommandMockedConstruction.constructed()).hasSize(1); + BuildPackBuildImageCommand buildPackBuildImageCommand = buildPackBuildImageCommandMockedConstruction.constructed().get(0); + verify(buildPackBuildImageCommand).execute(); + } + } + + @Test + void buildImage_whenPackCliDownloadFailed_thenUseLocalPackCli() throws IOException { + try (MockedConstruction buildPackBuildImageCommandMockedConstruction = mockConstruction(BuildPackBuildImageCommand.class); + MockedConstruction buildPackVersionCommandMockedConstruction = mockConstruction(BuildPackVersionCommand.class, (mock, ctx) -> { + when(mock.getVersion()).thenReturn("v0.31.0"); + })) { + // Given + gitHubCliDownloaderUtilMockedStatic.when(GitHubCliDownloaderUtil::getDownloadPlatform).thenReturn("linux"); + gitHubCliDownloaderUtilMockedStatic.when(() -> GitHubCliDownloaderUtil.downloadCLIFromGitHub(eq(kitLogger), anyString(), anyString(), anyString(), anyString(), anyString(), any(File.class))) + .thenThrow(new IOException("Network exception")); + gitHubCliDownloaderUtilMockedStatic.when(() -> GitHubCliDownloaderUtil.getApplicableBinary("pack")).thenReturn("pack"); + + // When + BuildPackBuildUtil.buildImage(kitLogger, temporaryFolder, "target", imageName, createNewBuildPackConfiguration()); + + // Then + assertThat(buildPackBuildImageCommandMockedConstruction.constructed()).hasSize(1); + assertThat(buildPackVersionCommandMockedConstruction.constructed()).hasSize(1); + BuildPackBuildImageCommand buildPackBuildImageCommand = buildPackBuildImageCommandMockedConstruction.constructed().get(0); + verify(buildPackBuildImageCommand).execute(); + } + } + + @Test + void buildImage_whenPackCliDownloadFailedAndNoLocalPackCli_thenThrowException() { + try (MockedConstruction buildPackVersionCommandMockedConstruction = mockConstruction(BuildPackVersionCommand.class, (mock, ctx) -> { + doThrow(new IOException("Failed to run pack")).when(mock).execute(); + })) { + // Given + gitHubCliDownloaderUtilMockedStatic.when(GitHubCliDownloaderUtil::getDownloadPlatform).thenReturn("linux"); + gitHubCliDownloaderUtilMockedStatic.when(() -> GitHubCliDownloaderUtil.downloadCLIFromGitHub(eq(kitLogger), anyString(), anyString(), anyString(), anyString(), anyString(), any(File.class))) + .thenThrow(new IOException("Network exception")); + gitHubCliDownloaderUtilMockedStatic.when(() -> GitHubCliDownloaderUtil.getApplicableBinary("pack")).thenReturn("pack"); + + // When + Then + assertThatIllegalStateException() + .isThrownBy(() -> BuildPackBuildUtil.buildImage(kitLogger, temporaryFolder, "target", imageName, createNewBuildPackConfiguration())) + .withMessage("No local pack binary found, Not able to download pack CLI : Network exception"); + assertThat(buildPackVersionCommandMockedConstruction.constructed()).hasSize(1); + } + } + + private BuildPackConfiguration createNewBuildPackConfiguration() { + return BuildPackConfiguration.builder() + .builderImage("foo/builder:base") + .build(); + } + + private void givenPackCliAlreadyDownloaded() throws IOException { + File extractedPackDir = new File(outputDir, "pack-v0.31.0-linux"); + Files.createDirectory(extractedPackDir.toPath()); + File packCli = new File(extractedPackDir, "pack"); + Files.createFile(packCli.toPath()); + assertThat(packCli.setExecutable(true)).isTrue(); + } +} diff --git a/jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackVersionCommandTest.java b/jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackVersionCommandTest.java new file mode 100644 index 0000000000..98acd44d35 --- /dev/null +++ b/jkube-kit/build/service/buildpacks/src/test/java/org/eclipse/jkube/kit/service/buildpacks/BuildPackVersionCommandTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.service.buildpacks; + +import org.eclipse.jkube.kit.common.KitLogger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class BuildPackVersionCommandTest { + private KitLogger kitLogger; + + @BeforeEach + void setUp() { + kitLogger = new KitLogger.SilentLogger(); + } + + @Test + void getArgs() { + // Given + BuildPackVersionCommand buildPackVersionCommand = new BuildPackVersionCommand(kitLogger); + + // When + Then + assertThat(buildPackVersionCommand.getArgs()) + .containsExactly("pack", "--version"); + } + + @Test + void processLine_whenInvoked_shouldSetVersion() { + // Given + BuildPackVersionCommand buildPackVersionCommand = new BuildPackVersionCommand(kitLogger); + // When + buildPackVersionCommand.processLine("0.30.0"); + // Then + assertThat(buildPackVersionCommand.getVersion()).isEqualTo("0.30.0"); + } +} diff --git a/jkube-kit/build/service/buildpacks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/jkube-kit/build/service/buildpacks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/jkube-kit/build/service/buildpacks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/util/GitHubCliDownloaderUtil.java b/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/util/GitHubCliDownloaderUtil.java new file mode 100644 index 0000000000..1d97b8f3b6 --- /dev/null +++ b/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/util/GitHubCliDownloaderUtil.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.common.util; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; +import org.eclipse.jkube.kit.common.KitLogger; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class GitHubCliDownloaderUtil { + private GitHubCliDownloaderUtil() { } + + public static String downloadCLIFromGitHub(KitLogger kitLogger, String baseDownloadUrl, String platform, String artifactNamePrefix, String version, String archiveSuffix, File outputDirectory) throws IOException { + String artifactName = getApplicableDownloadArtifact(artifactNamePrefix, platform, version, archiveSuffix); + URL downloadUrl; + downloadUrl = new URL(String.format("%s/%s", baseDownloadUrl, artifactName)); + File downloadedArchive = new File(outputDirectory, artifactName); + IoUtil.download(kitLogger, downloadUrl, downloadedArchive); + return extractDownloadedArchive(downloadedArchive, outputDirectory.toPath().resolve(artifactName.substring(0, artifactName.length() - 4))); + } + + public static String getDownloadPlatform() { + if (SystemUtils.IS_OS_LINUX) { + return "linux"; + } else if (SystemUtils.IS_OS_MAC) { + return "macos"; + } + return "windows"; + } + + public static String getApplicableBinary(String binaryName) { + return SystemUtils.IS_OS_WINDOWS ? binaryName + ".exe" : binaryName; + } + + private static String extractDownloadedArchive(File downloadedArchive, Path targetExtractionDir) throws IOException { + if (targetExtractionDir.toFile().exists()) { + FileUtil.cleanDirectory(targetExtractionDir.toFile()); + } + Files.createDirectory(targetExtractionDir); + if (downloadedArchive.getName().endsWith(".tgz")) { + try (BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(downloadedArchive.toPath())); + TarArchiveInputStream tar = new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))) { + ArchiveEntry entry; + while ((entry = tar.getNextEntry()) != null) { + Path extractTo = targetExtractionDir.resolve(entry.getName()); + if (entry.isDirectory()) { + Files.createDirectories(extractTo); + } else { + Files.copy(tar, extractTo); + } + } + } + } else if (downloadedArchive.getName().endsWith(".zip")) { + byte[] buffer = new byte[1024]; + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(downloadedArchive.toPath()))) { + ZipEntry zipEntry = zis.getNextEntry(); + while (zipEntry != null) { + File newFile = new File(targetExtractionDir.toFile(), zipEntry.getName()); + if (zipEntry.isDirectory()) { + if (!newFile.isDirectory() && !newFile.mkdirs()) { + throw new IOException("Failed to create directory " + newFile); + } + } else { + // fix for Windows-created archives + File parent = newFile.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) { + throw new IOException("Failed to create directory " + parent); + } + + // write file content + try (FileOutputStream fos = new FileOutputStream(newFile)) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + } + zipEntry = zis.getNextEntry(); + } + zis.closeEntry(); + } + } + String packCliPath = null; + File[] packCliArtifactList = targetExtractionDir.toFile().listFiles(f -> f.getName().startsWith("pack")); + if (packCliArtifactList != null && packCliArtifactList.length >= 1) { + boolean wasSetExecutable = packCliArtifactList[0].setExecutable(true); + if (!wasSetExecutable) { + throw new IllegalStateException("Unable to add executable permissions to downloaded pack CLI"); + } + packCliPath = packCliArtifactList[0].getAbsolutePath(); + } + return packCliPath; + } + + private static String getApplicableDownloadArtifact(String artifactNamePrefix, String platform, String version, String suffix) { + StringBuilder artifactNameBuilder = new StringBuilder(); + artifactNameBuilder.append(artifactNamePrefix).append("-").append(version); + artifactNameBuilder.append("-").append(platform); + String architecture = System.getenv("PROCESSOR_ARCHITECTURE"); + if (StringUtils.isNotBlank(architecture) && architecture.contains("arm")) { + artifactNameBuilder.append("-arm64"); + } + artifactNameBuilder.append(suffix); + return artifactNameBuilder.toString(); + } +} diff --git a/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/util/GitHubCliDownloaderUtilTest.java b/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/util/GitHubCliDownloaderUtilTest.java new file mode 100644 index 0000000000..12cc549847 --- /dev/null +++ b/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/util/GitHubCliDownloaderUtilTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.common.util; + +import org.eclipse.jkube.kit.common.KitLogger; +import org.eclipse.jkube.kit.common.TestHttpStaticServer; +import org.eclipse.jkube.kit.common.assertj.FileAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class GitHubCliDownloaderUtilTest { + private KitLogger kitLogger; + @TempDir + private File temporaryFolder; + + @BeforeEach + void setup() { + kitLogger = new KitLogger.SilentLogger(); + } + + @Test + @EnabledOnOs(OS.LINUX) + void getDownloadPlatform_whenLinuxOs_thenReturnLinuxPlatform() { + assertThat(GitHubCliDownloaderUtil.getDownloadPlatform()).isEqualTo("linux"); + } + + @Test + void downloadCLIFromGitHub_whenUnixArtifactProvided_thenDownloadAndExtract() throws IOException { + File remoteDirectory = new File(getClass().getResource("/downloadable-artifacts").getFile()); + try (TestHttpStaticServer http = new TestHttpStaticServer(remoteDirectory)) { + // Given + String baseUrl = String.format("http://localhost:%d", http.getPort()); + + // When + GitHubCliDownloaderUtil.downloadCLIFromGitHub(kitLogger, baseUrl, "linux", "pack", "v0.31.0", ".tgz", temporaryFolder); + + // Then + FileAssertions.assertThat(temporaryFolder) + .exists() + .fileTree() + .containsExactlyInAnyOrder("pack-v0.31.0-linux", "pack-v0.31.0-linux/pack", "pack-v0.31.0-linux.tgz"); + } + } + + @Test + void downloadCLIFromGitHub_whenZipArtifactProvided_thenDownloadAndExtract() throws IOException { + File remoteDirectory = new File(getClass().getResource("/downloadable-artifacts").getFile()); + try (TestHttpStaticServer http = new TestHttpStaticServer(remoteDirectory)) { + // Given + String baseUrl = String.format("http://localhost:%d", http.getPort()); + + // When + GitHubCliDownloaderUtil.downloadCLIFromGitHub(kitLogger, baseUrl, "windows", "pack", "v0.31.0", ".zip", temporaryFolder); + + // Then + FileAssertions.assertThat(temporaryFolder) + .exists() + .fileTree() + .containsExactlyInAnyOrder("pack-v0.31.0-windows", "pack-v0.31.0-windows/pack.exe", "pack-v0.31.0-windows.zip"); + } + } +} diff --git a/jkube-kit/common/src/test/resources/downloadable-artifacts/pack-v0.31.0-linux.tgz b/jkube-kit/common/src/test/resources/downloadable-artifacts/pack-v0.31.0-linux.tgz new file mode 100644 index 0000000000000000000000000000000000000000..79ab02b7851304643ce2a30816e9cc1d0e9a6c7c GIT binary patch literal 112 zcmb2|=3oE==C|iI@*Xk}VF~D;B>0E7@A*azWtWr_|0XbXvIVxj+WV|)_oHidk(UmB z@?KqfKEt&BS#(#iuHf1yM$2Uk=X=fibn$rE?Y|d~n=CDi-j*#NU%tNzVla^Wu~_~3 LrE?|>8Vn2osOvC= literal 0 HcmV?d00001 diff --git a/jkube-kit/common/src/test/resources/downloadable-artifacts/pack-v0.31.0-windows.zip b/jkube-kit/common/src/test/resources/downloadable-artifacts/pack-v0.31.0-windows.zip new file mode 100644 index 0000000000000000000000000000000000000000..06cc3ed7c5363d079452f3578058ac6c92be97a6 GIT binary patch literal 166 zcmWIWW@h1H0D;%Dlf%IbD8a!X!%&czoUNByks2Dp$-r#**CiE%ODnh;7+GF0GcbUO o0B=SnIc8jDNWhI;(g + * This field is applicable only for Buildpacks build strategy + */ + private BuildPackConfiguration buildpack; + /** * Array of images used for build cache resolution. *

@@ -415,6 +425,13 @@ public File getAbsoluteDockerTarPath(String sourceDirectory, String projectBaseD return EnvUtil.prepareAbsoluteSourceDirPath(sourceDirectory, projectBaseDir, getDockerArchive().getPath()); } + public BuildPackConfiguration getBuildpack() { + if (buildpack == null) { + buildpack = createOpinionatedBuildPackConfiguration(); + } + return mergeBuildPackConfigurationWithImageConfiguration(buildpack, imagePullPolicy, tags, env, volumes); + } + public String initAndValidate() { if (entryPoint != null) { entryPoint.validate(); diff --git a/jkube-kit/config/image/src/main/java/org/eclipse/jkube/kit/config/image/build/BuildPackConfiguration.java b/jkube-kit/config/image/src/main/java/org/eclipse/jkube/kit/config/image/build/BuildPackConfiguration.java new file mode 100644 index 0000000000..05f7a02f25 --- /dev/null +++ b/jkube-kit/config/image/src/main/java/org/eclipse/jkube/kit/config/image/build/BuildPackConfiguration.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.config.image.build; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Singular; +import org.apache.commons.lang3.StringUtils; + +import java.io.Serializable; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +@Builder(toBuilder = true) +@AllArgsConstructor +@NoArgsConstructor +@Getter +@EqualsAndHashCode(doNotUseGetters = true) +public class BuildPackConfiguration implements Serializable { + private boolean publish; + private boolean clearCache; + private boolean trustBuilder; + private String dockerHost; + private String cacheImage; + private String cache; + private String path; + private String registry; + private String network; + private String pullPolicy; + private String descriptor; + private String defaultProcess; + private String lifecycleImage; + @Singular("putEnv") + private Map env; + @Singular + private List envFiles; + @Singular + private List buildpacks; + @Singular + private List extensions; + @Singular + private List volumes; + @Singular + private List tags; + private String workspace; + private int gid; + private String previousImage; + private String sbomOutputDir; + private String reportOutputDir; + private String creationTime; + @Singular + private List preBuildpacks; + @Singular + private List postBuildpacks; + private String builderImage; + private String runImage; + + + public static BuildPackConfiguration mergeBuildPackConfigurationWithImageConfiguration(BuildPackConfiguration providedBuildPackConfig, String imagePullPolicy, List tags, Map env, List volumes) { + BuildPackConfiguration.BuildPackConfigurationBuilder buildpackConfigurationBuilder = providedBuildPackConfig.toBuilder(); + if (StringUtils.isBlank(providedBuildPackConfig.getPullPolicy()) && StringUtils.isNotBlank(imagePullPolicy)) { + if (imagePullPolicy.equalsIgnoreCase("IfNotPresent")) { + buildpackConfigurationBuilder.pullPolicy("if-not-present"); + } + buildpackConfigurationBuilder.pullPolicy(imagePullPolicy.toLowerCase(Locale.ROOT)); + } + if (tags != null) { + if (providedBuildPackConfig.getTags() == null) { + buildpackConfigurationBuilder.tags(tags); + } else { + tags.forEach(buildpackConfigurationBuilder::tag); + } + } + if (env != null) { + if (providedBuildPackConfig.getEnv() == null) { + buildpackConfigurationBuilder.env(env); + } else { + env.forEach(buildpackConfigurationBuilder::putEnv); + } + } + if (volumes != null) { + if (providedBuildPackConfig.getVolumes() == null) { + buildpackConfigurationBuilder.volumes(volumes); + } else { + volumes.forEach(buildpackConfigurationBuilder::volume); + } + } + if (StringUtils.isBlank(providedBuildPackConfig.getCreationTime())) { + buildpackConfigurationBuilder.creationTime("now"); + } + return buildpackConfigurationBuilder.build(); + } + + public static BuildPackConfiguration createOpinionatedBuildPackConfiguration() { + return BuildPackConfiguration.builder() + .builderImage("paketobuildpacks/builder:base") + .runImage("paketobuildpacks/run:base-cnb") + .creationTime("now") + .build(); + } +} diff --git a/jkube-kit/config/image/src/main/java/org/eclipse/jkube/kit/config/image/build/JKubeBuildStrategy.java b/jkube-kit/config/image/src/main/java/org/eclipse/jkube/kit/config/image/build/JKubeBuildStrategy.java index 9bacd48741..d4e3ea8e48 100644 --- a/jkube-kit/config/image/src/main/java/org/eclipse/jkube/kit/config/image/build/JKubeBuildStrategy.java +++ b/jkube-kit/config/image/src/main/java/org/eclipse/jkube/kit/config/image/build/JKubeBuildStrategy.java @@ -35,7 +35,8 @@ public enum JKubeBuildStrategy { /** * Docker build with a binary source */ - docker("Docker"); + docker("Docker"), + buildpacks("Buildpacks"); // Source strategy elements public enum SourceStrategy { diff --git a/jkube-kit/config/image/src/test/java/org/eclipse/jkube/kit/config/image/build/BuildPackConfigurationTest.java b/jkube-kit/config/image/src/test/java/org/eclipse/jkube/kit/config/image/build/BuildPackConfigurationTest.java new file mode 100644 index 0000000000..196cad70a7 --- /dev/null +++ b/jkube-kit/config/image/src/test/java/org/eclipse/jkube/kit/config/image/build/BuildPackConfigurationTest.java @@ -0,0 +1,42 @@ +package org.eclipse.jkube.kit.config.image.build; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class BuildPackConfigurationTest { + @Test + void mergeBuildPackConfigurationWithImageConfiguration_shouldMergeImageConfigurationFieldsWithBuildPackConfiguration() { + // Given + List tags = Arrays.asList("t1", "t2"); + BuildPackConfiguration buildPackConfiguration = BuildPackConfiguration.builder() + .build(); + Map env = new HashMap<>(); + env.put("FOO", "BAR"); + List volumes = Collections.singletonList("/tmp/volume:/platform/volume:ro"); + + // When + BuildPackConfiguration packConfiguration = BuildPackConfiguration.mergeBuildPackConfigurationWithImageConfiguration(buildPackConfiguration, + "IfNotPresent", tags, env, volumes); + + // Then + assertThat(packConfiguration) + .hasFieldOrPropertyWithValue("tags", Arrays.asList("t1", "t2")) + .hasFieldOrPropertyWithValue("env", Collections.singletonMap("FOO", "BAR")) + .hasFieldOrPropertyWithValue("volumes", Collections.singletonList("/tmp/volume:/platform/volume:ro")); + } + + @Test + void createOpinionatedBuildPackConfiguration() { + assertThat(BuildPackConfiguration.createOpinionatedBuildPackConfiguration()) + .hasFieldOrPropertyWithValue("builderImage", "paketobuildpacks/builder:base") + .hasFieldOrPropertyWithValue("runImage", "paketobuildpacks/run:base-cnb") + .hasFieldOrPropertyWithValue("creationTime", "now"); + } +} diff --git a/jkube-kit/config/service/pom.xml b/jkube-kit/config/service/pom.xml index 065893f25e..bcb3d350e5 100644 --- a/jkube-kit/config/service/pom.xml +++ b/jkube-kit/config/service/pom.xml @@ -41,6 +41,10 @@ org.eclipse.jkube jkube-kit-build-service-jib + + org.eclipse.jkube + jkube-kit-build-service-buildpacks + org.eclipse.jkube jkube-kit-resource-helm diff --git a/jkube-kit/config/service/src/main/java/org/eclipse/jkube/kit/config/service/kubernetes/BuildPackBuildService.java b/jkube-kit/config/service/src/main/java/org/eclipse/jkube/kit/config/service/kubernetes/BuildPackBuildService.java new file mode 100644 index 0000000000..bc4a6ae637 --- /dev/null +++ b/jkube-kit/config/service/src/main/java/org/eclipse/jkube/kit/config/service/kubernetes/BuildPackBuildService.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.config.service.kubernetes; + +import java.io.IOException; +import java.util.Objects; + +import org.eclipse.jkube.kit.build.service.docker.DockerServiceHub; +import org.eclipse.jkube.kit.common.JKubeConfiguration; +import org.eclipse.jkube.kit.common.KitLogger; +import org.eclipse.jkube.kit.common.RegistryConfig; +import org.eclipse.jkube.kit.config.image.ImageConfiguration; +import org.eclipse.jkube.kit.config.image.build.JKubeBuildStrategy; +import org.eclipse.jkube.kit.config.service.AbstractImageBuildService; +import org.eclipse.jkube.kit.config.service.BuildServiceConfig; +import org.eclipse.jkube.kit.config.service.JKubeServiceException; +import org.eclipse.jkube.kit.config.service.JKubeServiceHub; + +import static org.eclipse.jkube.kit.service.buildpacks.BuildPackBuildUtil.buildImage; + +public class BuildPackBuildService extends AbstractImageBuildService { + + private final JKubeConfiguration jKubeConfiguration; + private final DockerServiceHub dockerServices; + private final BuildServiceConfig buildServiceConfig; + private final KitLogger kitLogger; + + public BuildPackBuildService(JKubeServiceHub jKubeServiceHub) { + super(jKubeServiceHub); + this.jKubeConfiguration = Objects.requireNonNull(jKubeServiceHub.getConfiguration(), + "JKubeConfiguration is required"); + this.dockerServices = Objects.requireNonNull(jKubeServiceHub.getDockerServiceHub(), + "Docker Service Hub is required"); + this.buildServiceConfig = Objects.requireNonNull(jKubeServiceHub.getBuildServiceConfig(), + "BuildServiceConfig is required"); + this.kitLogger = Objects.requireNonNull(jKubeServiceHub.getLog()); + } + + @Override + protected void buildSingleImage(ImageConfiguration imageConfiguration) { + kitLogger.info("Delegating container image building process to Buildpacks"); + if (imageConfiguration.getBuild() != null) { + buildImage(kitLogger, jKubeConfiguration.getBasedir(), jKubeConfiguration.getOutputDirectory(), imageConfiguration.getName(), imageConfiguration.getBuildConfiguration().getBuildpack()); + } else { + kitLogger.info("No BuildPack Configuration found"); + } + } + + @Override + protected void pushSingleImage(ImageConfiguration imageConfiguration, int retries, RegistryConfig registryConfig, boolean skipTag) throws JKubeServiceException { + try { + dockerServices.getRegistryService().pushImage(imageConfiguration, retries, registryConfig, skipTag); + } catch (IOException ex) { + throw new JKubeServiceException("Error while trying to push the image: " + ex.getMessage(), ex); + } + } + + @Override + public boolean isApplicable() { + return buildServiceConfig.getJKubeBuildStrategy() != null && + buildServiceConfig.getJKubeBuildStrategy().equals(JKubeBuildStrategy.buildpacks); + } + + @Override + public void postProcess() { + // NOOP + } +} diff --git a/jkube-kit/config/service/src/main/resources/META-INF/jkube/build-service b/jkube-kit/config/service/src/main/resources/META-INF/jkube/build-service index 2922021448..cb5f809098 100644 --- a/jkube-kit/config/service/src/main/resources/META-INF/jkube/build-service +++ b/jkube-kit/config/service/src/main/resources/META-INF/jkube/build-service @@ -1,3 +1,4 @@ org.eclipse.jkube.kit.config.service.kubernetes.JibBuildService,100 +org.eclipse.jkube.kit.config.service.kubernetes.BuildPackBuildService,110 org.eclipse.jkube.kit.config.service.openshift.OpenshiftBuildService,200 org.eclipse.jkube.kit.config.service.kubernetes.DockerBuildService,9999 \ No newline at end of file diff --git a/jkube-kit/config/service/src/test/java/org/eclipse/jkube/kit/config/service/JKubeServiceHubBuildServiceTest.java b/jkube-kit/config/service/src/test/java/org/eclipse/jkube/kit/config/service/JKubeServiceHubBuildServiceTest.java index 0472df2b47..9df6805012 100644 --- a/jkube-kit/config/service/src/test/java/org/eclipse/jkube/kit/config/service/JKubeServiceHubBuildServiceTest.java +++ b/jkube-kit/config/service/src/test/java/org/eclipse/jkube/kit/config/service/JKubeServiceHubBuildServiceTest.java @@ -21,6 +21,7 @@ import org.eclipse.jkube.kit.common.util.LazyBuilder; import org.eclipse.jkube.kit.config.image.build.JKubeBuildStrategy; import org.eclipse.jkube.kit.config.resource.RuntimeMode; +import org.eclipse.jkube.kit.config.service.kubernetes.BuildPackBuildService; import org.eclipse.jkube.kit.config.service.kubernetes.DockerBuildService; import org.eclipse.jkube.kit.config.service.kubernetes.JibBuildService; import org.eclipse.jkube.kit.config.service.openshift.OpenshiftBuildService; @@ -42,6 +43,7 @@ static Stream data() { arguments(RuntimeMode.KUBERNETES, JKubeBuildStrategy.docker, DockerBuildService.class), arguments(RuntimeMode.KUBERNETES, JKubeBuildStrategy.s2i, DockerBuildService.class), arguments(RuntimeMode.KUBERNETES, JKubeBuildStrategy.jib, JibBuildService.class), + arguments(RuntimeMode.KUBERNETES, JKubeBuildStrategy.buildpacks, BuildPackBuildService.class), arguments(RuntimeMode.OPENSHIFT, null, OpenshiftBuildService.class), arguments(RuntimeMode.OPENSHIFT, JKubeBuildStrategy.docker, OpenshiftBuildService.class), arguments(RuntimeMode.OPENSHIFT, JKubeBuildStrategy.s2i, OpenshiftBuildService.class), diff --git a/jkube-kit/config/service/src/test/java/org/eclipse/jkube/kit/config/service/kubernetes/BuildPackBuildServiceTest.java b/jkube-kit/config/service/src/test/java/org/eclipse/jkube/kit/config/service/kubernetes/BuildPackBuildServiceTest.java new file mode 100644 index 0000000000..0034fbffa9 --- /dev/null +++ b/jkube-kit/config/service/src/test/java/org/eclipse/jkube/kit/config/service/kubernetes/BuildPackBuildServiceTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.config.service.kubernetes; + +import org.eclipse.jkube.kit.build.service.docker.DockerServiceHub; +import org.eclipse.jkube.kit.build.service.docker.RegistryService; +import org.eclipse.jkube.kit.common.JKubeConfiguration; +import org.eclipse.jkube.kit.common.JavaProject; +import org.eclipse.jkube.kit.common.KitLogger; +import org.eclipse.jkube.kit.common.RegistryConfig; +import org.eclipse.jkube.kit.config.image.ImageConfiguration; +import org.eclipse.jkube.kit.config.image.build.BuildConfiguration; +import org.eclipse.jkube.kit.config.image.build.BuildPackConfiguration; +import org.eclipse.jkube.kit.config.image.build.JKubeBuildStrategy; +import org.eclipse.jkube.kit.config.service.JKubeServiceException; +import org.eclipse.jkube.kit.config.service.JKubeServiceHub; +import org.eclipse.jkube.kit.service.buildpacks.BuildPackBuildUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class BuildPackBuildServiceTest { + @TempDir + private File temporaryFolder; + + private KitLogger logger; + + private JKubeServiceHub mockedServiceHub; + + private ImageConfiguration imageConfiguration; + private BuildPackConfiguration buildPackConfiguration; + private RegistryService mockedRegistryService; + + private RegistryConfig registryConfig; + + private MockedStatic buildPackBuildUtilMockedStatic; + + @BeforeEach + void setUp() { + logger = spy(new KitLogger.SilentLogger()); + mockedServiceHub = mock(JKubeServiceHub.class, RETURNS_DEEP_STUBS); + DockerServiceHub mockedDockerServiceHub = mock(DockerServiceHub.class); + mockedRegistryService = mock(RegistryService.class); + JKubeConfiguration mockedJKubeConfiguration = JKubeConfiguration.builder() + .project(JavaProject.builder() + .baseDirectory(temporaryFolder) + .build()) + .outputDirectory("target/docker") + .build(); + when(mockedServiceHub.getLog()).thenReturn(logger); + when(mockedServiceHub.getConfiguration()).thenReturn(mockedJKubeConfiguration); + when(mockedServiceHub.getDockerServiceHub()).thenReturn(mockedDockerServiceHub); + when(mockedDockerServiceHub.getRegistryService()).thenReturn(mockedRegistryService); + buildPackBuildUtilMockedStatic = mockStatic(BuildPackBuildUtil.class); + buildPackConfiguration = BuildPackConfiguration.builder() + .builderImage("foo/builder:base") + .runImage("foo/run:base") + .creationTime("now") + .build(); + imageConfiguration = ImageConfiguration.builder() + .name("test/testimage:0.0.1") + .build(BuildConfiguration.builder() + .buildpack(buildPackConfiguration) + .build()) + .build(); + registryConfig = RegistryConfig.builder() + .authConfig(Collections.emptyMap()) + .settings(Collections.emptyList()) + .build(); + } + + @AfterEach + void close() { + buildPackBuildUtilMockedStatic.close(); + } + + @Test + void isApplicable_withNoBuildStrategy_shouldReturnFalse() { + // When + final boolean result = new BuildPackBuildService(mockedServiceHub).isApplicable(); + // Then + assertThat(result).isFalse(); + } + + @ParameterizedTest + @CsvSource({ + "s2i,false", "jib,false", "docker,false", "buildpacks,true" + }) + void isApplicable_withGivenStrategy_shouldReturnTrueOnlyForBuildPackStrategy(String buildStrategyValue, boolean expectedResult) { + // Given + when(mockedServiceHub.getBuildServiceConfig().getJKubeBuildStrategy()).thenReturn(JKubeBuildStrategy.valueOf(buildStrategyValue)); + // When + final boolean result = new BuildPackBuildService(mockedServiceHub).isApplicable(); + // Then + assertThat(result).isEqualTo(expectedResult); + } + + @Test + void buildSingleImage_whenBuildPackConfigurationProvided_thenBuildImage() { + // Given + BuildPackBuildService buildPackBuildService = new BuildPackBuildService(mockedServiceHub); + + // When + buildPackBuildService.buildSingleImage(imageConfiguration); + + // Then + buildPackBuildUtilMockedStatic.verify(() -> BuildPackBuildUtil.buildImage(logger, temporaryFolder, "target/docker", "test/testimage:0.0.1", buildPackConfiguration)); + } + + @Test + void buildSingleImage_whenNoBuildConfiguration_thenNoBuild() { + // Given + imageConfiguration = imageConfiguration.toBuilder() + .build(null) + .build(); + BuildPackBuildService buildPackBuildService = new BuildPackBuildService(mockedServiceHub); + + // When + buildPackBuildService.buildSingleImage(imageConfiguration); + + // Then + buildPackBuildUtilMockedStatic.verify(() -> BuildPackBuildUtil.buildImage(eq(logger), eq(temporaryFolder), anyString(), anyString(), eq(buildPackConfiguration)), times(0)); + } + + @Test + void pushSingleImage_whenImageConfigurationProvided_thenPushUsingRegistryService() throws JKubeServiceException, IOException { + // Given + BuildPackBuildService buildPackBuildService = new BuildPackBuildService(mockedServiceHub); + + // When + buildPackBuildService.pushSingleImage(imageConfiguration, 0, registryConfig, false); + + // Then + verify(mockedRegistryService, times(1)).pushImage(imageConfiguration, 0, registryConfig, false); + } + + @Test + void pushSingleImage_whenPushFailed_thenThrowException() throws IOException { + // Given + BuildPackBuildService buildPackBuildService = new BuildPackBuildService(mockedServiceHub); + doThrow(new IOException("Failure in pushing image")).when(mockedRegistryService).pushImage(imageConfiguration, 0, registryConfig, false); + + // When + Then + assertThatExceptionOfType(JKubeServiceException.class) + .isThrownBy(() -> buildPackBuildService.pushSingleImage(imageConfiguration, 0, registryConfig, false)) + .withMessage("Error while trying to push the image: Failure in pushing image"); + } +} diff --git a/jkube-kit/parent/pom.xml b/jkube-kit/parent/pom.xml index d7273383c2..b1bdefa23d 100644 --- a/jkube-kit/parent/pom.xml +++ b/jkube-kit/parent/pom.xml @@ -31,7 +31,7 @@ ${project.name} - + UTF-8 1.76 @@ -207,6 +207,12 @@ ${project.version} + + org.eclipse.jkube + jkube-kit-build-service-buildpacks + ${project.version} + + org.eclipse.jkube jkube-kit-watcher-api diff --git a/jkube-kit/pom.xml b/jkube-kit/pom.xml index 8d2c9415f9..3213062995 100644 --- a/jkube-kit/pom.xml +++ b/jkube-kit/pom.xml @@ -42,6 +42,7 @@ build/api build/service/docker build/service/jib + build/service/buildpacks generator/api generator/java-exec generator/karaf