001/* 002 * Copyright 2017-2022 original authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.micronaut.maven; 017 018import com.google.common.io.FileWriteMode; 019import io.micronaut.core.util.StringUtils; 020import io.micronaut.maven.core.MicronautRuntime; 021import io.micronaut.maven.jib.JibConfigurationService; 022import io.micronaut.maven.jib.JibMicronautExtension; 023import io.micronaut.maven.services.ApplicationConfigurationService; 024import io.micronaut.maven.services.DockerService; 025import org.apache.maven.artifact.Artifact; 026import org.apache.maven.artifact.versioning.ArtifactVersion; 027import org.apache.maven.artifact.versioning.DefaultArtifactVersion; 028import org.apache.maven.execution.MavenSession; 029import org.apache.maven.plugin.MojoExecution; 030import org.apache.maven.plugin.PluginParameterExpressionEvaluator; 031import org.apache.maven.plugins.annotations.Parameter; 032import org.apache.maven.project.MavenProject; 033 034import java.io.File; 035import java.io.IOException; 036import java.nio.charset.Charset; 037import java.nio.file.Files; 038import java.nio.file.StandardCopyOption; 039import java.util.Arrays; 040import java.util.HashSet; 041import java.util.List; 042import java.util.Map; 043import java.util.NavigableSet; 044import java.util.Optional; 045import java.util.Set; 046import java.util.TreeSet; 047import java.util.stream.Collectors; 048 049import static io.micronaut.maven.services.ApplicationConfigurationService.DEFAULT_PORT; 050 051/** 052 * Abstract base class for mojos related to Docker files and builds. 053 * 054 * @author Álvaro Sánchez-Mariscal 055 * @author Iván López 056 * @since 1.1 057 */ 058public abstract class AbstractDockerMojo extends AbstractMicronautMojo { 059 060 public static final String LATEST_TAG = "latest"; 061 // GlibC 2.34 is used by native image 17 062 public static final String DEFAULT_BASE_IMAGE_GRAALVM_RUN = "cgr.dev/chainguard/wolfi-base:latest"; 063 public static final String MOSTLY_STATIC_NATIVE_IMAGE_GRAALVM_FLAG = "-H:+StaticExecutableWithDynamicLibC"; 064 public static final String ARM_ARCH = "aarch64"; 065 public static final String X86_64_ARCH = "x64"; 066 public static final String DEFAULT_ORACLE_LINUX_VERSION = "ol9"; 067 public static final String ORACLE_CLOUD_FUNCTION_DEFAULT_CMD = "CMD [\"io.micronaut.oraclecloud.function.http.HttpFunction::handleRequest\"]"; 068 public static final String GDS_DOWNLOAD_URL = "https://gds.oracle.com/download/graal/%s/latest-gftc/graalvm-jdk-%s_linux-%s_bin.tar.gz"; 069 private static final NavigableSet<Integer> GRAALVM_VERSIONS = new TreeSet<>(Set.of(17, 21, 25)); 070 071 protected final MavenProject mavenProject; 072 protected final MavenSession mavenSession; 073 protected final JibConfigurationService jibConfigurationService; 074 protected final ApplicationConfigurationService applicationConfigurationService; 075 protected final DockerService dockerService; 076 protected final PluginParameterExpressionEvaluator expressionEvaluator; 077 078 079 /** 080 * Additional arguments that will be passed to the <code>native-image</code> executable. Note that this will only 081 * be used when using a packaging of type <code>docker-native</code>. For <code>native-image</code> packaging 082 * you should use the 083 * <a href="https://www.graalvm.org/reference-manual/native-image/NativeImageMavenPlugin/#maven-plugin-customization"> 084 * Native Image Maven Plugin 085 * </a> configuration options. 086 */ 087 @Parameter(property = "micronaut.native-image.args") 088 protected List<String> nativeImageBuildArgs; 089 090 /** 091 * List of additional arguments that will be passed to the application. 092 */ 093 @Parameter(property = RunMojo.MN_APP_ARGS) 094 protected List<String> appArguments; 095 096 /** 097 * The main class of the application, as defined in the 098 * <a href="https://www.mojohaus.org/exec-maven-plugin/java-mojo.html#mainClass">Exec Maven Plugin</a>. 099 */ 100 @Parameter(defaultValue = RunMojo.EXEC_MAIN_CLASS, required = true) 101 protected String mainClass; 102 103 /** 104 * Whether to produce a static native image when using <code>docker-native</code> packaging. 105 */ 106 @Parameter(defaultValue = "false", property = "micronaut.native-image.static") 107 protected Boolean staticNativeImage; 108 109 /** 110 * The target runtime of the application. 111 */ 112 @Parameter(property = MicronautRuntime.PROPERTY, defaultValue = "NONE") 113 protected String micronautRuntime; 114 115 /** 116 * The Docker image used to run the native image. 117 * 118 * @since 1.2 119 */ 120 @Parameter(property = "micronaut.native-image.base-image-run", defaultValue = DEFAULT_BASE_IMAGE_GRAALVM_RUN) 121 protected String baseImageRun; 122 123 /** 124 * The version of Oracle Linux to use as a native-compile base when building a native image inside a Docker container. 125 */ 126 @Parameter(property = "micronaut.native-image.ol.version", defaultValue = DEFAULT_ORACLE_LINUX_VERSION) 127 protected String oracleLinuxVersion; 128 129 /** 130 * Networking mode for the RUN instructions during build. 131 * 132 * @since 4.0.0 133 */ 134 @Parameter(property = "docker.networkMode") 135 protected String networkMode; 136 137 /** 138 * <p> 139 * jib-maven-plugin goal used to build the Docker image (when Jib is used). Defaults to <code>dockerBuild</code>, 140 * but can be set to <code>build</code> or <code>buildTar</code> to build the image without a Docker daemon. 141 * </p> 142 * <p> 143 * Note that Jib's <code>build</code> goal with also push the image to a container registry. Check the 144 * <a href="https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#quickstart">jib-maven-plugin</a> 145 * documentation for more information. 146 * </p> 147 * 148 * @since 4.11.2 149 */ 150 @Parameter(property = "jib.buildGoal", defaultValue = "dockerBuild") 151 protected String jibBuildGoal; 152 153 protected AbstractDockerMojo(MavenProject mavenProject, JibConfigurationService jibConfigurationService, 154 ApplicationConfigurationService applicationConfigurationService, 155 DockerService dockerService, MavenSession mavenSession, MojoExecution mojoExecution) { 156 this.mavenProject = mavenProject; 157 this.mavenSession = mavenSession; 158 this.jibConfigurationService = jibConfigurationService; 159 this.applicationConfigurationService = applicationConfigurationService; 160 this.dockerService = dockerService; 161 this.expressionEvaluator = new PluginParameterExpressionEvaluator(mavenSession, mojoExecution); 162 } 163 164 /** 165 * @return the Java version from either the <code>maven.compiler.target</code> property or the <code>java.version</code> property. 166 */ 167 protected ArtifactVersion javaVersion() { 168 return new DefaultArtifactVersion(getJdkVersion()); 169 } 170 171 private String getJdkVersion() { 172 var releaseVersion = getPropertyValue(mavenProject, "maven.compiler.release"); 173 var targetVersion = getPropertyValue(mavenProject, "maven.compiler.target"); 174 return releaseVersion.or(() -> targetVersion).orElseGet(() -> System.getProperty("java.version")); 175 } 176 177 private static Optional<String> getPropertyValue(MavenProject project, String propertName) { 178 var systemProperty = Optional.of(propertName).map(System::getProperty); 179 var properties = project.getProperties(); 180 var projectProperty = Optional.of(propertName).map(properties::getProperty); 181 return systemProperty.or(() -> projectProperty); 182 } 183 184 /** 185 * @return the JVM version to use for GraalVM. 186 */ 187 protected String graalVmJvmVersion() { 188 return Integer.toString(resolveGraalVersion()); 189 } 190 191 /** 192 * @return the GraalVM download URL depending on the Java version. 193 */ 194 protected String graalVmDownloadUrl() { 195 Integer version = resolveGraalVersion(); 196 197 return GDS_DOWNLOAD_URL.formatted(version, version, graalVmArch()); 198 } 199 200 private Integer resolveGraalVersion() { 201 int target = javaVersion().getMajorVersion(); 202 Integer version = GRAALVM_VERSIONS.floor(target); 203 204 return version != null ? version : GRAALVM_VERSIONS.first(); 205 } 206 207 /** 208 * @return the OS architecture to use for GraalVM depending on the <code>os.arch</code> system property. 209 */ 210 protected String graalVmArch() { 211 return isArm() ? ARM_ARCH : X86_64_ARCH; 212 } 213 214 /** 215 * @return the base FROM image for the native image. 216 */ 217 protected String getFrom() { 218 if (Boolean.TRUE.equals(staticNativeImage)) { 219 return getFromImage().orElse("ghcr.io/graalvm/native-image-community:" + graalVmJvmVersion() + "-muslib-" + oracleLinuxVersion); 220 } else { 221 return getFromImage().orElse("ghcr.io/graalvm/native-image-community:" + graalVmJvmVersion() + "-" + oracleLinuxVersion); 222 } 223 } 224 225 /** 226 * Check os.arch against known ARM architecture identifiers. 227 * 228 * @return true if we think we're running on an arm JDK 229 */ 230 protected boolean isArm() { 231 return switch (System.getProperty("os.arch")) { 232 case ARM_ARCH, "arm64" -> true; 233 default -> false; 234 }; 235 } 236 237 /** 238 * @return the base image from the jib configuration (if any). 239 */ 240 protected Optional<String> getFromImage() { 241 return jibConfigurationService.getFromImage(); 242 } 243 244 /** 245 * @return the Docker image tags by looking at the Jib plugin configuration. 246 */ 247 protected Set<String> getTags() { 248 var tags = new HashSet<String>(); 249 Optional<String> toImageOptional = jibConfigurationService.getToImage(); 250 String imageName = mavenProject.getArtifactId(); 251 if (toImageOptional.isPresent()) { 252 String toImage = toImageOptional.get(); 253 if (toImage.contains(":")) { 254 tags.add(toImage); 255 imageName = toImageOptional.get().split(":")[0]; 256 } else { 257 tags.add(toImage + ":" + LATEST_TAG); 258 imageName = toImage; 259 } 260 } else { 261 tags.add(imageName + ":" + LATEST_TAG); 262 } 263 for (String tag : jibConfigurationService.getTags()) { 264 if (LATEST_TAG.equals(tag) && tags.stream().anyMatch(t -> t.contains(LATEST_TAG))) { 265 continue; 266 } 267 tags.add(String.format("%s:%s", imageName, tag)); 268 } 269 return tags.stream() 270 .map(this::evaluateExpression) 271 .collect(Collectors.toSet()); 272 } 273 274 private String evaluateExpression(String expression) { 275 try { 276 return expressionEvaluator.evaluate(expression, String.class).toString(); 277 } catch (Exception e) { 278 return expression; 279 } 280 } 281 282 /** 283 * @return the application ports to expose by looking at the Jib configuration or the application configuration. 284 */ 285 protected String getPorts() { 286 return jibConfigurationService.getPorts().orElseGet(() -> { 287 String port = applicationConfigurationService.getServerPort(); 288 return "-1".equals(port) ? DEFAULT_PORT : port; 289 }); 290 } 291 292 /** 293 * Copy project dependencies to a <code>target/dependency</code> directory. 294 */ 295 @SuppressWarnings("ResultOfMethodCallIgnored") 296 protected void copyDependencies() throws IOException { 297 var imageClasspathScopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME); 298 mavenProject.setArtifactFilter(artifact -> imageClasspathScopes.contains(artifact.getScope())); 299 var target = new File(mavenProject.getBuild().getDirectory(), "dependency"); 300 if (!target.exists()) { 301 target.mkdirs(); 302 } 303 for (Artifact dependency : mavenProject.getArtifacts()) { 304 Files.copy(dependency.getFile().toPath(), target.toPath().resolve(dependency.getFile().getName()), StandardCopyOption.REPLACE_EXISTING); 305 } 306 } 307 308 /** 309 * @return the Docker CMD command. 310 */ 311 protected String getCmd() { 312 return "CMD [" + 313 appArguments.stream() 314 .map(s -> "\"" + s + "\"") 315 .collect(Collectors.joining(", ")) + 316 "]"; 317 } 318 319 /** 320 * @return Networking mode for the RUN instructions during build (if any). 321 */ 322 protected Optional<String> getNetworkMode() { 323 return Optional.ofNullable(networkMode); 324 } 325 326 /** 327 * @return Map of proxy-related build arguments for Docker builds. 328 */ 329 protected Map<String, String> getProxyBuildArgs() { 330 var proxyArgs = new java.util.HashMap<String, String>(); 331 332 // HTTP proxy configuration from standard JVM properties 333 String httpProxyHost = System.getProperty("http.proxyHost"); 334 String httpProxyPort = System.getProperty("http.proxyPort", "80"); 335 if (StringUtils.hasText(httpProxyHost)) { 336 String httpProxy = "http://" + httpProxyHost + ":" + httpProxyPort; 337 proxyArgs.put("HTTP_PROXY", httpProxy); 338 proxyArgs.put("http_proxy", httpProxy); 339 } 340 341 // HTTPS proxy configuration from standard JVM properties 342 String httpsProxyHost = System.getProperty("https.proxyHost"); 343 String httpsProxyPort = System.getProperty("https.proxyPort", "443"); 344 if (StringUtils.hasText(httpsProxyHost)) { 345 String httpsProxy = "http://" + httpsProxyHost + ":" + httpsProxyPort; 346 proxyArgs.put("HTTPS_PROXY", httpsProxy); 347 proxyArgs.put("https_proxy", httpsProxy); 348 } 349 350 // No proxy configuration from standard JVM properties 351 String nonProxyHosts = System.getProperty("http.nonProxyHosts"); 352 if (StringUtils.hasText(nonProxyHosts)) { 353 // Convert Java format (e.g., "*.company.com|localhost") to standard format (e.g., "*.company.com,localhost") 354 String noProxy = nonProxyHosts.replace("|", ","); 355 proxyArgs.put("NO_PROXY", noProxy); 356 proxyArgs.put("no_proxy", noProxy); 357 } 358 359 return proxyArgs; 360 } 361 362 /** 363 * @return the base image to use for the Dockerfile. 364 */ 365 protected String getBaseImage() { 366 return JibMicronautExtension.determineBaseImage(JibMicronautExtension.getJdkVersion(mavenSession), MicronautRuntime.valueOf(micronautRuntime.toUpperCase()).getBuildStrategy()); 367 } 368 369 /** 370 * Adds cmd to docker oracle cloud function file. 371 * 372 * @param dockerfile the docker file 373 */ 374 protected void oracleCloudFunctionCmd(File dockerfile) throws IOException { 375 if (appArguments != null && !appArguments.isEmpty()) { 376 getLog().info("Using application arguments: " + appArguments); 377 com.google.common.io.Files.asCharSink(dockerfile, Charset.defaultCharset(), FileWriteMode.APPEND).write(System.lineSeparator() + getCmd()); 378 } else { 379 com.google.common.io.Files.asCharSink(dockerfile, Charset.defaultCharset(), FileWriteMode.APPEND).write(System.lineSeparator() + ORACLE_CLOUD_FUNCTION_DEFAULT_CMD); 380 } 381 } 382 383}