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