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