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.google.common.io.FileWriteMode;
019import io.micronaut.maven.core.MicronautRuntime;
020import io.micronaut.maven.jib.JibConfigurationService;
021import io.micronaut.maven.jib.JibMicronautExtension;
022import io.micronaut.maven.services.ApplicationConfigurationService;
023import io.micronaut.maven.services.DockerService;
024import org.apache.maven.artifact.Artifact;
025import org.apache.maven.artifact.versioning.ArtifactVersion;
026import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
027import org.apache.maven.execution.MavenSession;
028import org.apache.maven.plugin.MojoExecution;
029import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
030import org.apache.maven.plugins.annotations.Parameter;
031import org.apache.maven.project.MavenProject;
032
033import java.io.File;
034import java.io.IOException;
035import java.nio.charset.Charset;
036import java.nio.file.Files;
037import java.nio.file.StandardCopyOption;
038import java.util.Arrays;
039import java.util.HashSet;
040import java.util.List;
041import java.util.Optional;
042import java.util.Set;
043import java.util.stream.Collectors;
044
045import static io.micronaut.maven.services.ApplicationConfigurationService.DEFAULT_PORT;
046
047/**
048 * Abstract base class for mojos related to Docker files and builds.
049 *
050 * @author Álvaro Sánchez-Mariscal
051 * @author Iván López
052 * @since 1.1
053 */
054public abstract class AbstractDockerMojo extends AbstractMicronautMojo {
055
056    public static final String LATEST_TAG = "latest";
057    // GlibC 2.34 is used by native image 17
058    public static final String DEFAULT_BASE_IMAGE_GRAALVM_RUN = "cgr.dev/chainguard/wolfi-base:latest";
059    public static final String MOSTLY_STATIC_NATIVE_IMAGE_GRAALVM_FLAG = "-H:+StaticExecutableWithDynamicLibC";
060    public static final String ARM_ARCH = "aarch64";
061    public static final String X86_64_ARCH = "x64";
062    public static final String DEFAULT_ORACLE_LINUX_VERSION = "ol9";
063    public static final String ORACLE_CLOUD_FUNCTION_DEFAULT_CMD = "CMD [\"io.micronaut.oraclecloud.function.http.HttpFunction::handleRequest\"]";
064    public static final String GRAALVM_DOWNLOAD_URL = "https://download.oracle.com/graalvm/%s/%s/graalvm-jdk-%s_linux-%s_bin.tar.gz";
065
066    //Latest version of GraalVM for JDK 17 available under the GraalVM Free Terms and Conditions (GFTC) licence
067    public static final String GRAALVM_FOR_JDK17 = "17.0.12";
068
069    protected final MavenProject mavenProject;
070    protected final JibConfigurationService jibConfigurationService;
071    protected final ApplicationConfigurationService applicationConfigurationService;
072    protected final DockerService dockerService;
073    protected final PluginParameterExpressionEvaluator expressionEvaluator;
074
075
076    /**
077     * Additional arguments that will be passed to the <code>native-image</code> executable. Note that this will only
078     * be used when using a packaging of type <code>docker-native</code>. For <code>native-image</code> packaging
079     * you should use the
080     * <a href="https://www.graalvm.org/reference-manual/native-image/NativeImageMavenPlugin/#maven-plugin-customization">
081     * Native Image Maven Plugin
082     * </a> configuration options.
083     */
084    @Parameter(property = "micronaut.native-image.args")
085    protected List<String> nativeImageBuildArgs;
086
087    /**
088     * List of additional arguments that will be passed to the application.
089     */
090    @Parameter(property = RunMojo.MN_APP_ARGS)
091    protected List<String> appArguments;
092
093    /**
094     * The main class of the application, as defined in the
095     * <a href="https://www.mojohaus.org/exec-maven-plugin/java-mojo.html#mainClass">Exec Maven Plugin</a>.
096     */
097    @Parameter(defaultValue = RunMojo.EXEC_MAIN_CLASS, required = true)
098    protected String mainClass;
099
100    /**
101     * Whether to produce a static native image when using <code>docker-native</code> packaging.
102     */
103    @Parameter(defaultValue = "false", property = "micronaut.native-image.static")
104    protected Boolean staticNativeImage;
105
106    /**
107     * The target runtime of the application.
108     */
109    @Parameter(property = MicronautRuntime.PROPERTY, defaultValue = "NONE")
110    protected String micronautRuntime;
111
112    /**
113     * The Docker image used to run the native image.
114     *
115     * @since 1.2
116     */
117    @Parameter(property = "micronaut.native-image.base-image-run", defaultValue = DEFAULT_BASE_IMAGE_GRAALVM_RUN)
118    protected String baseImageRun;
119
120    /**
121     * The version of Oracle Linux to use as a native-compile base when building a native image inside a Docker container.
122     */
123    @Parameter(property = "micronaut.native-image.ol.version", defaultValue = DEFAULT_ORACLE_LINUX_VERSION)
124    protected String oracleLinuxVersion;
125
126    /**
127     * Networking mode for the RUN instructions during build.
128     *
129     * @since 4.0.0
130     */
131    @Parameter(property = "docker.networkMode")
132    protected String networkMode;
133
134    protected AbstractDockerMojo(MavenProject mavenProject, JibConfigurationService jibConfigurationService,
135                                 ApplicationConfigurationService applicationConfigurationService,
136                                 DockerService dockerService, MavenSession mavenSession, MojoExecution mojoExecution) {
137        this.mavenProject = mavenProject;
138        this.jibConfigurationService = jibConfigurationService;
139        this.applicationConfigurationService = applicationConfigurationService;
140        this.dockerService = dockerService;
141        this.expressionEvaluator = new PluginParameterExpressionEvaluator(mavenSession, mojoExecution);
142    }
143
144    /**
145     * @return the Java version from either the <code>maven.compiler.target</code> property or the <code>java.version</code> property.
146     */
147    protected ArtifactVersion javaVersion() {
148        return new DefaultArtifactVersion(Optional.ofNullable(mavenProject.getProperties().getProperty("maven.compiler.target")).orElse(System.getProperty("java.version")));
149    }
150
151    /**
152     * @return the JVM version to use for GraalVM.
153     */
154    protected String graalVmJvmVersion() {
155        return javaVersion().getMajorVersion() == 17 ? "17" : "21";
156    }
157
158    /**
159     * @return the GraalVM download URL depending on the Java version.
160     */
161    protected String graalVmDownloadUrl() {
162        if (javaVersion().getMajorVersion() == 17) {
163            return GRAALVM_DOWNLOAD_URL.formatted(17, "archive", GRAALVM_FOR_JDK17, graalVmArch());
164        } else {
165            return GRAALVM_DOWNLOAD_URL.formatted(21, "latest", 21, graalVmArch());
166        }
167    }
168
169    /**
170     * @return the OS architecture to use for GraalVM depending on the <code>os.arch</code> system property.
171     */
172    protected String graalVmArch() {
173        return isArm() ? ARM_ARCH : X86_64_ARCH;
174    }
175
176    /**
177     * @return the base FROM image for the native image.
178     */
179    protected String getFrom() {
180        if (Boolean.TRUE.equals(staticNativeImage)) {
181            return getFromImage().orElse("ghcr.io/graalvm/native-image-community:" + graalVmJvmVersion() + "-muslib-" + oracleLinuxVersion);
182        } else {
183            return getFromImage().orElse("ghcr.io/graalvm/native-image-community:" + graalVmJvmVersion() + "-" + oracleLinuxVersion);
184        }
185    }
186
187    /**
188     * Check os.arch against known ARM architecture identifiers.
189     *
190     * @return true if we think we're running on an arm JDK
191     */
192    protected boolean isArm() {
193        return switch (System.getProperty("os.arch")) {
194            case ARM_ARCH, "arm64" -> true;
195            default -> false;
196        };
197    }
198
199    /**
200     * @return the base image from the jib configuration (if any).
201     */
202    protected Optional<String> getFromImage() {
203        return jibConfigurationService.getFromImage();
204    }
205
206    /**
207     * @return the Docker image tags by looking at the Jib plugin configuration.
208     */
209    protected Set<String> getTags() {
210        var tags = new HashSet<String>();
211        Optional<String> toImageOptional = jibConfigurationService.getToImage();
212        String imageName = mavenProject.getArtifactId();
213        if (toImageOptional.isPresent()) {
214            String toImage = toImageOptional.get();
215            if (toImage.contains(":")) {
216                tags.add(toImage);
217                imageName = toImageOptional.get().split(":")[0];
218            } else {
219                tags.add(toImage + ":" + LATEST_TAG);
220                imageName = toImage;
221            }
222        } else {
223            tags.add(imageName + ":" + LATEST_TAG);
224        }
225        for (String tag : jibConfigurationService.getTags()) {
226            if (LATEST_TAG.equals(tag) && tags.stream().anyMatch(t -> t.contains(LATEST_TAG))) {
227                continue;
228            }
229            tags.add(String.format("%s:%s", imageName, tag));
230        }
231        return tags.stream()
232            .map(this::evaluateExpression)
233            .collect(Collectors.toSet());
234    }
235
236    private String evaluateExpression(String expression) {
237        try {
238            return expressionEvaluator.evaluate(expression, String.class).toString();
239        } catch (Exception e) {
240            return expression;
241        }
242    }
243
244    /**
245     * @return the application ports to expose by looking at the Jib configuration or the application configuration.
246     */
247    protected String getPorts() {
248        return jibConfigurationService.getPorts().orElseGet(() -> {
249            String port = applicationConfigurationService.getServerPort();
250            return "-1".equals(port) ? DEFAULT_PORT : port;
251        });
252    }
253
254    /**
255     * Copy project dependencies to a <code>target/dependency</code> directory.
256     */
257    @SuppressWarnings("ResultOfMethodCallIgnored")
258    protected void copyDependencies() throws IOException {
259        var imageClasspathScopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME);
260        mavenProject.setArtifactFilter(artifact -> imageClasspathScopes.contains(artifact.getScope()));
261        var target = new File(mavenProject.getBuild().getDirectory(), "dependency");
262        if (!target.exists()) {
263            target.mkdirs();
264        }
265        for (Artifact dependency : mavenProject.getArtifacts()) {
266            Files.copy(dependency.getFile().toPath(), target.toPath().resolve(dependency.getFile().getName()), StandardCopyOption.REPLACE_EXISTING);
267        }
268    }
269
270    /**
271     * @return the Docker CMD command.
272     */
273    protected String getCmd() {
274        return "CMD [" +
275            appArguments.stream()
276                .map(s -> "\"" + s + "\"")
277                .collect(Collectors.joining(", ")) +
278            "]";
279    }
280
281    /**
282     * @return Networking mode for the RUN instructions during build (if any).
283     */
284    protected Optional<String> getNetworkMode() {
285        return Optional.ofNullable(networkMode);
286    }
287
288    /**
289     * @return the base image to use for the Dockerfile.
290     */
291    protected String getBaseImage() {
292        return JibMicronautExtension.determineBaseImage(JibMicronautExtension.getJdkVersion(mavenProject), MicronautRuntime.valueOf(micronautRuntime.toUpperCase()).getBuildStrategy());
293    }
294
295    /**
296     * Adds cmd to docker oracle cloud function file.
297     *
298     * @param dockerfile the docker file
299     */
300    protected void oracleCloudFunctionCmd(File dockerfile) throws IOException {
301        if (appArguments != null && !appArguments.isEmpty()) {
302            getLog().info("Using application arguments: " + appArguments);
303            com.google.common.io.Files.asCharSink(dockerfile, Charset.defaultCharset(), FileWriteMode.APPEND).write(System.lineSeparator() + getCmd());
304        } else {
305            com.google.common.io.Files.asCharSink(dockerfile, Charset.defaultCharset(), FileWriteMode.APPEND).write(System.lineSeparator() + ORACLE_CLOUD_FUNCTION_DEFAULT_CMD);
306        }
307    }
308
309}