001/* 002 * Copyright 2017-2022 original authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.micronaut.maven; 017 018import com.github.dockerjava.api.command.BuildImageCmd; 019import com.google.cloud.tools.jib.api.ImageReference; 020import com.google.cloud.tools.jib.api.InvalidImageReferenceException; 021import io.micronaut.core.util.StringUtils; 022import io.micronaut.maven.core.MicronautRuntime; 023import io.micronaut.maven.core.MojoUtils; 024import io.micronaut.maven.jib.JibConfigurationService; 025import io.micronaut.maven.services.ApplicationConfigurationService; 026import io.micronaut.maven.services.DockerService; 027import org.apache.maven.execution.MavenSession; 028import org.apache.maven.plugin.MojoExecution; 029import org.apache.maven.plugin.MojoExecutionException; 030import org.apache.maven.plugins.annotations.Mojo; 031import org.apache.maven.plugins.annotations.ResolutionScope; 032import org.apache.maven.project.MavenProject; 033import org.graalvm.buildtools.utils.NativeImageUtils; 034 035import javax.inject.Inject; 036import java.io.File; 037import java.io.IOException; 038import java.nio.file.Files; 039import java.nio.file.Paths; 040import java.nio.file.StandardCopyOption; 041import java.util.HashMap; 042import java.util.List; 043import java.util.Map; 044import java.util.Set; 045import java.util.function.Supplier; 046 047/** 048 * <p>Implementation of the <code>docker-native</code> packaging.</p> 049 * <p><strong>WARNING</strong>: this goal is not intended to be executed directly. Instead, specify the packaging type 050 * using the <code>packaging</code> property, eg:</p> 051 * 052 * <pre>mvn package -Dpackaging=docker-native</pre> 053 * 054 * @author Álvaro Sánchez-Mariscal 055 * @author Iván López 056 * @since 1.1 057 */ 058@Mojo(name = DockerNativeMojo.DOCKER_NATIVE_PACKAGING, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) 059public class DockerNativeMojo extends AbstractDockerMojo { 060 061 public static final String DOCKER_NATIVE_PACKAGING = "docker-native"; 062 public static final String MICRONAUT_PARENT = "io.micronaut.platform:micronaut-parent"; 063 public static final String MICRONAUT_VERSION = "micronaut.version"; 064 public static final String ARGS_FILE_PROPERTY_NAME = "graalvm.native-image.args-file"; 065 static final int AWS_LAMBDA_MAX_ALLOWED_VERSION = 25; 066 static final int ORACLE_FUNCTION_MAX_ALLOWED_VERSION = 25; 067 static final int MAX_ALLOWED_VERSION = 25; 068 private MicronautRuntime runtime; 069 070 @SuppressWarnings("CdiInjectionPointsInspection") 071 @Inject 072 public DockerNativeMojo(MavenProject mavenProject, JibConfigurationService jibConfigurationService, 073 ApplicationConfigurationService applicationConfigurationService, DockerService dockerService, 074 MavenSession mavenSession, MojoExecution mojoExecution) { 075 super(mavenProject, jibConfigurationService, applicationConfigurationService, dockerService, mavenSession, mojoExecution); 076 } 077 078 @Override 079 public void execute() throws MojoExecutionException { 080 checkGraalVm(); 081 082 try { 083 copyDependencies(); 084 085 this.runtime = MicronautRuntime.valueOf(micronautRuntime.toUpperCase()); 086 087 switch (runtime.getBuildStrategy()) { 088 case LAMBDA -> { 089 checkJavaVersion(AWS_LAMBDA_MAX_ALLOWED_VERSION); 090 buildDockerNativeLambda(); 091 } 092 case ORACLE_FUNCTION -> { 093 checkJavaVersion(ORACLE_FUNCTION_MAX_ALLOWED_VERSION); 094 buildOracleCloud(); 095 } 096 case DEFAULT -> { 097 checkJavaVersion(MAX_ALLOWED_VERSION); 098 buildDockerNative(); 099 } 100 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 101 } 102 103 104 } catch (InvalidImageReferenceException iire) { 105 String message = "Invalid image reference " 106 + iire.getInvalidReference() 107 + ", perhaps you should check that the reference is formatted correctly according to " + 108 "https://docs.docker.com/engine/reference/commandline/tag/#extended-description" + 109 "\nFor example, slash-separated name components cannot have uppercase letters"; 110 throw new MojoExecutionException(message); 111 } catch (IOException | IllegalArgumentException e) { 112 throw new MojoExecutionException(e.getMessage(), e); 113 } 114 } 115 116 private void checkGraalVm() throws MojoExecutionException { 117 String micronautVersion = mavenProject.getProperties().getProperty(MICRONAUT_VERSION); 118 if (mavenProject.hasParent()) { 119 String ga = mavenProject.getParent().getGroupId() + ":" + mavenProject.getParent().getArtifactId(); 120 if (MICRONAUT_PARENT.equals(ga)) { 121 String micronautParentVersion = mavenProject.getModel().getParent().getVersion(); 122 if (micronautVersion.equals(micronautParentVersion)) { 123 if (!mavenProject.getInjectedProfileIds().get(MICRONAUT_PARENT + ":" + micronautParentVersion).contains("graalvm")) { 124 String javaVendor = System.getProperty("java.vendor", ""); 125 if (javaVendor.toLowerCase().contains("graalvm")) { 126 throw new MojoExecutionException("The [graalvm] profile was not activated automatically because the native-image component is not installed (or not found in your path). Either activate the profile manually (-Pgraalvm) or install the native-image component (gu install native-image), and try again"); 127 } else { 128 throw new MojoExecutionException("The [graalvm] profile was not activated automatically because you are not using a GraalVM JDK. Activate the profile manually (-Pgraalvm) and try again"); 129 } 130 } 131 } else { 132 String message = String.format("The %s version (%s) differs from the %s property (%s). Please, make sure both refer to the same version", MICRONAUT_PARENT, micronautParentVersion, MICRONAUT_VERSION, micronautVersion); 133 throw new MojoExecutionException(message); 134 } 135 } else { 136 getLog().warn("The parent POM of this project is not set to " + MICRONAUT_PARENT); 137 } 138 } else { 139 getLog().warn("This project has no parent POM defined. To avoid build problems, please set the parent to " + MICRONAUT_PARENT); 140 } 141 } 142 143 private void checkJavaVersion(int maxAllowedVersion) throws MojoExecutionException { 144 if (javaVersion().getMajorVersion() > maxAllowedVersion) { 145 throw new MojoExecutionException("To build native images you must set the Java target byte code level to Java %s or below".formatted(maxAllowedVersion)); 146 } 147 } 148 149 private void buildDockerNativeLambda() throws IOException, MojoExecutionException { 150 var buildImageCmdArguments = new HashMap<String, String>(); 151 152 // Add proxy settings if configured 153 buildImageCmdArguments.putAll(getProxyBuildArgs()); 154 155 File dockerfile = dockerService.loadDockerfileAsResource(DockerfileMojo.DOCKERFILE_AWS_CUSTOM_RUNTIME); 156 lambdaBootstrapCommand(dockerfile); 157 158 // Starter sets the right class in pom.xml: 159 // - For applications: io.micronaut.function.aws.runtime.MicronautLambdaRuntime 160 // - For function apps: com.example.BookLambdaRuntime 161 BuildImageCmd buildImageCmd = addNativeImageBuildArgs(buildImageCmdArguments, supportsSharedArena(), () -> dockerService.buildImageCmd() 162 .withDockerfile(dockerfile) 163 .withBuildArg("GRAALVM_DOWNLOAD_URL", graalVmDownloadUrl())); 164 buildImageCmd.withBuildArg("CLASS_NAME", escapeClassNameBuildArg(mainClass)); 165 String imageId = dockerService.buildImage(buildImageCmd); 166 File functionZip = dockerService.copyFromContainer(imageId, "/function/function.zip"); 167 getLog().info("AWS Lambda Custom Runtime ZIP: " + functionZip.getPath()); 168 } 169 170 private void buildDockerNative() throws IOException, InvalidImageReferenceException, MojoExecutionException { 171 String dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE; 172 if (Boolean.TRUE.equals(staticNativeImage)) { 173 getLog().info("Generating a static native image"); 174 dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_STATIC; 175 } else if (baseImageRun.contains("distroless")) { 176 getLog().info("Generating a mostly static native image"); 177 dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_DISTROLESS; 178 } 179 180 buildDockerfile(dockerfileName, true); 181 } 182 183 private void buildOracleCloud() throws IOException, InvalidImageReferenceException, MojoExecutionException { 184 buildDockerfile(DockerfileMojo.DOCKERFILE_NATIVE_ORACLE_CLOUD, false); 185 } 186 187 private void buildDockerfile(String dockerfileName, boolean passClassName) throws IOException, InvalidImageReferenceException, MojoExecutionException { 188 Set<String> tags = getTags(); 189 for (String tag : tags) { 190 ImageReference.parse(tag); 191 } 192 193 String from = getFrom(); 194 String ports = getPorts(); 195 getLog().info("Exposing port(s): " + ports); 196 197 File providedDockerfile = new File(mavenProject.getBasedir(), DockerfileMojo.DOCKERFILE); 198 if (providedDockerfile.isFile()) { 199 buildProvidedDockerfile(providedDockerfile, tags, passClassName, from, ports); 200 return; 201 } 202 203 File dockerfile = dockerService.loadDockerfileAsResource(dockerfileName); 204 if (DockerfileMojo.DOCKERFILE_NATIVE_ORACLE_CLOUD.equals(dockerfileName)) { 205 oracleCloudFunctionCmd(dockerfile); 206 } 207 208 BuildImageCmd buildImageCmd = addNativeImageBuildArgs(buildImageCmdArguments(passClassName), supportsSharedArena(), () -> dockerService.buildImageCmd() 209 .withDockerfile(dockerfile) 210 .withTags(tags) 211 .withBuildArg("BASE_IMAGE", from) 212 .withBuildArg("PORTS", ports)); 213 214 dockerService.buildImage(buildImageCmd); 215 } 216 217 private void buildProvidedDockerfile(File providedDockerfile, Set<String> tags, boolean passClassName, String from, String ports) throws IOException, MojoExecutionException { 218 getLog().info("Using Dockerfile: " + providedDockerfile.getAbsolutePath()); 219 220 File targetDir = new File(mavenProject.getBuild().getDirectory()); 221 Files.createDirectories(targetDir.toPath()); 222 File targetDockerfile = new File(targetDir, providedDockerfile.getName()); 223 Files.copy(providedDockerfile.toPath(), targetDockerfile.toPath(), StandardCopyOption.REPLACE_EXISTING); 224 225 BuildImageCmd buildImageCmd = addNativeImageBuildArgs(buildImageCmdArguments(passClassName), false, () -> dockerService.buildImageCmd() 226 .withDockerfile(targetDockerfile) 227 .withTags(tags) 228 .withBaseDirectory(targetDir) 229 .withBuildArg("BASE_IMAGE", from) 230 .withBuildArg("PORTS", ports)); 231 232 dockerService.buildImage(buildImageCmd); 233 } 234 235 private Map<String, String> buildImageCmdArguments(boolean passClassName) throws MojoExecutionException { 236 var buildImageCmdArguments = new HashMap<String, String>(); 237 238 // Add proxy settings if configured 239 buildImageCmdArguments.putAll(getProxyBuildArgs()); 240 241 if (StringUtils.isNotEmpty(baseImageRun) && Boolean.FALSE.equals(staticNativeImage)) { 242 buildImageCmdArguments.put("BASE_IMAGE_RUN", baseImageRun); 243 } 244 245 if (passClassName) { 246 buildImageCmdArguments.put("CLASS_NAME", escapeClassNameBuildArg(mainClass)); 247 } 248 return buildImageCmdArguments; 249 } 250 251 private BuildImageCmd addNativeImageBuildArgs(Map<String, String> buildImageCmdArguments, boolean sharedArenaSupport, Supplier<BuildImageCmd> buildImageCmdSupplier) throws IOException { 252 String argsFile = mavenProject.getProperties().getProperty(ARGS_FILE_PROPERTY_NAME); 253 List<String> allNativeImageBuildArgs = MojoUtils.computeNativeImageArgs(nativeImageBuildArgs, baseImageRun, argsFile, sharedArenaSupport); 254 //Remove extra main class argument 255 allNativeImageBuildArgs.remove(mainClass); 256 getLog().info("GraalVM native image build args: " + allNativeImageBuildArgs); 257 List<String> conversionResult = NativeImageUtils.convertToArgsFile(allNativeImageBuildArgs, Paths.get(mavenProject.getBuild().getDirectory())); 258 if (conversionResult.size() == 1) { 259 Files.delete(Paths.get(argsFile)); 260 261 BuildImageCmd buildImageCmd = buildImageCmdSupplier.get(); 262 263 for (Map.Entry<String, String> buildArg : buildImageCmdArguments.entrySet()) { 264 buildImageCmd.withBuildArg(buildArg.getKey(), buildArg.getValue()); 265 } 266 267 getNetworkMode().ifPresent(buildImageCmd::withNetworkMode); 268 return buildImageCmd; 269 } else { 270 throw new IOException("Unable to convert native image build args to args file"); 271 } 272 } 273 274 static String escapeClassNameBuildArg(String value) throws MojoExecutionException { 275 return escapeShellDoubleQuoted("exec.mainClass", value); 276 } 277 278}