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.cloud.tools.jib.api.ImageReference;
019import com.google.cloud.tools.jib.api.InvalidImageReferenceException;
020import com.google.common.io.FileWriteMode;
021import io.micronaut.core.util.StringUtils;
022import io.micronaut.maven.core.MicronautRuntime;
023import io.micronaut.maven.core.MojoUtils;
024import io.micronaut.maven.jib.JibConfigurationService;
025import io.micronaut.maven.jib.JibMicronautExtension;
026import io.micronaut.maven.services.ApplicationConfigurationService;
027import io.micronaut.maven.services.DockerService;
028import org.apache.maven.artifact.Artifact;
029import org.apache.maven.artifact.versioning.ArtifactVersion;
030import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
031import org.apache.maven.execution.MavenSession;
032import org.apache.maven.plugin.MojoExecutionException;
033import org.apache.maven.plugin.MojoExecution;
034import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
035import org.apache.maven.plugins.annotations.Parameter;
036import org.apache.maven.project.MavenProject;
037
038import java.io.File;
039import java.io.IOException;
040import java.net.URI;
041import java.net.URISyntaxException;
042import java.nio.charset.Charset;
043import java.nio.file.Files;
044import java.nio.file.StandardCopyOption;
045import java.util.ArrayList;
046import java.util.Arrays;
047import java.util.HashSet;
048import java.util.List;
049import java.util.Locale;
050import java.util.Map;
051import java.util.NavigableSet;
052import java.util.Optional;
053import java.util.Set;
054import java.util.TreeSet;
055import java.util.stream.Collectors;
056import java.util.regex.Matcher;
057import java.util.regex.Pattern;
058
059import static io.micronaut.maven.services.ApplicationConfigurationService.DEFAULT_PORT;
060
061/**
062 * Abstract base class for mojos related to Docker files and builds.
063 *
064 * @author Álvaro Sánchez-Mariscal
065 * @author Iván López
066 * @since 1.1
067 */
068public abstract class AbstractDockerMojo extends AbstractMicronautMojo {
069
070    public static final String LATEST_TAG = "latest";
071    public static final String DEFAULT_BASE_IMAGE_GRAALVM_RUN = "cgr.dev/chainguard/wolfi-base@sha256:52e71f61c6afd1f8d2625cff4465d8ecee156668ca665f7e9c582d1cc914eb6a";
072    public static final String DEFAULT_BASE_IMAGE_GRAALVM_BUILD = "container-registry.oracle.com/graalvm/native-image";
073    public static final String MOSTLY_STATIC_NATIVE_IMAGE_GRAALVM_FLAG = "-H:+StaticExecutableWithDynamicLibC";
074    public static final String ARM_ARCH = "aarch64";
075    public static final String X86_64_ARCH = "x64";
076    public static final String ORACLE_CLOUD_FUNCTION_DEFAULT_CMD = "CMD [\"io.micronaut.oraclecloud.function.http.HttpFunction::handleRequest\"]";
077    public static final String GDS_DOWNLOAD_URL = "https://gds.oracle.com/download/graal/%s/latest-gftc/graalvm-jdk-%s_linux-%s_bin.tar.gz";
078    public static final String LAMBDA_BOOTSTRAP_DOCKER_COMMAND_PLACEHOLDER = "${LAMBDA_BOOTSTRAP_DOCKER_COMMAND}";
079    static final String JIB_FROM_IMAGE_PROPERTY = "jib.from.image";
080    private static final String DEPENDENCY_DIRECTORY = "dependency";
081    private static final String RELEASE_DEPENDENCY_DIRECTORY = "release";
082    private static final String SNAPSHOT_DEPENDENCY_DIRECTORY = "snapshot";
083    private static final Pattern LEADING_MAJOR_VERSION = Pattern.compile("([1-9][0-9]*)(?:[.-].*)?");
084    private static final NavigableSet<Integer> GRAALVM_VERSIONS = new TreeSet<>(Set.of(25));
085    private static final List<String> DEFAULT_LAMBDA_BOOTSTRAP_ARGUMENTS = List.of(
086        "-XX:MaximumHeapSizePercent=80",
087        "-Dio.netty.allocator.numDirectArenas=0",
088        "-Dio.netty.noPreferDirect=true",
089        "-Djava.library.path=$(pwd)"
090    );
091
092    protected final MavenProject mavenProject;
093    protected final MavenSession mavenSession;
094    protected final JibConfigurationService jibConfigurationService;
095    protected final ApplicationConfigurationService applicationConfigurationService;
096    protected final DockerService dockerService;
097    protected final PluginParameterExpressionEvaluator expressionEvaluator;
098
099
100    /**
101     * Additional arguments that will be passed to the <code>native-image</code> executable. Note that this will only
102     * be used when using a packaging of type <code>docker-native</code>. For <code>native-image</code> packaging
103     * you should use the
104     * <a href="https://www.graalvm.org/reference-manual/native-image/NativeImageMavenPlugin/#maven-plugin-customization">
105     * Native Image Maven Plugin
106     * </a> configuration options.
107     */
108    @Parameter(property = "micronaut.native-image.args")
109    protected List<String> nativeImageBuildArgs;
110
111    /**
112     * List of additional arguments that will be passed to the application.
113     */
114    @Parameter(property = RunMojo.MN_APP_ARGS)
115    protected List<String> appArguments;
116
117    /**
118     * Additional arguments that will be appended to the generated AWS Lambda native bootstrap command.
119     *
120     * @since 5.0.0
121     */
122    @Parameter(property = "micronaut.lambda.bootstrap.args")
123    protected List<String> lambdaBootstrapArguments;
124
125    /**
126     * The main class of the application, as defined in the
127     * <a href="https://www.mojohaus.org/exec-maven-plugin/java-mojo.html#mainClass">Exec Maven Plugin</a>.
128     */
129    @Parameter(defaultValue = RunMojo.EXEC_MAIN_CLASS, required = true)
130    protected String mainClass;
131
132    /**
133     * Whether to produce a static native image when using <code>docker-native</code> packaging.
134     */
135    @Parameter(defaultValue = "false", property = "micronaut.native-image.static")
136    protected Boolean staticNativeImage;
137
138    /**
139     * The target runtime of the application.
140     */
141    @Parameter(property = MicronautRuntime.PROPERTY, defaultValue = "NONE")
142    protected String micronautRuntime;
143
144    /**
145     * The Docker image used to run the native image.
146     *
147     * @since 1.2
148     */
149    @Parameter(property = "micronaut.native-image.base-image-run", defaultValue = DEFAULT_BASE_IMAGE_GRAALVM_RUN)
150    protected String baseImageRun;
151
152    /**
153     * The builder-stage base image used to build the native image for docker-native packaging variants.
154     *
155     * @since 5.0.0
156     */
157    @Parameter(property = "micronaut.native-image.base-image")
158    protected String baseImage;
159
160    /**
161     * The version of Oracle Linux to use as a native-compile base when building a native image inside a Docker container.
162     */
163    @Parameter(property = "micronaut.native-image.ol.version", defaultValue = "ol9")
164    protected String oracleLinuxVersion;
165
166    /**
167     * Networking mode for the RUN instructions during build.
168     *
169     * @since 4.0.0
170     */
171    @Parameter(property = "docker.networkMode")
172    protected String networkMode;
173
174    /**
175     * <p>
176     * Jib goal used to build Docker images for {@code docker} packaging.
177     * </p>
178     * <p>
179     * Defaults to {@code dockerBuild}. Set it to {@code buildTar} or {@code build} to avoid talking to a local Docker daemon during {@code package}.
180     * </p>
181     *
182     * @since 5.0.0
183     */
184    @Parameter(property = "jib.buildGoal", defaultValue = "dockerBuild")
185    protected String jibBuildGoal;
186
187    protected AbstractDockerMojo(MavenProject mavenProject, JibConfigurationService jibConfigurationService,
188                                 ApplicationConfigurationService applicationConfigurationService,
189                                 DockerService dockerService, MavenSession mavenSession, MojoExecution mojoExecution) {
190        this.mavenProject = mavenProject;
191        this.mavenSession = mavenSession;
192        this.jibConfigurationService = jibConfigurationService;
193        this.applicationConfigurationService = applicationConfigurationService;
194        this.dockerService = dockerService;
195        this.expressionEvaluator = new PluginParameterExpressionEvaluator(mavenSession, mojoExecution);
196    }
197
198    /**
199     * @return the Java version from either the <code>maven.compiler.target</code> property or the <code>java.version</code> property.
200     */
201    protected ArtifactVersion javaVersion() {
202        return new DefaultArtifactVersion(getJdkVersion());
203    }
204
205    private String getJdkVersion() {
206        var releaseVersion = getPropertyValue(mavenProject, "maven.compiler.release");
207        var targetVersion = getPropertyValue(mavenProject, "maven.compiler.target");
208        return releaseVersion.or(() -> targetVersion).orElseGet(() -> System.getProperty("java.version"));
209    }
210
211    private static Optional<String> getPropertyValue(MavenProject project, String propertName) {
212        var systemProperty = Optional.of(propertName).map(System::getProperty);
213        var properties = project.getProperties();
214        var projectProperty = Optional.of(propertName).map(properties::getProperty);
215        return systemProperty.or(() -> projectProperty);
216    }
217
218    /**
219     * @return the JVM version to use for GraalVM.
220     */
221    protected String graalVmJvmVersion() {
222        return Integer.toString(resolveGraalVersion());
223    }
224
225    /**
226     * @return the GraalVM download URL depending on the Java version.
227     */
228    protected String graalVmDownloadUrl() {
229        Integer version = resolveGraalVersion();
230
231        return GDS_DOWNLOAD_URL.formatted(version, version, graalVmArch());
232    }
233
234    private Integer resolveGraalVersion() {
235        int target = javaVersion().getMajorVersion();
236        Integer version = GRAALVM_VERSIONS.floor(target);
237
238        return version != null ? version : GRAALVM_VERSIONS.first();
239    }
240
241    /**
242     * @return the OS architecture to use for GraalVM depending on the <code>os.arch</code> system property.
243     */
244    protected String graalVmArch() {
245        return isArm() ? ARM_ARCH : X86_64_ARCH;
246    }
247
248    /**
249     * @return the base FROM image for the native image.
250     */
251    protected String getFrom() {
252        return getJibFromImageSystemProperty()
253            .or(() -> Optional.ofNullable(baseImage).filter(StringUtils::hasText))
254            .or(() -> getFromImage().filter(StringUtils::hasText))
255            .orElse(DEFAULT_BASE_IMAGE_GRAALVM_BUILD + ":" + graalVmTag(graalVmJvmVersion(), staticNativeImage, oracleLinuxVersion));
256    }
257
258    /**
259     * @return whether the selected native-image builder is known to support {@code -H:+SharedArenaSupport}.
260     */
261    protected boolean supportsSharedArena() {
262        return sharedArenaBuilderMajorVersion()
263            .map(MojoUtils::supportsSharedArena)
264            .orElse(false);
265    }
266
267    private Optional<Integer> sharedArenaBuilderMajorVersion() {
268        return getJibFromImageSystemProperty()
269            .or(() -> Optional.ofNullable(baseImage).filter(StringUtils::hasText))
270            .or(() -> getFromImage().filter(StringUtils::hasText))
271            .map(AbstractDockerMojo::graalVmNativeImageBuilderMajorVersion)
272            .orElseGet(() -> Optional.of(resolveGraalVersion()));
273    }
274
275    static Optional<Integer> graalVmNativeImageBuilderMajorVersion(String image) {
276        if (!StringUtils.hasText(image)) {
277            return Optional.empty();
278        }
279
280        int digestStart = image.indexOf('@');
281        String imageWithoutDigest = digestStart >= 0 ? image.substring(0, digestStart) : image;
282        int lastSlash = imageWithoutDigest.lastIndexOf('/');
283        int tagSeparator = imageWithoutDigest.lastIndexOf(':');
284        if (tagSeparator <= lastSlash) {
285            return Optional.empty();
286        }
287
288        String repository = imageWithoutDigest.substring(0, tagSeparator).toLowerCase(Locale.ROOT);
289        if (!isGraalVmNativeImageRepository(repository)) {
290            return Optional.empty();
291        }
292
293        String tag = imageWithoutDigest.substring(tagSeparator + 1);
294        Matcher matcher = LEADING_MAJOR_VERSION.matcher(tag);
295        if (!matcher.matches()) {
296            return Optional.empty();
297        }
298        try {
299            return Optional.of(Integer.parseInt(matcher.group(1)));
300        } catch (NumberFormatException e) {
301            return Optional.empty();
302        }
303    }
304
305    private static boolean isGraalVmNativeImageRepository(String repository) {
306        int lastSlash = repository.lastIndexOf('/');
307        String imageName = lastSlash >= 0 ? repository.substring(lastSlash + 1) : repository;
308        return (repository.startsWith("graalvm/") || repository.contains("/graalvm/"))
309            && imageName.startsWith("native-image");
310    }
311
312    /**
313     * @param graalVmJvmVersion the JVM version string
314     * @param staticNativeImage whether to produce a static native image
315     * @param oracleLinuxVersion the Oracle Linux version to use
316     * @return the GraalVM Docker image tag based on the provided parameters
317     */
318    protected String graalVmTag(String graalVmJvmVersion, Boolean staticNativeImage, String oracleLinuxVersion) {
319        String suffix = Boolean.TRUE.equals(staticNativeImage)
320            ? "-muslib" + (StringUtils.hasText(oracleLinuxVersion) ? "-" + oracleLinuxVersion : "")
321            : (StringUtils.hasText(oracleLinuxVersion) ? "-" + oracleLinuxVersion : "");
322        return graalVmJvmVersion + suffix;
323    }
324
325    /**
326     * Check os.arch against known ARM architecture identifiers.
327     *
328     * @return true if we think we're running on an arm JDK
329     */
330    protected boolean isArm() {
331        return switch (System.getProperty("os.arch")) {
332            case ARM_ARCH, "arm64" -> true;
333            default -> false;
334        };
335    }
336
337    /**
338     * @return the base image from the jib configuration (if any).
339     */
340    protected Optional<String> getFromImage() {
341        return jibConfigurationService.getFromImage();
342    }
343
344    /**
345     * @return the base image from the Jib system property override, if any.
346     */
347    protected Optional<String> getJibFromImageSystemProperty() {
348        return Optional.ofNullable(System.getProperty(JIB_FROM_IMAGE_PROPERTY))
349            .filter(StringUtils::hasText);
350    }
351
352    /**
353     * @return the Docker image tags by looking at the Jib plugin configuration.
354     */
355    protected Set<String> getTags() {
356        var tags = new HashSet<String>();
357        Optional<String> toImageOptional = jibConfigurationService.getToImage();
358        String imageName = mavenProject.getArtifactId();
359        if (toImageOptional.isPresent()) {
360            String toImage = toImageOptional.get();
361            if (toImage.contains(":")) {
362                tags.add(toImage);
363                imageName = toImageOptional.get().split(":")[0];
364            } else {
365                tags.add(toImage + ":" + LATEST_TAG);
366                imageName = toImage;
367            }
368        } else {
369            tags.add(imageName + ":" + LATEST_TAG);
370        }
371        for (String tag : jibConfigurationService.getTags()) {
372            if (LATEST_TAG.equals(tag) && tags.stream().anyMatch(t -> t.contains(LATEST_TAG))) {
373                continue;
374            }
375            tags.add(String.format("%s:%s", imageName, tag));
376        }
377        return tags.stream()
378            .map(this::evaluateExpression)
379            .collect(Collectors.toSet());
380    }
381
382    private String evaluateExpression(String expression) {
383        try {
384            return expressionEvaluator.evaluate(expression, String.class).toString();
385        } catch (Exception e) {
386            return expression;
387        }
388    }
389
390    /**
391     * @return the application ports to expose by looking at the Jib configuration or the application configuration.
392     */
393    protected String getPorts() {
394        return jibConfigurationService.getPorts().orElseGet(() -> {
395            String port = applicationConfigurationService.getServerPort();
396            return "-1".equals(port) ? DEFAULT_PORT : port;
397        });
398    }
399
400    /**
401     * Copy project dependencies to a <code>target/dependency</code> directory.
402     */
403    protected void copyDependencies() throws IOException {
404        var imageClasspathScopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME);
405        var target = new File(mavenProject.getBuild().getDirectory(), DEPENDENCY_DIRECTORY).toPath();
406        Files.createDirectories(target);
407        Files.createDirectories(target.resolve(RELEASE_DEPENDENCY_DIRECTORY));
408        Files.createDirectories(target.resolve(SNAPSHOT_DEPENDENCY_DIRECTORY));
409        for (Artifact dependency : mavenProject.getArtifacts()) {
410            if (!imageClasspathScopes.contains(dependency.getScope())) {
411                continue;
412            }
413            var dependencyFile = dependency.getFile().toPath();
414            var dependencyName = dependency.getFile().getName();
415            var layeredPath = target.resolve(dependencyLayerDirectory(dependency)).resolve(dependencyName);
416            Files.copy(dependencyFile, layeredPath, StandardCopyOption.REPLACE_EXISTING);
417            Files.copy(dependencyFile, target.resolve(dependencyName), StandardCopyOption.REPLACE_EXISTING);
418        }
419    }
420
421    private static String dependencyLayerDirectory(Artifact dependency) {
422        return dependency.isSnapshot() ? SNAPSHOT_DEPENDENCY_DIRECTORY : RELEASE_DEPENDENCY_DIRECTORY;
423    }
424
425    /**
426     * @return the Docker CMD command.
427     */
428    protected String getCmd() throws MojoExecutionException {
429        var escapedArguments = new ArrayList<String>(appArguments.size());
430        for (String argument : appArguments) {
431            escapedArguments.add(jsonStringLiteral("mn.app.args", argument));
432        }
433        return "CMD [" + String.join(", ", escapedArguments) + "]";
434    }
435
436    /**
437     * @return the generated AWS Lambda bootstrap command.
438     */
439    protected String getLambdaBootstrapCommand() throws MojoExecutionException {
440        var command = new StringBuilder("./func");
441        for (String bootstrapArgument : DEFAULT_LAMBDA_BOOTSTRAP_ARGUMENTS) {
442            command.append(' ').append(bootstrapArgument);
443        }
444        if (lambdaBootstrapArguments != null && !lambdaBootstrapArguments.isEmpty()) {
445            for (String lambdaBootstrapArgument : lambdaBootstrapArguments) {
446                command.append(' ').append(escapeBootstrapArgument(lambdaBootstrapArgument));
447            }
448        }
449        return command.toString();
450    }
451
452    /**
453     * Applies the generated AWS Lambda bootstrap script to a dockerfile template.
454     *
455     * @param dockerfile the docker file
456     */
457    protected void lambdaBootstrapCommand(File dockerfile) throws IOException, MojoExecutionException {
458        if (dockerfile == null) {
459            return;
460        }
461        String lambdaBootstrapDockerCommand = getLambdaBootstrapDockerCommand();
462        if (lambdaBootstrapArguments != null && !lambdaBootstrapArguments.isEmpty()) {
463            getLog().info("Using AWS Lambda bootstrap arguments: " + lambdaBootstrapArguments);
464        }
465        var allLines = Files.readAllLines(dockerfile.toPath());
466        var result = new ArrayList<String>(allLines.size());
467        for (String line : allLines) {
468            if (line.contains(LAMBDA_BOOTSTRAP_DOCKER_COMMAND_PLACEHOLDER)) {
469                result.add(line.replace(LAMBDA_BOOTSTRAP_DOCKER_COMMAND_PLACEHOLDER, lambdaBootstrapDockerCommand));
470            } else {
471                result.add(line);
472            }
473        }
474        Files.write(dockerfile.toPath(), result);
475    }
476
477    private String getLambdaBootstrapDockerCommand() throws MojoExecutionException {
478        var quotedLines = new ArrayList<String>();
479        for (String line : List.of("#!/bin/sh", "set -euo pipefail", getLambdaBootstrapCommand())) {
480            quotedLines.add(quoteShellLiteral("AWS Lambda bootstrap command", line));
481        }
482        return "printf '%s\\n' " + String.join(" ", quotedLines) + " > bootstrap";
483    }
484
485    private static String escapeBootstrapArgument(String argument) throws MojoExecutionException {
486        String sanitized = validateDockerfileValue("micronaut.lambda.bootstrap.args", argument);
487        if (isShellSafe(sanitized)) {
488            return sanitized;
489        }
490        return quoteShellLiteral("micronaut.lambda.bootstrap.args", sanitized);
491    }
492
493    private static boolean isShellSafe(String argument) {
494        if (argument == null || argument.isEmpty()) {
495            return false;
496        }
497        for (int i = 0; i < argument.length(); i++) {
498            char c = argument.charAt(i);
499            if (!Character.isLetterOrDigit(c)
500                && "_@%+=:,./-".indexOf(c) == -1) {
501                return false;
502            }
503        }
504        return true;
505    }
506
507    static String quoteShellLiteral(String source, String value) throws MojoExecutionException {
508        validateDockerfileValue(source, value);
509        return "'" + value.replace("'", "'\"'\"'") + "'";
510    }
511
512    protected static String validateDockerfileValue(String source, String value) throws MojoExecutionException {
513        if (value == null) {
514            throw new MojoExecutionException(source + " must not be null when generating a Dockerfile");
515        }
516        for (int i = 0; i < value.length(); i++) {
517            char c = value.charAt(i);
518            if (Character.isISOControl(c)) {
519                throw new MojoExecutionException(source + " contains an unsupported control character at index " + i
520                    + " and cannot be written into a generated Dockerfile");
521            }
522        }
523        return value;
524    }
525
526    protected static String validateImageReference(String source, String value) throws MojoExecutionException {
527        String sanitized = validateDockerfileValue(source, value);
528        try {
529            ImageReference.parse(sanitized);
530        } catch (InvalidImageReferenceException e) {
531            throw new MojoExecutionException(source + " is not a valid Docker image reference: " + sanitized, e);
532        }
533        return sanitized;
534    }
535
536    protected static String validateExposedPorts(String source, String value) throws MojoExecutionException {
537        String sanitized = validateDockerfileValue(source, value);
538        for (String token : sanitized.split("\\s+")) {
539            if (token.isEmpty()) {
540                continue;
541            }
542            if (!token.matches("\\d+(/(?:tcp|udp))?")) {
543                throw new MojoExecutionException(source + " contains an invalid exposed port token: " + token);
544            }
545        }
546        return sanitized;
547    }
548
549    protected static String validateDownloadUrl(String source, String value) throws MojoExecutionException {
550        String sanitized = validateDockerfileValue(source, value);
551        try {
552            URI uri = new URI(sanitized);
553            String scheme = uri.getScheme();
554            if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) {
555                throw new MojoExecutionException(source + " must use an http or https URL: " + sanitized);
556            }
557            if (!uri.isAbsolute()) {
558                throw new MojoExecutionException(source + " must be an absolute URL: " + sanitized);
559            }
560        } catch (URISyntaxException e) {
561            throw new MojoExecutionException(source + " is not a valid URL: " + sanitized, e);
562        }
563        return sanitized;
564    }
565
566    protected static String jsonStringLiteral(String source, String value) throws MojoExecutionException {
567        return "\"" + escapeJsonString(source, value) + "\"";
568    }
569
570    protected static String escapeJsonString(String source, String value) throws MojoExecutionException {
571        String sanitized = validateDockerfileValue(source, value);
572        var result = new StringBuilder(sanitized.length() + 8);
573        for (int i = 0; i < sanitized.length(); i++) {
574            char c = sanitized.charAt(i);
575            switch (c) {
576                case '"' -> result.append("\\\"");
577                case '\\' -> result.append("\\\\");
578                case '\b' -> result.append("\\b");
579                case '\f' -> result.append("\\f");
580                case '\n' -> result.append("\\n");
581                case '\r' -> result.append("\\r");
582                case '\t' -> result.append("\\t");
583                default -> {
584                    if (c < 0x20) {
585                        result.append(String.format("\\u%04x", (int) c));
586                    } else {
587                        result.append(c);
588                    }
589                }
590            }
591        }
592        return result.toString();
593    }
594
595    protected static String shellLiteral(String source, String value) throws MojoExecutionException {
596        return quoteShellLiteral(source, validateDockerfileValue(source, value));
597    }
598
599    protected static String escapeShellDoubleQuoted(String source, String value) throws MojoExecutionException {
600        return validateDockerfileValue(source, value)
601            .replace("\\", "\\\\")
602            .replace("\"", "\\\"")
603            .replace("$", "\\$")
604            .replace("`", "\\`");
605    }
606
607    /**
608     * @return Networking mode for the RUN instructions during build (if any).
609     */
610    protected Optional<String> getNetworkMode() {
611        return Optional.ofNullable(networkMode);
612    }
613
614    /**
615     * @return Map of proxy-related build arguments for Docker builds.
616     */
617    protected Map<String, String> getProxyBuildArgs() {
618        var proxyArgs = new java.util.HashMap<String, String>();
619        
620        // HTTP proxy configuration from standard JVM properties
621        String httpProxyHost = System.getProperty("http.proxyHost");
622        String httpProxyPort = System.getProperty("http.proxyPort", "80");
623        if (StringUtils.hasText(httpProxyHost)) {
624            String httpProxy = "http://" + httpProxyHost + ":" + httpProxyPort;
625            proxyArgs.put("HTTP_PROXY", httpProxy);
626            proxyArgs.put("http_proxy", httpProxy);
627        }
628        
629        // HTTPS proxy configuration from standard JVM properties
630        String httpsProxyHost = System.getProperty("https.proxyHost");
631        String httpsProxyPort = System.getProperty("https.proxyPort", "443");
632        if (StringUtils.hasText(httpsProxyHost)) {
633            String httpsProxy = "http://" + httpsProxyHost + ":" + httpsProxyPort;
634            proxyArgs.put("HTTPS_PROXY", httpsProxy);
635            proxyArgs.put("https_proxy", httpsProxy);
636        }
637        
638        // No proxy configuration from standard JVM properties
639        String nonProxyHosts = System.getProperty("http.nonProxyHosts");
640        if (StringUtils.hasText(nonProxyHosts)) {
641            // Convert Java format (e.g., "*.company.com|localhost") to standard format (e.g., "*.company.com,localhost")
642            String noProxy = nonProxyHosts.replace("|", ",");
643            proxyArgs.put("NO_PROXY", noProxy);
644            proxyArgs.put("no_proxy", noProxy);
645        }
646        
647        return proxyArgs;
648    }
649
650    /**
651     * @return the base image to use for the Dockerfile.
652     */
653    protected String getBaseImage() {
654        return JibMicronautExtension.determineBaseImage(JibMicronautExtension.getJdkVersion(mavenSession), MicronautRuntime.valueOf(micronautRuntime.toUpperCase()).getBuildStrategy());
655    }
656
657    /**
658     * Adds cmd to docker oracle cloud function file.
659     *
660     * @param dockerfile the docker file
661     */
662    protected void oracleCloudFunctionCmd(File dockerfile) throws IOException, MojoExecutionException {
663        if (appArguments != null && !appArguments.isEmpty()) {
664            getLog().info("Using application arguments: " + appArguments);
665            com.google.common.io.Files.asCharSink(dockerfile, Charset.defaultCharset(), FileWriteMode.APPEND)
666                .write(System.lineSeparator() + getCmd());
667        } else {
668            com.google.common.io.Files.asCharSink(dockerfile, Charset.defaultCharset(), FileWriteMode.APPEND).write(System.lineSeparator() + ORACLE_CLOUD_FUNCTION_DEFAULT_CMD);
669        }
670    }
671
672}