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