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