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.MicronautRuntime; 019import io.micronaut.maven.core.MojoUtils; 020import io.micronaut.maven.jib.JibConfigurationService; 021import io.micronaut.maven.jib.JibMicronautExtension; 022import io.micronaut.maven.services.ApplicationConfigurationService; 023import io.micronaut.maven.services.DockerService; 024import io.micronaut.maven.services.ExecutorService; 025import org.apache.maven.execution.MavenSession; 026import org.apache.maven.plugin.MojoExecution; 027import org.apache.maven.plugin.MojoExecutionException; 028import org.apache.maven.plugins.annotations.Execute; 029import org.apache.maven.plugins.annotations.LifecyclePhase; 030import org.apache.maven.plugins.annotations.Mojo; 031import org.apache.maven.plugins.annotations.ResolutionScope; 032import org.apache.maven.project.MavenProject; 033import org.apache.maven.shared.invoker.MavenInvocationException; 034import org.graalvm.buildtools.utils.NativeImageUtils; 035 036import javax.inject.Inject; 037import java.io.File; 038import java.io.IOException; 039import java.nio.file.Files; 040import java.nio.file.Path; 041import java.nio.file.Paths; 042import java.util.ArrayList; 043import java.util.List; 044import java.util.Optional; 045import java.util.stream.Collectors; 046import java.util.stream.Stream; 047 048import static io.micronaut.maven.DockerNativeMojo.ARGS_FILE_PROPERTY_NAME; 049 050/** 051 * <p>Generates a <code>Dockerfile</code> depending on the <code>packaging</code> and <code>micronaut.runtime</code> 052 * properties. 053 * 054 * <pre>mvn mn:dockerfile -Dpackaging=docker-native -Dmicronaut.runtime=lambda</pre> 055 * 056 * @author Álvaro Sánchez-Mariscal 057 * @since 1.1 058 */ 059@Mojo(name = "dockerfile", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) 060@Execute(phase = LifecyclePhase.PROCESS_CLASSES) 061public class DockerfileMojo extends AbstractDockerMojo { 062 public static final String DOCKERFILE = "Dockerfile"; 063 public static final String DOCKERFILE_AWS_CUSTOM_RUNTIME = "DockerfileNativeLambda"; 064 public static final String DOCKERFILE_AWS = "DockerfileLambda"; 065 public static final String DOCKERFILE_ORACLE_CLOUD = "DockerfileOracleCloud"; 066 public static final String DOCKERFILE_NATIVE = "DockerfileNative"; 067 public static final String DOCKERFILE_CRAC = "DockerfileCrac"; 068 public static final String DOCKERFILE_CRAC_CHECKPOINT = "DockerfileCracCheckpoint"; 069 public static final String DOCKERFILE_CRAC_CHECKPOINT_FILE = "Dockerfile.crac.checkpoint"; 070 public static final String DOCKERFILE_NATIVE_DISTROLESS = "DockerfileNativeDistroless"; 071 public static final String DOCKERFILE_NATIVE_STATIC = "DockerfileNativeStatic"; 072 public static final String DOCKERFILE_NATIVE_ORACLE_CLOUD = "DockerfileNativeOracleCloud"; 073 public static final String NATIVE_BUILD_TOOLS_MAVEN_PLUGIN = "org.graalvm.buildtools:native-maven-plugin"; 074 private static final String CLASS_NAME_PLACEHOLDER = "CLASS_NAME"; 075 private static final String EXEC_MAIN_CLASS_SOURCE = "exec.mainClass"; 076 077 private final ExecutorService executorService; 078 079 @Inject 080 public DockerfileMojo(MavenProject mavenProject, DockerService dockerService, JibConfigurationService jibConfigurationService, 081 ApplicationConfigurationService applicationConfigurationService, ExecutorService executorService, 082 MavenSession mavenSession, MojoExecution mojoExecution) { 083 super(mavenProject, jibConfigurationService, applicationConfigurationService, dockerService, mavenSession, mojoExecution); 084 this.executorService = executorService; 085 } 086 087 @Override 088 public void execute() throws MojoExecutionException { 089 var runtime = MicronautRuntime.valueOf(micronautRuntime.toUpperCase()); 090 var packaging = Packaging.of(mavenProject.getPackaging()); 091 try { 092 copyDependencies(); 093 var dockerfile = switch (packaging) { 094 case DOCKER_NATIVE -> buildDockerfileNative(runtime); 095 case DOCKER -> buildDockerfile(runtime); 096 case DOCKER_CRAC -> buildCracDockerfile(runtime); 097 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() + "]"); 098 }; 099 100 dockerfile.ifPresent(file -> getLog().info("Dockerfile written to: " + file.getAbsolutePath())); 101 102 } catch (IOException | MavenInvocationException e) { 103 throw new MojoExecutionException(e.getMessage(), e); 104 } 105 } 106 107 private Optional<File> buildDockerfile(MicronautRuntime runtime) throws IOException, MojoExecutionException { 108 File dockerfile; 109 switch (runtime.getBuildStrategy()) { 110 case ORACLE_FUNCTION -> { 111 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_ORACLE_CLOUD); 112 oracleCloudFunctionCmd(dockerfile); 113 processOracleFunctionDockerfile(dockerfile); 114 } 115 case LAMBDA -> { 116 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_AWS); 117 processDockerfile(dockerfile); 118 } 119 case DEFAULT -> { 120 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE); 121 processDockerfile(dockerfile); 122 } 123 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 124 } 125 return Optional.ofNullable(dockerfile); 126 } 127 128 private Optional<File> buildCracDockerfile(MicronautRuntime runtime) throws IOException, MojoExecutionException { 129 File dockerfile; 130 switch (runtime.getBuildStrategy()) { 131 case ORACLE_FUNCTION -> throw new MojoExecutionException("Oracle Functions are currently unsupported"); 132 case LAMBDA -> throw new MojoExecutionException("Lambda Functions are currently unsupported"); 133 case DEFAULT -> { 134 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_CRAC_CHECKPOINT, DOCKERFILE_CRAC_CHECKPOINT_FILE); 135 processDockerfile(dockerfile); 136 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_CRAC); 137 processDockerfile(dockerfile); 138 } 139 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 140 } 141 return Optional.ofNullable(dockerfile); 142 } 143 144 static void processOracleFunctionDockerfile(File dockerfile) throws IOException { 145 if (dockerfile != null) { 146 var allLines = Files.readAllLines(dockerfile.toPath()); 147 String projectFnVersion = JibMicronautExtension.determineProjectFnVersion(System.getProperty("java.version")); 148 allLines.add(0, allLines.remove(0) + projectFnVersion); 149 String entrypoint = JibMicronautExtension.buildProjectFnEntrypoint() 150 .stream() 151 .map(s -> "\"" + s + "\"") 152 .collect(Collectors.joining(", ")); 153 154 allLines.add("ENTRYPOINT [" + entrypoint + "]"); 155 156 Files.write(dockerfile.toPath(), allLines); 157 } 158 } 159 160 private Optional<File> buildDockerfileNative(MicronautRuntime runtime) throws IOException, MavenInvocationException, MojoExecutionException { 161 getLog().info("Generating GraalVM args file"); 162 executorService.invokeGoal(NATIVE_BUILD_TOOLS_MAVEN_PLUGIN, "write-args-file"); 163 File dockerfile; 164 switch (runtime.getBuildStrategy()) { 165 case LAMBDA -> { 166 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_AWS_CUSTOM_RUNTIME); 167 lambdaBootstrapCommand(dockerfile); 168 } 169 case ORACLE_FUNCTION -> { 170 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_NATIVE_ORACLE_CLOUD); 171 oracleCloudFunctionCmd(dockerfile); 172 } 173 case DEFAULT -> { 174 String dockerfileName = DOCKERFILE_NATIVE; 175 if (Boolean.TRUE.equals(staticNativeImage)) { 176 getLog().info("Generating a static native image"); 177 dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_STATIC; 178 } else if (baseImageRun.contains("distroless")) { 179 getLog().info("Generating a mostly static native image"); 180 dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_DISTROLESS; 181 } 182 dockerfile = dockerService.loadDockerfileAsResource(dockerfileName); 183 } 184 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 185 } 186 processDockerfile(dockerfile); 187 return Optional.ofNullable(dockerfile); 188 } 189 190 private void processDockerfile(File dockerfile) throws IOException, MojoExecutionException { 191 if (dockerfile == null) { 192 return; 193 } 194 195 var allLines = Files.readAllLines(dockerfile.toPath()); 196 Files.write(dockerfile.toPath(), processDockerfileLines(allLines)); 197 198 String argsFile = findArgsFile(); 199 if (argsFile != null) { 200 processNativeImageArgs(argsFile); 201 } 202 } 203 204 private List<String> processDockerfileLines(List<String> allLines) throws MojoExecutionException { 205 var result = new ArrayList<String>(); 206 for (String line : allLines) { 207 String processedLine = processDockerfileLine(line); 208 if (processedLine != null) { 209 result.add(processedLine); 210 } 211 } 212 return result; 213 } 214 215 private String processDockerfileLine(String line) throws MojoExecutionException { 216 if (line.startsWith("ARG")) { 217 return shouldInlineArgLine(line) ? null : line; 218 } 219 if (containsPlaceholder(line, "BASE_IMAGE_RUN")) { 220 return line.replace("${BASE_IMAGE_RUN}", validateImageReference("micronaut.native-image.base-image-run", baseImageRun)); 221 } 222 if (containsPlaceholder(line, "BASE_IMAGE")) { 223 return line.replace("${BASE_IMAGE}", validateImageReference("jib.from.image", getFrom())); 224 } 225 if (containsPlaceholder(line, "BASE_JAVA_IMAGE")) { 226 return line.replace("${BASE_JAVA_IMAGE}", validateImageReference("base Java image", getBaseImage())); 227 } 228 if (containsPlaceholder(line, "GRAALVM_DOWNLOAD_URL")) { 229 return line.replace("${GRAALVM_DOWNLOAD_URL}", shellLiteral("GraalVM download URL", validateDownloadUrl("GraalVM download URL", graalVmDownloadUrl()))); 230 } 231 if (containsPlaceholder(line, CLASS_NAME_PLACEHOLDER)) { 232 return replaceClassName(line); 233 } 234 if (containsPlaceholder(line, "PORTS")) { 235 return line.replace("${PORTS}", validateExposedPorts("jib.container.ports", getPorts())); 236 } 237 return line; 238 } 239 240 private static boolean containsPlaceholder(String line, String placeholderName) { 241 return line.contains("${" + placeholderName + "}"); 242 } 243 244 private static boolean shouldInlineArgLine(String line) { 245 String trimmed = line.trim(); 246 if (!trimmed.startsWith("ARG")) { 247 return false; 248 } 249 String remainder = trimmed.substring(3).trim(); 250 if (remainder.isEmpty()) { 251 return false; 252 } 253 254 int endEquals = remainder.indexOf('='); 255 int endWhitespace = -1; 256 for (int i = 0; i < remainder.length(); i++) { 257 if (Character.isWhitespace(remainder.charAt(i))) { 258 endWhitespace = i; 259 break; 260 } 261 } 262 263 int end = remainder.length(); 264 if (endEquals >= 0) { 265 end = Math.min(end, endEquals); 266 } 267 if (endWhitespace >= 0) { 268 end = Math.min(end, endWhitespace); 269 } 270 271 String argName = remainder.substring(0, end); 272 return "BASE_IMAGE_RUN".equals(argName) 273 || "BASE_JAVA_IMAGE".equals(argName) 274 || "BASE_IMAGE".equals(argName) 275 || "GRAALVM_DOWNLOAD_URL".equals(argName) 276 || CLASS_NAME_PLACEHOLDER.equals(argName) 277 || "PORTS".equals(argName); 278 } 279 280 private String replaceClassName(String line) throws MojoExecutionException { 281 String className = isJsonArrayClassNameContext(line) 282 ? escapeJsonString(EXEC_MAIN_CLASS_SOURCE, mainClass) 283 : line.contains("\"${CLASS_NAME}\"") 284 ? escapeShellDoubleQuoted(EXEC_MAIN_CLASS_SOURCE, mainClass) 285 : shellLiteral(EXEC_MAIN_CLASS_SOURCE, mainClass); 286 return line.replace("${CLASS_NAME}", className); 287 } 288 289 private static boolean isJsonArrayClassNameContext(String line) { 290 return containsPlaceholder(line, CLASS_NAME_PLACEHOLDER) 291 && (line.contains("ENTRYPOINT [") || line.contains("CMD [")); 292 } 293 294 private String findArgsFile() throws IOException { 295 String argsFile = mavenProject.getProperties().getProperty(ARGS_FILE_PROPERTY_NAME); 296 if (argsFile != null) { 297 return argsFile; 298 } 299 Path targetPath = Paths.get(mavenProject.getBuild().getDirectory()); 300 try (Stream<Path> listStream = Files.list(targetPath)) { 301 return listStream 302 .filter(path -> { 303 String fileName = path.getFileName().toString(); 304 return fileName.startsWith("native-image") && fileName.endsWith("args"); 305 }) 306 .findFirst() 307 .map(path -> path.toAbsolutePath().toString()) 308 .orElse(null); 309 } 310 } 311 312 private void processNativeImageArgs(String argsFile) throws IOException, MojoExecutionException { 313 List<String> allNativeImageBuildArgs = MojoUtils.computeNativeImageArgs(nativeImageBuildArgs, baseImageRun, argsFile, supportsSharedArena()); 314 //Remove extra main class argument 315 allNativeImageBuildArgs.remove(mainClass); 316 getLog().info("GraalVM native image build args: " + allNativeImageBuildArgs); 317 List<String> conversionResult = NativeImageUtils.convertToArgsFile(allNativeImageBuildArgs, Paths.get(mavenProject.getBuild().getDirectory())); 318 if (conversionResult.size() == 1) { 319 Files.delete(Paths.get(argsFile)); 320 } 321 } 322}