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}