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