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 io.micronaut.maven.core.DockerBuildStrategy; 019import io.micronaut.maven.core.MicronautRuntime; 020import io.micronaut.maven.core.MojoUtils; 021import io.micronaut.maven.jdkaotcache.JdkAotCacheTraining; 022import io.micronaut.maven.jdkaotcache.TrainingMode; 023import io.micronaut.maven.jdkaotcache.TrainingRun; 024import io.micronaut.maven.jib.JibConfigurationService; 025import io.micronaut.maven.jib.JibMicronautExtension; 026import io.micronaut.maven.services.ApplicationConfigurationService; 027import io.micronaut.maven.services.DockerService; 028import io.micronaut.maven.services.ExecutorService; 029import org.apache.maven.execution.MavenSession; 030import org.apache.maven.plugin.MojoExecution; 031import org.apache.maven.plugin.MojoExecutionException; 032import org.apache.maven.plugins.annotations.Execute; 033import org.apache.maven.plugins.annotations.LifecyclePhase; 034import org.apache.maven.plugins.annotations.Mojo; 035import org.apache.maven.plugins.annotations.Parameter; 036import org.apache.maven.plugins.annotations.ResolutionScope; 037import org.apache.maven.project.MavenProject; 038import org.apache.maven.shared.invoker.MavenInvocationException; 039import org.graalvm.buildtools.utils.NativeImageUtils; 040 041import javax.inject.Inject; 042import java.io.File; 043import java.io.IOException; 044import java.nio.file.Files; 045import java.nio.file.Path; 046import java.nio.file.Paths; 047import java.util.ArrayList; 048import java.util.List; 049import java.util.Optional; 050import java.util.stream.Collectors; 051import java.util.stream.Stream; 052 053import static io.micronaut.maven.DockerNativeMojo.ARGS_FILE_PROPERTY_NAME; 054 055/** 056 * <p>Generates a <code>Dockerfile</code> depending on the <code>packaging</code> and <code>micronaut.runtime</code> 057 * properties. 058 * 059 * <pre>mvn mn:dockerfile -Dpackaging=docker-native -Dmicronaut.runtime=lambda</pre> 060 * 061 * @author Álvaro Sánchez-Mariscal 062 * @since 1.1 063 */ 064@Mojo(name = "dockerfile", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) 065@Execute(phase = LifecyclePhase.PROCESS_CLASSES) 066public class DockerfileMojo extends AbstractDockerMojo { 067 public static final String DOCKERFILE = "Dockerfile"; 068 public static final String DOCKERFILE_AWS_CUSTOM_RUNTIME = "DockerfileNativeLambda"; 069 public static final String DOCKERFILE_AWS = "DockerfileLambda"; 070 public static final String DOCKERFILE_ORACLE_CLOUD = "DockerfileOracleCloud"; 071 public static final String DOCKERFILE_NATIVE = "DockerfileNative"; 072 public static final String DOCKERFILE_CRAC = "DockerfileCrac"; 073 public static final String DOCKERFILE_CRAC_CHECKPOINT = "DockerfileCracCheckpoint"; 074 public static final String DOCKERFILE_CRAC_CHECKPOINT_FILE = "Dockerfile.crac.checkpoint"; 075 public static final String DOCKERFILE_NATIVE_DISTROLESS = "DockerfileNativeDistroless"; 076 public static final String DOCKERFILE_NATIVE_STATIC = "DockerfileNativeStatic"; 077 public static final String DOCKERFILE_NATIVE_ORACLE_CLOUD = "DockerfileNativeOracleCloud"; 078 public static final String NATIVE_BUILD_TOOLS_MAVEN_PLUGIN = "org.graalvm.buildtools:native-maven-plugin"; 079 static final String DOCKERFILE_JDK_AOT_CACHE = "DockerfileJdkAotCache"; 080 private static final String CLASS_NAME_PLACEHOLDER = "CLASS_NAME"; 081 private static final String JDK_AOT_CACHE_TRAINING_PLACEHOLDER = "JDK_AOT_CACHE_TRAINING"; 082 private static final String EXEC_MAIN_CLASS_SOURCE = "exec.mainClass"; 083 /** 084 * Where the training script writes the JDK AOT cache. The {@code ENTRYPOINT} of the {@code DockerfileJdkAotCache} 085 * template loads it from there. 086 */ 087 private static final String JDK_AOT_CACHE_IMAGE_FILE = "/home/app/app.aot"; 088 089 private final ExecutorService executorService; 090 private String jdkAotCacheTrainingArguments = ""; 091 092 /** 093 * Whether the generated Dockerfile trains a JDK AOT cache (Java 25 or later) while the image is built, and starts 094 * the application with it so that it starts faster. Only for {@code docker} packaging, and not for the 095 * {@code lambda} or {@code oracle_function} runtimes. The base image needs {@code bash}. 096 * 097 * @since 5.1.0 098 */ 099 @Parameter(property = "micronaut.docker.jdkAotCache", defaultValue = "false") 100 private boolean jdkAotCache; 101 102 /** 103 * How the JDK AOT cache is trained. {@code LOAD} loads the application's bean definitions and exits without 104 * starting it, so the training needs no database or other service. {@code START} starts the application, sends 105 * the training requests and stops it, which trains a more complete cache. When not set, the mode is {@code LOAD} if 106 * the application's Micronaut version supports it (Micronaut Core 5.3.0 or later), and {@code START} otherwise. 107 * 108 * @since 5.1.0 109 */ 110 @Parameter(property = "micronaut.docker.jdkAotCache.trainingMode") 111 private TrainingMode jdkAotCacheTrainingMode; 112 113 /** 114 * The paths that a {@code START} training run sends a GET request to once the application is ready. A short, 115 * representative warm-up makes a better cache. A request that gets no response, or a status of 500 or more, fails 116 * the build. A status from 400 to 499 is logged as a warning with Micronaut Core 5.3.0 or later, and fails the build 117 * with earlier versions. 118 * 119 * @since 5.1.0 120 */ 121 @Parameter(property = "micronaut.docker.jdkAotCache.trainingPaths") 122 private List<String> jdkAotCacheTrainingPaths; 123 124 /** 125 * The timeout, in seconds, of the JDK AOT cache training run. With Micronaut Core before 5.3.0, it applies to each 126 * step: waiting for the application to be ready, each training request, and waiting for the application to exit. 127 * 128 * @since 5.1.0 129 */ 130 @Parameter(property = "micronaut.docker.jdkAotCache.trainingTimeout", defaultValue = "180") 131 private int jdkAotCacheTrainingTimeout; 132 133 @Inject 134 public DockerfileMojo(MavenProject mavenProject, DockerService dockerService, JibConfigurationService jibConfigurationService, 135 ApplicationConfigurationService applicationConfigurationService, ExecutorService executorService, 136 MavenSession mavenSession, MojoExecution mojoExecution) { 137 super(mavenProject, jibConfigurationService, applicationConfigurationService, dockerService, mavenSession, mojoExecution); 138 this.executorService = executorService; 139 } 140 141 @Override 142 public void execute() throws MojoExecutionException { 143 var runtime = MicronautRuntime.valueOf(micronautRuntime.toUpperCase()); 144 var packaging = Packaging.of(mavenProject.getPackaging()); 145 try { 146 if (jdkAotCache && packaging != Packaging.DOCKER) { 147 getLog().warn("micronaut.docker.jdkAotCache only applies to " + Packaging.DOCKER.id() + " packaging, ignoring it"); 148 } 149 copyDependencies(jdkAotCache); 150 var dockerfile = switch (packaging) { 151 case DOCKER_NATIVE -> buildDockerfileNative(runtime); 152 case DOCKER -> buildDockerfile(runtime); 153 case DOCKER_CRAC -> buildCracDockerfile(runtime); 154 default -> throw new MojoExecutionException("Packaging is set to [" + packaging + "]. To generate a Dockerfile, set the packaging to either [" + Packaging.DOCKER.id() + "] or [" + Packaging.DOCKER_NATIVE.id() + "]"); 155 }; 156 157 dockerfile.ifPresent(file -> getLog().info("Dockerfile written to: " + file.getAbsolutePath())); 158 159 } catch (IOException | MavenInvocationException e) { 160 throw new MojoExecutionException(e.getMessage(), e); 161 } 162 } 163 164 private Optional<File> buildDockerfile(MicronautRuntime runtime) throws IOException, MojoExecutionException { 165 if (jdkAotCache) { 166 return buildJdkAotCacheDockerfile(runtime); 167 } 168 File dockerfile; 169 switch (runtime.getBuildStrategy()) { 170 case ORACLE_FUNCTION -> { 171 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_ORACLE_CLOUD); 172 oracleCloudFunctionCmd(dockerfile); 173 processOracleFunctionDockerfile(dockerfile); 174 } 175 case LAMBDA -> { 176 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_AWS); 177 processDockerfile(dockerfile); 178 } 179 case DEFAULT -> { 180 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE); 181 processDockerfile(dockerfile); 182 } 183 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 184 } 185 return Optional.ofNullable(dockerfile); 186 } 187 188 private Optional<File> buildJdkAotCacheDockerfile(MicronautRuntime runtime) throws IOException, MojoExecutionException { 189 if (runtime.getBuildStrategy() != DockerBuildStrategy.DEFAULT) { 190 throw new MojoExecutionException("micronaut.docker.jdkAotCache only supports the default runtime, not " 191 + micronautRuntime); 192 } 193 List<String> trainingPaths = JdkAotCacheTraining.validateTrainingPaths(jdkAotCacheTrainingPaths); 194 TrainingRun trainingRun = TrainingRun.resolve(jdkAotCacheTrainingMode, trainingPaths, mavenProject.getArtifacts()); 195 trainingRun.logGeneratedDockerfileRun(getLog()); 196 jdkAotCacheTrainingArguments = jdkAotCacheTrainingArguments(trainingPaths, trainingRun); 197 File dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_JDK_AOT_CACHE); 198 processDockerfile(dockerfile); 199 return Optional.ofNullable(dockerfile); 200 } 201 202 private String jdkAotCacheTrainingArguments(List<String> trainingPaths, TrainingRun trainingRun) throws MojoExecutionException { 203 if (jdkAotCacheTrainingTimeout <= 0) { 204 throw new MojoExecutionException("micronaut.docker.jdkAotCache.trainingTimeout must be positive"); 205 } 206 String port = validateExposedPorts("jib.container.ports", getPorts()).trim().split("[\\s/]+")[0]; 207 if (!trainingRun.usesSwitch() && port.isEmpty()) { 208 throw new MojoExecutionException("micronaut.docker.jdkAotCache needs the HTTP port of the application to " 209 + "warm it up. Set it with the Jib container.ports configuration."); 210 } 211 var arguments = new ArrayList<String>(); 212 arguments.add(JDK_AOT_CACHE_IMAGE_FILE); 213 arguments.add(port.isEmpty() ? "0" : port); 214 arguments.add(String.valueOf(jdkAotCacheTrainingTimeout)); 215 arguments.add(trainingRun.scriptArgument()); 216 for (String path : trainingPaths) { 217 arguments.add(shellLiteral("micronaut.docker.jdkAotCache.trainingPaths", path)); 218 } 219 return String.join(" ", arguments); 220 } 221 222 private Optional<File> buildCracDockerfile(MicronautRuntime runtime) throws IOException, MojoExecutionException { 223 File dockerfile; 224 switch (runtime.getBuildStrategy()) { 225 case ORACLE_FUNCTION -> throw new MojoExecutionException("Oracle Functions are currently unsupported"); 226 case LAMBDA -> throw new MojoExecutionException("Lambda Functions are currently unsupported"); 227 case DEFAULT -> { 228 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_CRAC_CHECKPOINT, DOCKERFILE_CRAC_CHECKPOINT_FILE); 229 processDockerfile(dockerfile); 230 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_CRAC); 231 processDockerfile(dockerfile); 232 } 233 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 234 } 235 return Optional.ofNullable(dockerfile); 236 } 237 238 static void processOracleFunctionDockerfile(File dockerfile) throws IOException { 239 if (dockerfile != null) { 240 var allLines = Files.readAllLines(dockerfile.toPath()); 241 String projectFnVersion = JibMicronautExtension.determineProjectFnVersion(System.getProperty("java.version")); 242 allLines.add(0, allLines.remove(0) + projectFnVersion); 243 String entrypoint = JibMicronautExtension.buildProjectFnEntrypoint() 244 .stream() 245 .map(s -> "\"" + s + "\"") 246 .collect(Collectors.joining(", ")); 247 248 allLines.add("ENTRYPOINT [" + entrypoint + "]"); 249 250 Files.write(dockerfile.toPath(), allLines); 251 } 252 } 253 254 private Optional<File> buildDockerfileNative(MicronautRuntime runtime) throws IOException, MavenInvocationException, MojoExecutionException { 255 getLog().info("Generating GraalVM args file"); 256 executorService.invokeGoal(NATIVE_BUILD_TOOLS_MAVEN_PLUGIN, "write-args-file"); 257 File dockerfile; 258 switch (runtime.getBuildStrategy()) { 259 case LAMBDA -> { 260 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_AWS_CUSTOM_RUNTIME); 261 lambdaBootstrapCommand(dockerfile); 262 } 263 case ORACLE_FUNCTION -> { 264 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_NATIVE_ORACLE_CLOUD); 265 oracleCloudFunctionCmd(dockerfile); 266 } 267 case DEFAULT -> { 268 String dockerfileName = DOCKERFILE_NATIVE; 269 if (Boolean.TRUE.equals(staticNativeImage)) { 270 getLog().info("Generating a static native image"); 271 dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_STATIC; 272 } else if (baseImageRun.contains("distroless")) { 273 getLog().info("Generating a mostly static native image"); 274 dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_DISTROLESS; 275 } 276 dockerfile = dockerService.loadDockerfileAsResource(dockerfileName); 277 } 278 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 279 } 280 processDockerfile(dockerfile); 281 return Optional.ofNullable(dockerfile); 282 } 283 284 private void processDockerfile(File dockerfile) throws IOException, MojoExecutionException { 285 if (dockerfile == null) { 286 return; 287 } 288 289 var allLines = Files.readAllLines(dockerfile.toPath()); 290 Files.write(dockerfile.toPath(), processDockerfileLines(allLines)); 291 292 String argsFile = findArgsFile(); 293 if (argsFile != null) { 294 processNativeImageArgs(argsFile); 295 } 296 } 297 298 private List<String> processDockerfileLines(List<String> allLines) throws MojoExecutionException { 299 var result = new ArrayList<String>(); 300 for (String line : allLines) { 301 String processedLine = processDockerfileLine(line); 302 if (processedLine != null) { 303 result.add(processedLine); 304 } 305 } 306 return result; 307 } 308 309 private String processDockerfileLine(String line) throws MojoExecutionException { 310 if (containsPlaceholder(line, JDK_AOT_CACHE_TRAINING_PLACEHOLDER)) { 311 line = line.replace("${" + JDK_AOT_CACHE_TRAINING_PLACEHOLDER + "}", jdkAotCacheTrainingArguments); 312 } 313 if (line.startsWith("ARG")) { 314 return shouldInlineArgLine(line) ? null : line; 315 } 316 if (containsPlaceholder(line, "BASE_IMAGE_RUN")) { 317 return line.replace("${BASE_IMAGE_RUN}", validateImageReference("micronaut.native-image.base-image-run", baseImageRun)); 318 } 319 if (containsPlaceholder(line, "BASE_IMAGE")) { 320 return line.replace("${BASE_IMAGE}", validateImageReference("jib.from.image", getFrom())); 321 } 322 if (containsPlaceholder(line, "BASE_JAVA_IMAGE")) { 323 return line.replace("${BASE_JAVA_IMAGE}", validateImageReference("base Java image", getBaseImage())); 324 } 325 if (containsPlaceholder(line, "GRAALVM_DOWNLOAD_URL")) { 326 return line.replace("${GRAALVM_DOWNLOAD_URL}", shellLiteral("GraalVM download URL", validateDownloadUrl("GraalVM download URL", graalVmDownloadUrl()))); 327 } 328 if (containsPlaceholder(line, CLASS_NAME_PLACEHOLDER)) { 329 return replaceClassName(line); 330 } 331 if (containsPlaceholder(line, "PORTS")) { 332 return line.replace("${PORTS}", validateExposedPorts("jib.container.ports", getPorts())); 333 } 334 return line; 335 } 336 337 private static boolean containsPlaceholder(String line, String placeholderName) { 338 return line.contains("${" + placeholderName + "}"); 339 } 340 341 private static boolean shouldInlineArgLine(String line) { 342 String trimmed = line.trim(); 343 if (!trimmed.startsWith("ARG")) { 344 return false; 345 } 346 String remainder = trimmed.substring(3).trim(); 347 if (remainder.isEmpty()) { 348 return false; 349 } 350 351 int endEquals = remainder.indexOf('='); 352 int endWhitespace = -1; 353 for (int i = 0; i < remainder.length(); i++) { 354 if (Character.isWhitespace(remainder.charAt(i))) { 355 endWhitespace = i; 356 break; 357 } 358 } 359 360 int end = remainder.length(); 361 if (endEquals >= 0) { 362 end = Math.min(end, endEquals); 363 } 364 if (endWhitespace >= 0) { 365 end = Math.min(end, endWhitespace); 366 } 367 368 String argName = remainder.substring(0, end); 369 return "BASE_IMAGE_RUN".equals(argName) 370 || "BASE_JAVA_IMAGE".equals(argName) 371 || "BASE_IMAGE".equals(argName) 372 || "GRAALVM_DOWNLOAD_URL".equals(argName) 373 || CLASS_NAME_PLACEHOLDER.equals(argName) 374 || "PORTS".equals(argName); 375 } 376 377 private String replaceClassName(String line) throws MojoExecutionException { 378 String className = isJsonArrayClassNameContext(line) 379 ? escapeJsonString(EXEC_MAIN_CLASS_SOURCE, mainClass) 380 : line.contains("\"${CLASS_NAME}\"") 381 ? escapeShellDoubleQuoted(EXEC_MAIN_CLASS_SOURCE, mainClass) 382 : shellLiteral(EXEC_MAIN_CLASS_SOURCE, mainClass); 383 return line.replace("${CLASS_NAME}", className); 384 } 385 386 private static boolean isJsonArrayClassNameContext(String line) { 387 return containsPlaceholder(line, CLASS_NAME_PLACEHOLDER) 388 && (line.contains("ENTRYPOINT [") || line.contains("CMD [")); 389 } 390 391 private String findArgsFile() throws IOException { 392 String argsFile = mavenProject.getProperties().getProperty(ARGS_FILE_PROPERTY_NAME); 393 if (argsFile != null) { 394 return argsFile; 395 } 396 Path targetPath = Paths.get(mavenProject.getBuild().getDirectory()); 397 try (Stream<Path> listStream = Files.list(targetPath)) { 398 return listStream 399 .filter(path -> { 400 String fileName = path.getFileName().toString(); 401 return fileName.startsWith("native-image") && fileName.endsWith("args"); 402 }) 403 .findFirst() 404 .map(path -> path.toAbsolutePath().toString()) 405 .orElse(null); 406 } 407 } 408 409 private void processNativeImageArgs(String argsFile) throws IOException, MojoExecutionException { 410 List<String> allNativeImageBuildArgs = MojoUtils.computeNativeImageArgs(nativeImageBuildArgs, baseImageRun, argsFile, supportsSharedArena()); 411 //Remove extra main class argument 412 allNativeImageBuildArgs.remove(mainClass); 413 getLog().info("GraalVM native image build args: " + allNativeImageBuildArgs); 414 List<String> conversionResult = NativeImageUtils.convertToArgsFile(allNativeImageBuildArgs, Paths.get(mavenProject.getBuild().getDirectory())); 415 if (conversionResult.size() == 1) { 416 Files.delete(Paths.get(argsFile)); 417 } 418 } 419}