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.jib.JibConfigurationService;
024import io.micronaut.maven.services.ApplicationConfigurationService;
025import io.micronaut.maven.services.DockerService;
026import org.apache.maven.execution.MavenSession;
027import org.apache.maven.plugin.MojoExecution;
028import org.apache.maven.plugin.MojoExecutionException;
029import org.apache.maven.plugins.annotations.Mojo;
030import org.apache.maven.plugins.annotations.ResolutionScope;
031import org.apache.maven.project.MavenProject;
032import org.graalvm.buildtools.utils.NativeImageUtils;
033
034import javax.inject.Inject;
035import java.io.File;
036import java.io.IOException;
037import java.nio.file.Files;
038import java.nio.file.Paths;
039import java.nio.file.StandardCopyOption;
040import java.util.HashMap;
041import java.util.List;
042import java.util.Map;
043import java.util.Set;
044import java.util.function.Supplier;
045
046/**
047 * <p>Implementation of the <code>docker-native</code> packaging.</p>
048 * <p><strong>WARNING</strong>: this goal is not intended to be executed directly. Instead, specify the packaging type
049 * using the <code>packaging</code> property, eg:</p>
050 *
051 * <pre>mvn package -Dpackaging=docker-native</pre>
052 *
053 * @author Álvaro Sánchez-Mariscal
054 * @author Iván López
055 * @since 1.1
056 */
057@Mojo(name = DockerNativeMojo.DOCKER_NATIVE_PACKAGING, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
058public class DockerNativeMojo extends AbstractDockerMojo {
059
060    public static final String DOCKER_NATIVE_PACKAGING = "docker-native";
061    public static final String MICRONAUT_PARENT = "io.micronaut.platform:micronaut-parent";
062    public static final String MICRONAUT_VERSION = "micronaut.version";
063    public static final String ARGS_FILE_PROPERTY_NAME = "graalvm.native-image.args-file";
064    static final int AWS_LAMBDA_MAX_ALLOWED_VERSION = 25;
065    static final int ORACLE_FUNCTION_MAX_ALLOWED_VERSION = 25;
066    static final int MAX_ALLOWED_VERSION = 25;
067    private MicronautRuntime runtime;
068
069    @SuppressWarnings("CdiInjectionPointsInspection")
070    @Inject
071    public DockerNativeMojo(MavenProject mavenProject, JibConfigurationService jibConfigurationService,
072                            ApplicationConfigurationService applicationConfigurationService, DockerService dockerService,
073                            MavenSession mavenSession, MojoExecution mojoExecution) {
074        super(mavenProject, jibConfigurationService, applicationConfigurationService, dockerService, mavenSession, mojoExecution);
075    }
076
077    @Override
078    public void execute() throws MojoExecutionException {
079        checkGraalVm();
080
081        try {
082            copyDependencies();
083
084            this.runtime = MicronautRuntime.valueOf(micronautRuntime.toUpperCase());
085
086            switch (runtime.getBuildStrategy()) {
087                case LAMBDA -> {
088                    checkJavaVersion(AWS_LAMBDA_MAX_ALLOWED_VERSION);
089                    buildDockerNativeLambda();
090                }
091                case ORACLE_FUNCTION -> {
092                    checkJavaVersion(ORACLE_FUNCTION_MAX_ALLOWED_VERSION);
093                    buildOracleCloud();
094                }
095                case DEFAULT -> {
096                    checkJavaVersion(MAX_ALLOWED_VERSION);
097                    buildDockerNative();
098                }
099                default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy());
100            }
101
102
103        } catch (InvalidImageReferenceException iire) {
104            String message = "Invalid image reference "
105                + iire.getInvalidReference()
106                + ", perhaps you should check that the reference is formatted correctly according to " +
107                "https://docs.docker.com/engine/reference/commandline/tag/#extended-description" +
108                "\nFor example, slash-separated name components cannot have uppercase letters";
109            throw new MojoExecutionException(message);
110        } catch (IOException | IllegalArgumentException e) {
111            throw new MojoExecutionException(e.getMessage(), e);
112        }
113    }
114
115    private void checkGraalVm() throws MojoExecutionException {
116        String micronautVersion = mavenProject.getProperties().getProperty(MICRONAUT_VERSION);
117        if (mavenProject.hasParent()) {
118            String ga = mavenProject.getParent().getGroupId() + ":" + mavenProject.getParent().getArtifactId();
119            if (MICRONAUT_PARENT.equals(ga)) {
120                String micronautParentVersion = mavenProject.getModel().getParent().getVersion();
121                if (micronautVersion.equals(micronautParentVersion)) {
122                    if (!mavenProject.getInjectedProfileIds().get(MICRONAUT_PARENT + ":" + micronautParentVersion).contains("graalvm")) {
123                        String javaVendor = System.getProperty("java.vendor", "");
124                        if (javaVendor.toLowerCase().contains("graalvm")) {
125                            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");
126                        } else {
127                            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");
128                        }
129                    }
130                } else {
131                    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);
132                    throw new MojoExecutionException(message);
133                }
134            } else {
135                getLog().warn("The parent POM of this project is not set to " + MICRONAUT_PARENT);
136            }
137        } else {
138            getLog().warn("This project has no parent POM defined. To avoid build problems, please set the parent to " + MICRONAUT_PARENT);
139        }
140    }
141
142    private void checkJavaVersion(int maxAllowedVersion) throws MojoExecutionException {
143        if (javaVersion().getMajorVersion() > maxAllowedVersion) {
144            throw new MojoExecutionException("To build native images you must set the Java target byte code level to Java %s or below".formatted(maxAllowedVersion));
145        }
146    }
147
148    private void buildDockerNativeLambda() throws IOException, MojoExecutionException {
149        var buildImageCmdArguments = new HashMap<String, String>();
150
151        // Add proxy settings if configured
152        buildImageCmdArguments.putAll(getProxyBuildArgs());
153
154        File dockerfile = dockerService.loadDockerfileAsResource(DockerfileMojo.DOCKERFILE_AWS_CUSTOM_RUNTIME);
155        lambdaBootstrapCommand(dockerfile);
156
157        // Starter sets the right class in pom.xml:
158        //   - For applications: io.micronaut.function.aws.runtime.MicronautLambdaRuntime
159        //   - For function apps: com.example.BookLambdaRuntime
160        BuildImageCmd buildImageCmd = addNativeImageBuildArgs(buildImageCmdArguments, () -> dockerService.buildImageCmd()
161            .withDockerfile(dockerfile)
162            .withBuildArg("GRAALVM_DOWNLOAD_URL", graalVmDownloadUrl()));
163        buildImageCmd.withBuildArg("CLASS_NAME", escapeClassNameBuildArg(mainClass));
164        String imageId = dockerService.buildImage(buildImageCmd);
165        File functionZip = dockerService.copyFromContainer(imageId, "/function/function.zip");
166        getLog().info("AWS Lambda Custom Runtime ZIP: " + functionZip.getPath());
167    }
168
169    private void buildDockerNative() throws IOException, InvalidImageReferenceException, MojoExecutionException {
170        String dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE;
171        if (Boolean.TRUE.equals(staticNativeImage)) {
172            getLog().info("Generating a static native image");
173            dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_STATIC;
174        } else if (baseImageRun.contains("distroless")) {
175            getLog().info("Generating a mostly static native image");
176            dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_DISTROLESS;
177        }
178
179        buildDockerfile(dockerfileName, true);
180    }
181
182    private void buildOracleCloud() throws IOException, InvalidImageReferenceException, MojoExecutionException {
183        buildDockerfile(DockerfileMojo.DOCKERFILE_NATIVE_ORACLE_CLOUD, false);
184    }
185
186    private void buildDockerfile(String dockerfileName, boolean passClassName) throws IOException, InvalidImageReferenceException, MojoExecutionException {
187        Set<String> tags = getTags();
188        for (String tag : tags) {
189            ImageReference.parse(tag);
190        }
191
192        String from = getFrom();
193        String ports = getPorts();
194        getLog().info("Exposing port(s): " + ports);
195
196        File providedDockerfile = new File(mavenProject.getBasedir(), DockerfileMojo.DOCKERFILE);
197        if (providedDockerfile.isFile()) {
198            buildProvidedDockerfile(providedDockerfile, tags, passClassName, from, ports);
199            return;
200        }
201
202        File dockerfile = dockerService.loadDockerfileAsResource(dockerfileName);
203        if (DockerfileMojo.DOCKERFILE_NATIVE_ORACLE_CLOUD.equals(dockerfileName)) {
204            oracleCloudFunctionCmd(dockerfile);
205        }
206
207        BuildImageCmd buildImageCmd = addNativeImageBuildArgs(buildImageCmdArguments(passClassName), () -> dockerService.buildImageCmd()
208            .withDockerfile(dockerfile)
209            .withTags(tags)
210            .withBuildArg("BASE_IMAGE", from)
211            .withBuildArg("PORTS", ports));
212
213        dockerService.buildImage(buildImageCmd);
214    }
215
216    private void buildProvidedDockerfile(File providedDockerfile, Set<String> tags, boolean passClassName, String from, String ports) throws IOException, MojoExecutionException {
217        getLog().info("Using Dockerfile: " + providedDockerfile.getAbsolutePath());
218
219        File targetDir = new File(mavenProject.getBuild().getDirectory());
220        Files.createDirectories(targetDir.toPath());
221        File targetDockerfile = new File(targetDir, providedDockerfile.getName());
222        Files.copy(providedDockerfile.toPath(), targetDockerfile.toPath(), StandardCopyOption.REPLACE_EXISTING);
223
224        BuildImageCmd buildImageCmd = addNativeImageBuildArgs(buildImageCmdArguments(passClassName), () -> dockerService.buildImageCmd()
225            .withDockerfile(targetDockerfile)
226            .withTags(tags)
227            .withBaseDirectory(targetDir)
228            .withBuildArg("BASE_IMAGE", from)
229            .withBuildArg("PORTS", ports));
230
231        dockerService.buildImage(buildImageCmd);
232    }
233
234    private Map<String, String> buildImageCmdArguments(boolean passClassName) throws MojoExecutionException {
235        var buildImageCmdArguments = new HashMap<String, String>();
236
237        // Add proxy settings if configured
238        buildImageCmdArguments.putAll(getProxyBuildArgs());
239
240        if (StringUtils.isNotEmpty(baseImageRun) && Boolean.FALSE.equals(staticNativeImage)) {
241            buildImageCmdArguments.put("BASE_IMAGE_RUN", baseImageRun);
242        }
243
244        if (passClassName) {
245            buildImageCmdArguments.put("CLASS_NAME", escapeClassNameBuildArg(mainClass));
246        }
247        return buildImageCmdArguments;
248    }
249
250    private BuildImageCmd addNativeImageBuildArgs(Map<String, String> buildImageCmdArguments, Supplier<BuildImageCmd> buildImageCmdSupplier) throws IOException {
251        String argsFile = mavenProject.getProperties().getProperty(ARGS_FILE_PROPERTY_NAME);
252        List<String> allNativeImageBuildArgs = MojoUtils.computeNativeImageArgs(nativeImageBuildArgs, baseImageRun, argsFile);
253        //Remove extra main class argument
254        allNativeImageBuildArgs.remove(mainClass);
255        getLog().info("GraalVM native image build args: " + allNativeImageBuildArgs);
256        List<String> conversionResult = NativeImageUtils.convertToArgsFile(allNativeImageBuildArgs, Paths.get(mavenProject.getBuild().getDirectory()));
257        if (conversionResult.size() == 1) {
258            Files.delete(Paths.get(argsFile));
259
260            BuildImageCmd buildImageCmd = buildImageCmdSupplier.get();
261
262            for (Map.Entry<String, String> buildArg : buildImageCmdArguments.entrySet()) {
263                buildImageCmd.withBuildArg(buildArg.getKey(), buildArg.getValue());
264            }
265
266            getNetworkMode().ifPresent(buildImageCmd::withNetworkMode);
267            return buildImageCmd;
268        } else {
269            throw new IOException("Unable to convert native image build args to args file");
270        }
271    }
272
273    static String escapeClassNameBuildArg(String value) throws MojoExecutionException {
274        return escapeShellDoubleQuoted("exec.mainClass", value);
275    }
276
277}