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.jib.JibConfigurationService; 020import io.micronaut.maven.jib.JibMicronautExtension; 021import io.micronaut.maven.services.ApplicationConfigurationService; 022import io.micronaut.maven.services.DockerService; 023import io.micronaut.maven.services.ExecutorService; 024import org.apache.maven.execution.MavenSession; 025import org.apache.maven.plugin.MojoExecution; 026import org.apache.maven.plugin.MojoExecutionException; 027import org.apache.maven.plugins.annotations.Execute; 028import org.apache.maven.plugins.annotations.LifecyclePhase; 029import org.apache.maven.plugins.annotations.Mojo; 030import org.apache.maven.plugins.annotations.ResolutionScope; 031import org.apache.maven.project.MavenProject; 032import org.apache.maven.shared.invoker.MavenInvocationException; 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.Path; 040import java.nio.file.Paths; 041import java.util.ArrayList; 042import java.util.List; 043import java.util.Optional; 044import java.util.stream.Collectors; 045import java.util.stream.Stream; 046 047import static io.micronaut.maven.DockerNativeMojo.ARGS_FILE_PROPERTY_NAME; 048 049/** 050 * <p>Generates a <code>Dockerfile</code> depending on the <code>packaging</code> and <code>micronaut.runtime</code> 051 * properties. 052 * 053 * <pre>mvn mn:dockerfile -Dpackaging=docker-native -Dmicronaut.runtime=lambda</pre> 054 * 055 * @author Álvaro Sánchez-Mariscal 056 * @since 1.1 057 */ 058@Mojo(name = "dockerfile", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) 059@Execute(phase = LifecyclePhase.PROCESS_CLASSES) 060public class DockerfileMojo extends AbstractDockerMojo { 061 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 075 private final ExecutorService executorService; 076 077 @Inject 078 public DockerfileMojo(MavenProject mavenProject, DockerService dockerService, JibConfigurationService jibConfigurationService, 079 ApplicationConfigurationService applicationConfigurationService, ExecutorService executorService, 080 MavenSession mavenSession, MojoExecution mojoExecution) { 081 super(mavenProject, jibConfigurationService, applicationConfigurationService, dockerService, mavenSession, mojoExecution); 082 this.executorService = executorService; 083 } 084 085 @Override 086 public void execute() throws MojoExecutionException { 087 var runtime = MicronautRuntime.valueOf(micronautRuntime.toUpperCase()); 088 var packaging = Packaging.of(mavenProject.getPackaging()); 089 try { 090 copyDependencies(); 091 Optional<File> dockerfile = switch (packaging) { 092 case DOCKER_NATIVE -> buildDockerfileNative(runtime); 093 case DOCKER -> buildDockerfile(runtime); 094 case DOCKER_CRAC -> buildCracDockerfile(runtime); 095 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() + "]"); 096 }; 097 098 dockerfile.ifPresent(file -> getLog().info("Dockerfile written to: " + file.getAbsolutePath())); 099 100 } catch (IOException | MavenInvocationException e) { 101 throw new MojoExecutionException(e.getMessage(), e); 102 } 103 } 104 105 private Optional<File> buildDockerfile(MicronautRuntime runtime) throws IOException { 106 File dockerfile; 107 switch (runtime.getBuildStrategy()) { 108 case ORACLE_FUNCTION -> { 109 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_ORACLE_CLOUD); 110 oracleCloudFunctionCmd(dockerfile); 111 processOracleFunctionDockerfile(dockerfile); 112 } 113 case LAMBDA -> { 114 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_AWS); 115 processDockerfile(dockerfile); 116 } 117 case DEFAULT -> { 118 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE); 119 processDockerfile(dockerfile); 120 } 121 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 122 } 123 return Optional.ofNullable(dockerfile); 124 } 125 126 private Optional<File> buildCracDockerfile(MicronautRuntime runtime) throws IOException, MojoExecutionException { 127 File dockerfile; 128 switch (runtime.getBuildStrategy()) { 129 case ORACLE_FUNCTION -> throw new MojoExecutionException("Oracle Functions are currently unsupported"); 130 case LAMBDA -> throw new MojoExecutionException("Lambda Functions are currently unsupported"); 131 case DEFAULT -> { 132 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_CRAC_CHECKPOINT, DOCKERFILE_CRAC_CHECKPOINT_FILE); 133 processDockerfile(dockerfile); 134 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_CRAC); 135 processDockerfile(dockerfile); 136 } 137 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 138 } 139 return Optional.ofNullable(dockerfile); 140 } 141 142 static void processOracleFunctionDockerfile(File dockerfile) throws IOException { 143 if (dockerfile != null) { 144 var allLines = Files.readAllLines(dockerfile.toPath()); 145 String projectFnVersion = JibMicronautExtension.determineProjectFnVersion(System.getProperty("java.version")); 146 allLines.add(0, allLines.remove(0) + projectFnVersion); 147 String entrypoint = JibMicronautExtension.buildProjectFnEntrypoint() 148 .stream() 149 .map(s -> "\"" + s + "\"") 150 .collect(Collectors.joining(", ")); 151 152 allLines.add("ENTRYPOINT [" + entrypoint + "]"); 153 154 Files.write(dockerfile.toPath(), allLines); 155 } 156 } 157 158 private Optional<File> buildDockerfileNative(MicronautRuntime runtime) throws IOException, MavenInvocationException { 159 getLog().info("Generating GraalVM args file"); 160 executorService.invokeGoal(NATIVE_BUILD_TOOLS_MAVEN_PLUGIN, "write-args-file"); 161 File dockerfile; 162 switch (runtime.getBuildStrategy()) { 163 case LAMBDA -> dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_AWS_CUSTOM_RUNTIME); 164 case ORACLE_FUNCTION -> { 165 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_NATIVE_ORACLE_CLOUD); 166 oracleCloudFunctionCmd(dockerfile); 167 } 168 case DEFAULT -> { 169 String dockerfileName = DOCKERFILE_NATIVE; 170 if (Boolean.TRUE.equals(staticNativeImage)) { 171 getLog().info("Generating a static native image"); 172 dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_STATIC; 173 } else if (baseImageRun.contains("distroless")) { 174 getLog().info("Generating a mostly static native image"); 175 dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_DISTROLESS; 176 } 177 dockerfile = dockerService.loadDockerfileAsResource(dockerfileName); 178 } 179 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 180 } 181 processDockerfile(dockerfile); 182 return Optional.ofNullable(dockerfile); 183 } 184 185 private void processDockerfile(File dockerfile) throws IOException { 186 187 if (dockerfile != null) { 188 var allLines = Files.readAllLines(dockerfile.toPath()); 189 var result = new ArrayList<String>(); 190 for (String line : allLines) { 191 if (!line.startsWith("ARG")) { 192 if (line.contains("BASE_IMAGE_RUN")) { 193 result.add(line.replace("${BASE_IMAGE_RUN}", baseImageRun)); 194 } else if (line.contains("BASE_IMAGE")) { 195 result.add(line.replace("${BASE_IMAGE}", getFrom())); 196 } else if (line.contains("BASE_JAVA_IMAGE")) { 197 result.add(line.replace("${BASE_JAVA_IMAGE}", getBaseImage())); 198 } else if (line.contains("GRAALVM_DOWNLOAD_URL")) { 199 result.add(line.replace("${GRAALVM_DOWNLOAD_URL}", graalVmDownloadUrl())); 200 } else if (line.contains("CLASS_NAME")) { 201 result.add(line.replace("${CLASS_NAME}", mainClass)); 202 } else if (line.contains("PORTS")) { 203 result.add(line.replace("${PORTS}", getPorts())); 204 } else { 205 result.add(line); 206 } 207 } 208 } 209 210 String argsFile = mavenProject.getProperties().getProperty(ARGS_FILE_PROPERTY_NAME); 211 if (argsFile == null) { 212 Path targetPath = Paths.get(mavenProject.getBuild().getDirectory()); 213 try (Stream<Path> listStream = Files.list(targetPath)) { 214 Path argsFilePath = listStream 215 .map(path -> path.getFileName().toString()) 216 .filter(f -> f.startsWith("native-image") && f.endsWith("args")) 217 .map(targetPath::resolve) 218 .findFirst() 219 .orElse(null); 220 if (argsFilePath != null) { 221 argsFile = argsFilePath.toAbsolutePath().toString(); 222 } 223 } 224 } 225 if (argsFile != null) { 226 List<String> allNativeImageBuildArgs = MojoUtils.computeNativeImageArgs(nativeImageBuildArgs, baseImageRun, argsFile); 227 //Remove extra main class argument 228 allNativeImageBuildArgs.remove(mainClass); 229 getLog().info("GraalVM native image build args: " + allNativeImageBuildArgs); 230 List<String> conversionResult = NativeImageUtils.convertToArgsFile(allNativeImageBuildArgs, Paths.get(mavenProject.getBuild().getDirectory())); 231 if (conversionResult.size() == 1) { 232 Files.delete(Paths.get(argsFile)); 233 } 234 } 235 236 Files.write(dockerfile.toPath(), result); 237 } 238 } 239}