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    /**
138     * <p>
139     * jib-maven-plugin goal used to build the Docker image (when Jib is used). Defaults to <code>dockerBuild</code>,
140     * but can be set to <code>build</code> or <code>buildTar</code> to build the image without a Docker daemon.
141     * </p>
142     * <p>
143     * Note that Jib's <code>build</code> goal with also push the image to a container registry. Check the
144     * <a href="https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#quickstart">jib-maven-plugin</a>
145     * documentation for more information.
146     * </p>
147     *
148     * @since 4.11.2
149     */
150    @Parameter(property = "jib.buildGoal", defaultValue = "dockerBuild")
151    protected String jibBuildGoal;
152
153    protected AbstractDockerMojo(MavenProject mavenProject, JibConfigurationService jibConfigurationService,
154                                 ApplicationConfigurationService applicationConfigurationService,
155                                 DockerService dockerService, MavenSession mavenSession, MojoExecution mojoExecution) {
156        this.mavenProject = mavenProject;
157        this.mavenSession = mavenSession;
158        this.jibConfigurationService = jibConfigurationService;
159        this.applicationConfigurationService = applicationConfigurationService;
160        this.dockerService = dockerService;
161        this.expressionEvaluator = new PluginParameterExpressionEvaluator(mavenSession, mojoExecution);
162    }
163
164    /**
165     * @return the Java version from either the <code>maven.compiler.target</code> property or the <code>java.version</code> property.
166     */
167    protected ArtifactVersion javaVersion() {
168        return new DefaultArtifactVersion(getJdkVersion());
169    }
170
171    private String getJdkVersion() {
172        var releaseVersion = getPropertyValue(mavenProject, "maven.compiler.release");
173        var targetVersion = getPropertyValue(mavenProject, "maven.compiler.target");
174        return releaseVersion.or(() -> targetVersion).orElseGet(() -> System.getProperty("java.version"));
175    }
176
177    private static Optional<String> getPropertyValue(MavenProject project, String propertName) {
178        var systemProperty = Optional.of(propertName).map(System::getProperty);
179        var properties = project.getProperties();
180        var projectProperty = Optional.of(propertName).map(properties::getProperty);
181        return systemProperty.or(() -> projectProperty);
182    }
183
184    /**
185     * @return the JVM version to use for GraalVM.
186     */
187    protected String graalVmJvmVersion() {
188        return Integer.toString(resolveGraalVersion());
189    }
190
191    /**
192     * @return the GraalVM download URL depending on the Java version.
193     */
194    protected String graalVmDownloadUrl() {
195        Integer version = resolveGraalVersion();
196
197        return GDS_DOWNLOAD_URL.formatted(version, version, graalVmArch());
198    }
199
200    private Integer resolveGraalVersion() {
201        int target = javaVersion().getMajorVersion();
202        Integer version = GRAALVM_VERSIONS.floor(target);
203
204        return version != null ? version : GRAALVM_VERSIONS.first();
205    }
206
207    /**
208     * @return the OS architecture to use for GraalVM depending on the <code>os.arch</code> system property.
209     */
210    protected String graalVmArch() {
211        return isArm() ? ARM_ARCH : X86_64_ARCH;
212    }
213
214    /**
215     * @return the base FROM image for the native image.
216     */
217    protected String getFrom() {
218        if (Boolean.TRUE.equals(staticNativeImage)) {
219            return getFromImage().orElse("ghcr.io/graalvm/native-image-community:" + graalVmJvmVersion() + "-muslib-" + oracleLinuxVersion);
220        } else {
221            return getFromImage().orElse("ghcr.io/graalvm/native-image-community:" + graalVmJvmVersion() + "-" + oracleLinuxVersion);
222        }
223    }
224
225    /**
226     * Check os.arch against known ARM architecture identifiers.
227     *
228     * @return true if we think we're running on an arm JDK
229     */
230    protected boolean isArm() {
231        return switch (System.getProperty("os.arch")) {
232            case ARM_ARCH, "arm64" -> true;
233            default -> false;
234        };
235    }
236
237    /**
238     * @return the base image from the jib configuration (if any).
239     */
240    protected Optional<String> getFromImage() {
241        return jibConfigurationService.getFromImage();
242    }
243
244    /**
245     * @return the Docker image tags by looking at the Jib plugin configuration.
246     */
247    protected Set<String> getTags() {
248        var tags = new HashSet<String>();
249        Optional<String> toImageOptional = jibConfigurationService.getToImage();
250        String imageName = mavenProject.getArtifactId();
251        if (toImageOptional.isPresent()) {
252            String toImage = toImageOptional.get();
253            if (toImage.contains(":")) {
254                tags.add(toImage);
255                imageName = toImageOptional.get().split(":")[0];
256            } else {
257                tags.add(toImage + ":" + LATEST_TAG);
258                imageName = toImage;
259            }
260        } else {
261            tags.add(imageName + ":" + LATEST_TAG);
262        }
263        for (String tag : jibConfigurationService.getTags()) {
264            if (LATEST_TAG.equals(tag) && tags.stream().anyMatch(t -> t.contains(LATEST_TAG))) {
265                continue;
266            }
267            tags.add(String.format("%s:%s", imageName, tag));
268        }
269        return tags.stream()
270            .map(this::evaluateExpression)
271            .collect(Collectors.toSet());
272    }
273
274    private String evaluateExpression(String expression) {
275        try {
276            return expressionEvaluator.evaluate(expression, String.class).toString();
277        } catch (Exception e) {
278            return expression;
279        }
280    }
281
282    /**
283     * @return the application ports to expose by looking at the Jib configuration or the application configuration.
284     */
285    protected String getPorts() {
286        return jibConfigurationService.getPorts().orElseGet(() -> {
287            String port = applicationConfigurationService.getServerPort();
288            return "-1".equals(port) ? DEFAULT_PORT : port;
289        });
290    }
291
292    /**
293     * Copy project dependencies to a <code>target/dependency</code> directory.
294     */
295    @SuppressWarnings("ResultOfMethodCallIgnored")
296    protected void copyDependencies() throws IOException {
297        var imageClasspathScopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME);
298        mavenProject.setArtifactFilter(artifact -> imageClasspathScopes.contains(artifact.getScope()));
299        var target = new File(mavenProject.getBuild().getDirectory(), "dependency");
300        if (!target.exists()) {
301            target.mkdirs();
302        }
303        for (Artifact dependency : mavenProject.getArtifacts()) {
304            Files.copy(dependency.getFile().toPath(), target.toPath().resolve(dependency.getFile().getName()), StandardCopyOption.REPLACE_EXISTING);
305        }
306    }
307
308    /**
309     * @return the Docker CMD command.
310     */
311    protected String getCmd() {
312        return "CMD [" +
313            appArguments.stream()
314                .map(s -> "\"" + s + "\"")
315                .collect(Collectors.joining(", ")) +
316            "]";
317    }
318
319    /**
320     * @return Networking mode for the RUN instructions during build (if any).
321     */
322    protected Optional<String> getNetworkMode() {
323        return Optional.ofNullable(networkMode);
324    }
325
326    /**
327     * @return Map of proxy-related build arguments for Docker builds.
328     */
329    protected Map<String, String> getProxyBuildArgs() {
330        var proxyArgs = new java.util.HashMap<String, String>();
331        
332        // HTTP proxy configuration from standard JVM properties
333        String httpProxyHost = System.getProperty("http.proxyHost");
334        String httpProxyPort = System.getProperty("http.proxyPort", "80");
335        if (StringUtils.hasText(httpProxyHost)) {
336            String httpProxy = "http://" + httpProxyHost + ":" + httpProxyPort;
337            proxyArgs.put("HTTP_PROXY", httpProxy);
338            proxyArgs.put("http_proxy", httpProxy);
339        }
340        
341        // HTTPS proxy configuration from standard JVM properties
342        String httpsProxyHost = System.getProperty("https.proxyHost");
343        String httpsProxyPort = System.getProperty("https.proxyPort", "443");
344        if (StringUtils.hasText(httpsProxyHost)) {
345            String httpsProxy = "http://" + httpsProxyHost + ":" + httpsProxyPort;
346            proxyArgs.put("HTTPS_PROXY", httpsProxy);
347            proxyArgs.put("https_proxy", httpsProxy);
348        }
349        
350        // No proxy configuration from standard JVM properties
351        String nonProxyHosts = System.getProperty("http.nonProxyHosts");
352        if (StringUtils.hasText(nonProxyHosts)) {
353            // Convert Java format (e.g., "*.company.com|localhost") to standard format (e.g., "*.company.com,localhost")
354            String noProxy = nonProxyHosts.replace("|", ",");
355            proxyArgs.put("NO_PROXY", noProxy);
356            proxyArgs.put("no_proxy", noProxy);
357        }
358        
359        return proxyArgs;
360    }
361
362    /**
363     * @return the base image to use for the Dockerfile.
364     */
365    protected String getBaseImage() {
366        return JibMicronautExtension.determineBaseImage(JibMicronautExtension.getJdkVersion(mavenSession), MicronautRuntime.valueOf(micronautRuntime.toUpperCase()).getBuildStrategy());
367    }
368
369    /**
370     * Adds cmd to docker oracle cloud function file.
371     *
372     * @param dockerfile the docker file
373     */
374    protected void oracleCloudFunctionCmd(File dockerfile) throws IOException {
375        if (appArguments != null && !appArguments.isEmpty()) {
376            getLog().info("Using application arguments: " + appArguments);
377            com.google.common.io.Files.asCharSink(dockerfile, Charset.defaultCharset(), FileWriteMode.APPEND).write(System.lineSeparator() + getCmd());
378        } else {
379            com.google.common.io.Files.asCharSink(dockerfile, Charset.defaultCharset(), FileWriteMode.APPEND).write(System.lineSeparator() + ORACLE_CLOUD_FUNCTION_DEFAULT_CMD);
380        }
381    }
382
383}