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