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}