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}