From 0ce5eac1a90c89e67fbfc0831d3eb32ba6e5f651 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Sat, 12 Oct 2019 17:00:03 +0530 Subject: [PATCH] Support for JIB(Java Image Builder) mode in dmp This ports Jib support added in FMP in https://github.com/fabric8io/fabric8-maven-plugin/pull/1675 with better logging. --- doc/changelog.md | 1 + pom.xml | 15 +- .../io/fabric8/maven/docker/BuildMojo.java | 17 +- .../maven/docker/service/JibBuildService.java | 185 +++++++++++++ .../maven/docker/util/FatJarDetector.java | 84 ++++++ .../docker/util/JibBuildServiceUtil.java | 254 ++++++++++++++++++ 6 files changed, 551 insertions(+), 5 deletions(-) create mode 100644 src/main/java/io/fabric8/maven/docker/service/JibBuildService.java create mode 100644 src/main/java/io/fabric8/maven/docker/util/FatJarDetector.java create mode 100644 src/main/java/io/fabric8/maven/docker/util/JibBuildServiceUtil.java diff --git a/doc/changelog.md b/doc/changelog.md index bb06d1e05..57b990d95 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -6,6 +6,7 @@ - Allow merging of image configurations using ([#360](https://github.com/fabric8io/docker-maven-plugin/issues/360)) - Treat bridged and default network mode the same #1234 - Fix NPE when cacheFrom is missing from config #1274 + - Support for JIB mode * **0.31.0** (2019-08-10) - Fix test cases on Windows ([#1220](https://github.com/fabric8io/docker-maven-plugin/issues/1220)) diff --git a/pom.xml b/pom.xml index 32604a360..afb6f4b99 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@ 3.3.9 1.43 3.0.0-M2 + 0.10.0 @@ -172,7 +173,7 @@ com.google.guava guava - 23.0-android + 27.0.1-jre @@ -181,6 +182,18 @@ 1.24 + + com.google.cloud.tools + jib-core + ${jib.version} + + + + com.google.cloud.tools + jib-maven-plugin + 1.6.1 + + diff --git a/src/main/java/io/fabric8/maven/docker/BuildMojo.java b/src/main/java/io/fabric8/maven/docker/BuildMojo.java index e713fd80f..f20cce57c 100644 --- a/src/main/java/io/fabric8/maven/docker/BuildMojo.java +++ b/src/main/java/io/fabric8/maven/docker/BuildMojo.java @@ -15,6 +15,7 @@ import io.fabric8.maven.docker.config.ImageConfiguration; import io.fabric8.maven.docker.service.BuildService; import io.fabric8.maven.docker.service.ImagePullManager; +import io.fabric8.maven.docker.service.JibBuildService; import io.fabric8.maven.docker.service.ServiceHub; import io.fabric8.maven.docker.util.Logger; import io.fabric8.maven.docker.util.EnvUtil; @@ -41,6 +42,9 @@ public class BuildMojo extends AbstractBuildSupportMojo { @Parameter(property = "docker.name", defaultValue = "") protected String name; + @Parameter(property = "docker.build.jib", defaultValue = "false") + protected boolean jib; + /** * Skip building tags */ @@ -69,11 +73,16 @@ protected void buildAndTag(ServiceHub hub, ImageConfiguration imageConfig) BuildService.BuildContext buildContext = getBuildContext(); ImagePullManager pullManager = getImagePullManager(determinePullPolicy(imageConfig.getBuildConfiguration()), autoPull); - BuildService buildService = hub.getBuildService(); - buildService.buildImage(imageConfig, pullManager, buildContext); - if (!skipTag) { - buildService.tagImage(imageConfig.getName(), imageConfig); + if (Boolean.TRUE.equals(jib)) { + log.info("Building Container image with [[B]]JIB(Java Image Builder)[[B]] mode"); + new JibBuildService(buildContext, createMojoParameters(), log).buildImage(imageConfig, pullManager, buildContext); + } else { + BuildService buildService = hub.getBuildService(); + buildService.buildImage(imageConfig, pullManager, buildContext); + if (!skipTag) { + buildService.tagImage(imageConfig.getName(), imageConfig); + } } } diff --git a/src/main/java/io/fabric8/maven/docker/service/JibBuildService.java b/src/main/java/io/fabric8/maven/docker/service/JibBuildService.java new file mode 100644 index 000000000..a3bc7173e --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/service/JibBuildService.java @@ -0,0 +1,185 @@ +package io.fabric8.maven.docker.service; + +import io.fabric8.maven.docker.access.DockerAccess; +import io.fabric8.maven.docker.config.BuildImageConfiguration; +import io.fabric8.maven.docker.config.ImageConfiguration; +import io.fabric8.maven.docker.util.ImageName; +import io.fabric8.maven.docker.util.JibBuildServiceUtil; +import io.fabric8.maven.docker.util.Logger; + +import java.util.List; +import com.google.cloud.tools.jib.api.Credential; +import io.fabric8.maven.docker.config.Arguments; +import io.fabric8.maven.docker.util.DeepCopy; +import io.fabric8.maven.docker.util.MojoParameters; +import org.apache.maven.plugin.Mojo; + +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; + +public class JibBuildService { + + private Logger log; + private BuildService.BuildContext dockerBuildContext; + private MojoParameters mojoParameters; + + private JibBuildService() { } + + public JibBuildService (BuildService.BuildContext dockerBuildContext, MojoParameters mojoParameters, Logger log) { + Objects.requireNonNull(dockerBuildContext, "dockerBuildContext"); + this.dockerBuildContext = dockerBuildContext; + this.mojoParameters = mojoParameters; + this.log = log; + } + + public void buildImage(ImageConfiguration imageConfiguration, ImagePullManager imagePullManager, BuildService.BuildContext buildContext) { + try { + BuildImageConfiguration buildImageConfiguration = imageConfiguration.getBuildConfiguration(); + List tags = buildImageConfiguration.getTags(); + + JibBuildService.JibBuildConfiguration jibBuildConfiguration; + String fullName = ""; + if (tags.size() > 0) { + for (String tag : tags) { + if (tag != null) { + fullName = new ImageName(imageConfiguration.getName(), tag).getFullName(); + } + } + } else { + fullName = new ImageName(imageConfiguration.getName(), null).getFullName(); + } + log.info("Image tagging successful!"); + jibBuildConfiguration = JibBuildServiceUtil.getJibBuildConfiguration(dockerBuildContext, mojoParameters, buildImageConfiguration, fullName, log); + JibBuildServiceUtil.buildImage(jibBuildConfiguration, log); + } catch (Exception ex) { + throw new UnsupportedOperationException(); + } + } + + public static class JibBuildConfiguration { + + private Map envMap; + + private Credential credential; + + private List ports; + + private String from; + + private String target; + + private Path fatJarPath; + + private Arguments entrypoint; + + private String targetDir; + + private String outputDir; + + private JibBuildConfiguration() {} + + public Arguments getEntryPoint() { + return entrypoint; + } + + public String getTargetDir() { + return targetDir; + } + + public String getOutputDir() { + return outputDir; + } + + public Map getEnvMap() { + return envMap; + } + + public Credential getCredential() { + return credential; + } + + public List getPorts() { + return ports; + } + + public String getFrom() { + return from; + } + + public String getTargetImage() { + return target; + } + + public Path getFatJar() { + return fatJarPath; + } + + public static class Builder { + private final JibBuildConfiguration configutil; + private final Logger logger; + + public Builder(Logger logger) { + this(null, logger); + } + + public Builder(JibBuildConfiguration that, Logger logger) { + this.logger = logger; + if (that == null) { + this.configutil = new JibBuildConfiguration(); + } else { + this.configutil = DeepCopy.copy(that); + } + } + + public Builder envMap(Map envMap) { + configutil.envMap = envMap; + return this; + } + + public Builder credential(Credential credential) { + configutil.credential = credential; + return this; + } + + public Builder ports(List ports) { + configutil.ports = ports; + return this; + } + + public Builder from(String from) { + configutil.from = from; + return this; + } + + public Builder targetImage(String imageName) { + configutil.target = imageName; + return this; + } + + public Builder entrypoint(Arguments entrypoint) { + configutil.entrypoint = entrypoint; + return this; + } + + public Builder buildDirectory(String buildDir) { + configutil.fatJarPath = JibBuildServiceUtil.getFatJar(buildDir, logger); + return this; + } + + public Builder targetDir(String targetDir) { + configutil.targetDir = targetDir; + return this; + } + + public Builder outputDir(String outputDir) { + configutil.outputDir = outputDir; + return this; + } + + public JibBuildConfiguration build() { + return configutil; + } + } + } +} diff --git a/src/main/java/io/fabric8/maven/docker/util/FatJarDetector.java b/src/main/java/io/fabric8/maven/docker/util/FatJarDetector.java new file mode 100644 index 000000000..1480e8bf0 --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/util/FatJarDetector.java @@ -0,0 +1,84 @@ +package io.fabric8.maven.docker.util; + +import org.apache.maven.plugin.MojoExecutionException; + +import java.io.File; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +/** + * Class for finding out the fat jar of a directory and provide + * some insights into the fat jar + * @author roland + * @since 10/11/16 + */ +public class FatJarDetector { + + private File directory; + private Result result; + + public FatJarDetector(String dir) { + this.directory = new File(dir); + } + + public Result scan() throws MojoExecutionException { + // Scanning is lazy ... + if (result == null) { + if (!directory.exists()) { + // No directory to check found so we return null here ... + return null; + } + String[] jarOrWars = directory.list((dir, name) -> name.endsWith(".war") || name.endsWith(".jar")); + if (jarOrWars == null || jarOrWars.length == 0) { + return null; + } + long maxSize = 0; + for (String jarOrWar : jarOrWars) { + File archiveFile = new File(directory, jarOrWar); + try (JarFile archive = new JarFile(archiveFile)){ + Manifest mf = archive.getManifest(); + Attributes mainAttributes = mf.getMainAttributes(); + if (mainAttributes != null) { + String mainClass = mainAttributes.getValue("Main-Class"); + long size = archiveFile.length(); + // Take the largest jar / war file found + if (size > maxSize) { + maxSize = size; + result = new Result(archiveFile, mainClass, mainAttributes); + } + } + } catch (IOException e) { + throw new MojoExecutionException("Cannot examine file " + archiveFile + " for the manifest"); + } + } + } + return result; + } + + public class Result { + + private final File archiveFile; + private final String mainClass; + private final Attributes attributes; + + public Result(File archiveFile, String mainClass, Attributes attributes) { + this.archiveFile = archiveFile; + this.mainClass = mainClass; + this.attributes = attributes; + } + + public File getArchiveFile() { + return archiveFile; + } + + public String getMainClass() { + return mainClass; + } + + public String getManifestEntry(String key) { + return attributes.getValue(key); + } + } +} diff --git a/src/main/java/io/fabric8/maven/docker/util/JibBuildServiceUtil.java b/src/main/java/io/fabric8/maven/docker/util/JibBuildServiceUtil.java new file mode 100644 index 000000000..28eca95b7 --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/util/JibBuildServiceUtil.java @@ -0,0 +1,254 @@ +package io.fabric8.maven.docker.util; + +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.Jib; +import com.google.cloud.tools.jib.api.JibContainer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.Port; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.api.TarImage; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent; +import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; +import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLoggerBuilder; +import com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator; +import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; +import io.fabric8.maven.docker.access.AuthConfig; +import io.fabric8.maven.docker.config.BuildImageConfiguration; +import io.fabric8.maven.docker.service.BuildService; +import io.fabric8.maven.docker.service.JibBuildService; +import io.fabric8.maven.docker.service.RegistryService; +import org.apache.maven.plugin.MojoExecutionException; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +public class JibBuildServiceUtil { + + private JibBuildServiceUtil() {} + + private static final String DEFAULT_JAR_NAME = "/app.jar"; + private static final String DEFAULT_USER_NAME = "fabric8/"; + private static ConsoleLogger consoleLogger; + + /** + * Builds a container image using JIB + * @param buildConfiguration + * @param log + * @throws InvalidImageReferenceException + */ + public static void buildImage(JibBuildService.JibBuildConfiguration buildConfiguration, Logger log) throws InvalidImageReferenceException { + + String fromImage = buildConfiguration.getFrom(); + String targetImage = buildConfiguration.getTargetImage(); + Credential credential = buildConfiguration.getCredential(); + Map envMap = buildConfiguration.getEnvMap(); + List portList = buildConfiguration.getPorts(); + Set portSet = getPortSet(portList); + String outputDir = buildConfiguration.getOutputDir(); + String targetDir = buildConfiguration.getTargetDir(); + Path fatJar = buildConfiguration.getFatJar(); + + List entrypointList = new ArrayList<>(); + if(buildConfiguration.getEntryPoint() != null) { + entrypointList = buildConfiguration.getEntryPoint().asStrings(); + } + + buildImage(fromImage, targetImage, envMap, credential, portSet, fatJar, entrypointList, targetDir, outputDir, log); + } + + /** + * Builds a container image using Jib from all the following parameters: + * + * @param baseImage + * @param targetImage + * @param envMap + * @param credential + * @param portSet + * @param fatJar + * @param entrypointList + * @param targetDir + * @param outputDir + * @param log + * @return + * @throws InvalidImageReferenceException + */ + protected static JibContainer buildImage(String baseImage, String targetImage, Map envMap, Credential credential, Set portSet, Path fatJar, List entrypointList, String targetDir, String outputDir, Logger log) throws InvalidImageReferenceException { + String username = ""; + String password = ""; + + JibContainerBuilder contBuild = Jib.from(baseImage); + + if (envMap != null) { + contBuild = contBuild.setEnvironment(envMap); + } + + if (portSet != null) { + contBuild = contBuild.setExposedPorts(portSet); + } + + if (fatJar != null) { + String fatJarName = fatJar.getFileName().toString(); + String jarPath = targetDir + "/" + (fatJarName.isEmpty() ? DEFAULT_JAR_NAME: fatJarName); + contBuild = contBuild + .addLayer(LayerConfiguration.builder().addEntry(fatJar, AbsoluteUnixPath.get(jarPath)).build()); + } + + if(!entrypointList.isEmpty()) { + contBuild = contBuild.setEntrypoint(entrypointList); + } + + if (credential != null) { + username = credential.getUsername(); + password = credential.getPassword(); + + if (targetImage.contains(DEFAULT_USER_NAME)) { + targetImage = targetImage.replaceFirst(DEFAULT_USER_NAME, username + "/"); + } + } + + RegistryImage registryImage = RegistryImage.named(targetImage).addCredential(username, password); + String imageTarName = ImageReference.parse(targetImage).getRepository().concat(".tar"); + TarImage tarImage = TarImage.named(targetImage).saveTo(Paths.get(outputDir + "/" + imageTarName)); + try { + JibContainer jibContainer = buildContainer(contBuild, registryImage, log); + log.info("Image %s successfully built and pushed.", targetImage); + return jibContainer; + } catch (RegistryException re) { + log.warn("Registry Exception occureed : %s", re.getMessage()); + log.warn("Credentials are probably either not configured or are incorrect."); + log.info("Building Image Tarball at %s.", imageTarName); + JibContainer jibContainer = buildContainer(contBuild, tarImage, log, false); + log.info(" %s successfully built.", Paths.get(outputDir + "/" + imageTarName)); + return jibContainer; + } catch (ExecutionException e) { + log.warn("Can't connect to the remote registry host: %s", e.getMessage()); + JibContainer jibContainer = buildContainer(contBuild, tarImage, log, true); + log.info("%s successfully built.", Paths.get(outputDir + "/" + imageTarName)); + return jibContainer; + } + } + + public static JibContainer buildContainer(JibContainerBuilder jibContainerBuilder, TarImage image, Logger logger, boolean offline) { + try { + if (offline) { + logger.info("Trying to build the image tarball in the offline mode."); + } + return jibContainerBuilder.containerize(Containerizer.to(image).setOfflineMode(offline)); + } catch (CacheDirectoryCreationException | IOException | InterruptedException | ExecutionException | RegistryException ex) { + logger.error("Unable to build the image tarball: %s", ex.getMessage()); + throw new IllegalStateException(ex); + } + } + public static JibContainer buildContainer(JibContainerBuilder jibContainerBuilder, RegistryImage image,Logger logger) throws RegistryException, ExecutionException { + try { + consoleLogger = getConsoleLogger(logger); + + return jibContainerBuilder.containerize(Containerizer.to(image) + .addEventHandler(LogEvent.class, JibBuildServiceUtil::log) + .addEventHandler( + TimerEvent.class, + new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) + .addEventHandler( + ProgressEvent.class, + new ProgressEventHandler( + update -> + consoleLogger.setFooter( + ProgressDisplayGenerator.generateProgressDisplay( + update.getProgress(), update.getUnfinishedLeafTasks()))))); + } catch (CacheDirectoryCreationException | IOException | InterruptedException e) { + logger.error("Unable to build the image in the offline mode: %s", e.getMessage()); + throw new IllegalStateException(e); + } + } + + public static void log(LogEvent event) { + consoleLogger.log(event.getLevel(), event.getMessage()); + } + + public static ConsoleLogger getConsoleLogger(Logger logger) { + ConsoleLoggerBuilder consoleLoggerBuilder = ConsoleLoggerBuilder + .rich(new SingleThreadedExecutor(), true) + .lifecycle(logger::info); + if (logger.isDebugEnabled()) { + consoleLoggerBuilder + .debug(logger::debug) + .info(logger::info); + } + return consoleLoggerBuilder.build(); + } + + public static JibBuildService.JibBuildConfiguration getJibBuildConfiguration(BuildService.BuildContext dockerBuildContext, MojoParameters mojoParameters, BuildImageConfiguration buildImageConfiguration, String fullImageName, Logger log) throws MojoExecutionException { + + RegistryService.RegistryConfig registryConfig = dockerBuildContext.getRegistryConfig(); + + String targetDir = buildImageConfiguration.getAssemblyConfiguration().getTargetDir(); + + String outputDir = mojoParameters.getOutputDirectory(); + + if(targetDir == null) { + targetDir = "/deployments"; + } + + AuthConfig authConfig = registryConfig.getAuthConfigFactory() + .createAuthConfig(true, true, registryConfig.getAuthConfig(), + registryConfig.getSettings(), null, registryConfig.getRegistry()); + + JibBuildService.JibBuildConfiguration.Builder jibBuildConfigurationBuilder = new JibBuildService.JibBuildConfiguration.Builder(log).from(buildImageConfiguration.getFrom()) + .envMap(buildImageConfiguration.getEnv()) + .ports(buildImageConfiguration.getPorts()) + .entrypoint(buildImageConfiguration.getEntryPoint()) + .targetImage(fullImageName) + .targetDir(targetDir) + .outputDir(outputDir) + .buildDirectory(mojoParameters.getProject().getBuild().getDirectory()); + if(authConfig != null) { + jibBuildConfigurationBuilder.credential(Credential.from(authConfig.getUsername(), authConfig.getPassword())); + } + + return jibBuildConfigurationBuilder.build(); + } + + + private static Set getPortSet(List ports) { + + Set portSet = new HashSet(); + for(String port : ports) { + portSet.add(Port.tcp(Integer.parseInt(port))); + } + + return portSet; + } + + public static Path getFatJar(String buildDir, Logger log) { + FatJarDetector fatJarDetector = new FatJarDetector(buildDir); + try { + FatJarDetector.Result result = fatJarDetector.scan(); + if(result != null) { + return result.getArchiveFile().toPath(); + } + + } catch (MojoExecutionException e) { + log.error("MOJO Execution exception occurred: %s", e); + throw new UnsupportedOperationException(); + } + return null; + } +}