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