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