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.github.dockerjava.api.exception.DockerClientException;
020import com.google.cloud.tools.jib.api.ImageReference;
021import com.google.cloud.tools.jib.api.InvalidImageReferenceException;
022import io.micronaut.core.util.StringUtils;
023import io.micronaut.maven.core.MicronautRuntime;
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.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 = 21;
065    static final int ORACLE_FUNCTION_MAX_ALLOWED_VERSION = 21;
066    static final int MAX_ALLOWED_VERSION = 21;
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 {
149        var buildImageCmdArguments = new HashMap<String, String>();
150
151        // Starter sets the right class in pom.xml:
152        //   - For applications: io.micronaut.function.aws.runtime.MicronautLambdaRuntime
153        //   - For function apps: com.example.BookLambdaRuntime
154        BuildImageCmd buildImageCmd = addNativeImageBuildArgs(buildImageCmdArguments, () -> {
155            try {
156                return dockerService.buildImageCmd(DockerfileMojo.DOCKERFILE_AWS_CUSTOM_RUNTIME)
157                    .withBuildArg("GRAALVM_DOWNLOAD_URL", graalVmDownloadUrl());
158            } catch (IOException e) {
159                throw new DockerClientException(e.getMessage(), e);
160            }
161        });
162        buildImageCmd.withBuildArg("CLASS_NAME", mainClass);
163        String imageId = dockerService.buildImage(buildImageCmd);
164        File functionZip = dockerService.copyFromContainer(imageId, "/function/function.zip");
165        getLog().info("AWS Lambda Custom Runtime ZIP: " + functionZip.getPath());
166    }
167
168    private void buildDockerNative() throws IOException, InvalidImageReferenceException {
169        String dockerfileName = DockerfileMojo.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
178        buildDockerfile(dockerfileName, true);
179    }
180
181    private void buildOracleCloud() throws IOException, InvalidImageReferenceException {
182        buildDockerfile(DockerfileMojo.DOCKERFILE_NATIVE_ORACLE_CLOUD, false);
183    }
184
185    private void buildDockerfile(String dockerfileName, boolean passClassName) throws IOException, InvalidImageReferenceException {
186        Set<String> tags = getTags();
187        for (String tag : tags) {
188            ImageReference.parse(tag);
189        }
190
191        String from = getFrom();
192        String ports = getPorts();
193        getLog().info("Exposing port(s): " + ports);
194
195        File dockerfile = dockerService.loadDockerfileAsResource(dockerfileName);
196
197        oracleCloudFunctionCmd(dockerfile);
198
199        var buildImageCmdArguments = new HashMap<String, String>();
200
201        if (StringUtils.isNotEmpty(baseImageRun) && Boolean.FALSE.equals(staticNativeImage)) {
202            buildImageCmdArguments.put("BASE_IMAGE_RUN", baseImageRun);
203        }
204
205        if (passClassName) {
206            buildImageCmdArguments.put("CLASS_NAME", mainClass);
207        }
208
209        BuildImageCmd buildImageCmd = addNativeImageBuildArgs(buildImageCmdArguments, () -> dockerService.buildImageCmd()
210            .withDockerfile(dockerfile)
211            .withTags(getTags())
212            .withBuildArg("BASE_IMAGE", from)
213            .withBuildArg("PORTS", ports));
214
215        dockerService.buildImage(buildImageCmd);
216    }
217
218    private BuildImageCmd addNativeImageBuildArgs(Map<String, String> buildImageCmdArguments, Supplier<BuildImageCmd> buildImageCmdSupplier) throws IOException {
219        String argsFile = mavenProject.getProperties().getProperty(ARGS_FILE_PROPERTY_NAME);
220        List<String> allNativeImageBuildArgs = MojoUtils.computeNativeImageArgs(nativeImageBuildArgs, baseImageRun, argsFile);
221        //Remove extra main class argument
222        allNativeImageBuildArgs.remove(mainClass);
223        getLog().info("GraalVM native image build args: " + allNativeImageBuildArgs);
224        List<String> conversionResult = NativeImageUtils.convertToArgsFile(allNativeImageBuildArgs, Paths.get(mavenProject.getBuild().getDirectory()));
225        if (conversionResult.size() == 1) {
226            Files.delete(Paths.get(argsFile));
227
228            BuildImageCmd buildImageCmd = buildImageCmdSupplier.get();
229
230            for (Map.Entry<String, String> buildArg : buildImageCmdArguments.entrySet()) {
231                buildImageCmd.withBuildArg(buildArg.getKey(), buildArg.getValue());
232            }
233
234            getNetworkMode().ifPresent(buildImageCmd::withNetworkMode);
235            return buildImageCmd;
236        } else {
237            throw new IOException("Unable to convert native image build args to args file");
238        }
239    }
240
241}