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.github.dockerjava.api.command.BuildImageCmd; 019import com.google.cloud.tools.jib.api.ImageReference; 020import com.google.cloud.tools.jib.api.InvalidImageReferenceException; 021import io.micronaut.core.annotation.Experimental; 022import io.micronaut.maven.core.MicronautRuntime; 023import io.micronaut.maven.jib.JibConfigurationService; 024import io.micronaut.maven.services.ApplicationConfigurationService; 025import io.micronaut.maven.services.DockerService; 026import org.apache.commons.io.IOUtils; 027import org.apache.maven.execution.MavenSession; 028import org.apache.maven.plugin.MojoExecution; 029import org.apache.maven.plugin.MojoExecutionException; 030import org.apache.maven.plugins.annotations.Mojo; 031import org.apache.maven.plugins.annotations.Parameter; 032import org.apache.maven.plugins.annotations.ResolutionScope; 033import org.apache.maven.project.MavenProject; 034import org.apache.maven.shared.filtering.MavenFilteringException; 035import org.apache.maven.shared.filtering.MavenReaderFilter; 036import org.apache.maven.shared.filtering.MavenReaderFilterRequest; 037 038import javax.inject.Inject; 039import java.io.File; 040import java.io.IOException; 041import java.io.InputStream; 042import java.io.InputStreamReader; 043import java.io.Reader; 044import java.io.Writer; 045import java.nio.file.Files; 046import java.nio.file.Path; 047import java.nio.file.StandardOpenOption; 048import java.nio.file.attribute.PosixFilePermission; 049import java.util.Collections; 050import java.util.EnumSet; 051import java.util.Properties; 052 053/** 054 * <p>Implementation of the <code>docker-crac</code> packaging.</p> 055 * <p><strong>WARNING</strong>: this goal is not intended to be executed directly. Instead, specify the packaging type 056 * using the <code>packaging</code> property, eg:</p> 057 * 058 * <pre>mvn package -Dpackaging=docker-crac</pre> 059 * <p> 060 * This is a two stage process. First a docker image is built that runs the application under a CRaC enabled JDK. Then 061 * the application is warmed up via a shell script. And then a checkpoint is taken via a signal using jcmd. 062 * <p> 063 * The second stage takes this checkpoint, and creates the final image containing it plus a run script which passes the 064 * correct flags to the CRaC enabled JDK. 065 * 066 * @author Tim Yates 067 * @since 3.5.0 068 */ 069@Experimental 070@Mojo(name = DockerCracMojo.DOCKER_CRAC_PACKAGING, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) 071public class DockerCracMojo extends AbstractDockerMojo { 072 073 public static final String DOCKER_CRAC_PACKAGING = "docker-crac"; 074 public static final String CHECKPOINT_SCRIPT_NAME = "checkpoint.sh"; 075 public static final String WARMUP_SCRIPT_NAME = "warmup.sh"; 076 public static final String RUN_SCRIPT_NAME = "run.sh"; 077 public static final String DEFAULT_READINESS_COMMAND = "curl --output /dev/null --silent --head http://localhost:8080"; 078 public static final String CRAC_READINESS_PROPERTY = "crac.readiness"; 079 public static final String DEFAULT_CRAC_CHECKPOINT_TIMEOUT = "60"; 080 public static final String CRAC_CHECKPOINT_NETWORK_PROPERTY = "crac.checkpoint.network"; 081 public static final String CRAC_CHECKPOINT_TIMEOUT_PROPERTY = "crac.checkpoint.timeout"; 082 083 public static final String CRAC_JAVA_VERSION = "crac.java.version"; 084 085 public static final String CRAC_ARCHITECTURE = "crac.arch"; 086 087 public static final String CRAC_OS = "crac.os"; 088 public static final String DEFAULT_CRAC_OS = "linux-glibc"; 089 090 public static final String DEFAULT_BASE_IMAGE = "ubuntu:22.04"; 091 092 public static final String ARM_ARCH = "aarch64"; 093 public static final String X86_64_ARCH = "amd64"; 094 095 private static final EnumSet<PosixFilePermission> POSIX_FILE_PERMISSIONS = EnumSet.of( 096 PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, 097 PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, 098 PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_EXECUTE 099 ); 100 private final MavenReaderFilter mavenReaderFilter; 101 102 /** 103 * The command to execute to determine if the application is ready to receive traffic. 104 */ 105 @Parameter(property = DockerCracMojo.CRAC_READINESS_PROPERTY, defaultValue = DockerCracMojo.DEFAULT_READINESS_COMMAND) 106 private String readinessCommand; 107 108 /** 109 * The timeout in seconds to wait for the checkpoint to complete. 110 */ 111 @Parameter(property = DockerCracMojo.CRAC_CHECKPOINT_TIMEOUT_PROPERTY, defaultValue = DockerCracMojo.DEFAULT_CRAC_CHECKPOINT_TIMEOUT) 112 private Integer checkpointTimeoutSeconds; 113 114 /** 115 * The name of the docker network to run the checkpoint container in. 116 */ 117 @Parameter(property = DockerCracMojo.CRAC_CHECKPOINT_NETWORK_PROPERTY) 118 private String checkpointNetworkName; 119 120 /** 121 * The version of the Azul CRaC enabled JDK to use. 122 */ 123 @Parameter(property = DockerCracMojo.CRAC_JAVA_VERSION, defaultValue = "${jdk.version}") 124 private String cracJavaVersion; 125 126 /** 127 * The architecture to use for the CRaC enabled JDK. Defaults to {@code os.arch} 128 */ 129 @Parameter(property = DockerCracMojo.CRAC_ARCHITECTURE) 130 private String cracArchitecture; 131 132 /** 133 * The os to use for the CRaC enabled JDK. 134 */ 135 @Parameter(property = DockerCracMojo.CRAC_OS, defaultValue = DEFAULT_CRAC_OS) 136 private String cracOs; 137 138 @SuppressWarnings("CdiInjectionPointsInspection") 139 @Inject 140 public DockerCracMojo( 141 MavenProject mavenProject, 142 JibConfigurationService jibConfigurationService, 143 ApplicationConfigurationService applicationConfigurationService, 144 DockerService dockerService, 145 MavenReaderFilter mavenReaderFilter, 146 MavenSession mavenSession, 147 MojoExecution mojoExecution 148 ) { 149 super(mavenProject, jibConfigurationService, applicationConfigurationService, dockerService, mavenSession, mojoExecution); 150 this.mavenReaderFilter = mavenReaderFilter; 151 } 152 153 @Override 154 public void execute() throws MojoExecutionException { 155 try { 156 copyDependencies(); 157 158 MicronautRuntime runtime = MicronautRuntime.valueOf(micronautRuntime.toUpperCase()); 159 160 switch (runtime.getBuildStrategy()) { 161 case LAMBDA -> throw new MojoExecutionException("Lambda Functions are currently unsupported"); 162 case ORACLE_FUNCTION -> throw new MojoExecutionException("Oracle Functions are currently unsupported"); 163 case DEFAULT -> buildDockerCrac(); 164 default -> throw new IllegalStateException("Unexpected value: " + runtime.getBuildStrategy()); 165 } 166 } catch (InvalidImageReferenceException iire) { 167 String message = "Invalid image reference " 168 + iire.getInvalidReference() 169 + ", perhaps you should check that the reference is formatted correctly according to " + 170 "https://docs.docker.com/engine/reference/commandline/tag/#extended-description" + 171 "\nFor example, slash-separated name components cannot have uppercase letters"; 172 throw new MojoExecutionException(message); 173 } catch (IOException | IllegalArgumentException | MavenFilteringException e) { 174 throw new MojoExecutionException(e.getMessage(), e); 175 } 176 } 177 178 private void buildDockerCrac() throws IOException, InvalidImageReferenceException, MavenFilteringException { 179 String checkpointImage = buildCheckpointDockerfile(); 180 getLog().info("CRaC Checkpoint image: " + checkpointImage); 181 File checkpointDir = new File(mavenProject.getBuild().getDirectory(), "cr"); 182 // We need to make this folder first, or else Docker on linux will make it as root and break clean on CI 183 checkpointDir.mkdirs(); 184 dockerService.runPrivilegedImageAndWait( 185 checkpointImage, 186 checkpointTimeoutSeconds, 187 checkpointNetworkName, 188 checkpointDir.getAbsolutePath() + ":/home/app/cr" 189 ); 190 buildFinalDockerfile(checkpointImage); 191 } 192 193 private String limitArchitecture(String architecture) { 194 if (architecture == null) { 195 return null; 196 } 197 if (ARM_ARCH.equals(architecture)) { 198 return architecture; 199 } 200 return X86_64_ARCH; 201 } 202 203 private String buildCheckpointDockerfile() throws IOException, MavenFilteringException { 204 String name = mavenProject.getArtifactId() + "-crac-checkpoint"; 205 var checkpointTags = Collections.singleton(name); 206 copyScripts(CHECKPOINT_SCRIPT_NAME, WARMUP_SCRIPT_NAME, RUN_SCRIPT_NAME); 207 File dockerfile = dockerService.loadDockerfileAsResource(DockerfileMojo.DOCKERFILE_CRAC_CHECKPOINT); 208 209 String systemArchitecture = limitArchitecture(System.getProperty("os.arch")); 210 String filteredCracArchitecture = limitArchitecture(cracArchitecture); 211 String finalArchitecture = filteredCracArchitecture == null ? systemArchitecture : filteredCracArchitecture; 212 String baseImage = getFromImage().orElse(DEFAULT_BASE_IMAGE); 213 214 getLog().info("Using BASE_IMAGE: " + baseImage); 215 getLog().info("Using CRAC_ARCH: " + finalArchitecture); 216 getLog().info("Using CRAC_JDK_VERSION: " + cracJavaVersion); 217 getLog().info("Using CRAC_OS: " + cracOs); 218 219 BuildImageCmd buildImageCmd = dockerService.buildImageCmd() 220 .withDockerfile(dockerfile) 221 .withBuildArg("BASE_IMAGE", baseImage) 222 .withBuildArg("CRAC_ARCH", finalArchitecture) 223 .withBuildArg("CRAC_OS", cracOs) 224 .withBuildArg("CRAC_JDK_VERSION", cracJavaVersion) 225 .withTags(checkpointTags); 226 getNetworkMode().ifPresent(buildImageCmd::withNetworkMode); 227 dockerService.buildImage(buildImageCmd); 228 return name; 229 } 230 231 private void buildFinalDockerfile(String checkpointContainerId) throws IOException, InvalidImageReferenceException, MavenFilteringException { 232 var tags = getTags(); 233 for (String tag : tags) { 234 ImageReference.parse(tag); 235 } 236 237 String ports = getPorts(); 238 getLog().info("Exposing port(s): " + ports); 239 240 copyScripts(RUN_SCRIPT_NAME); 241 File dockerfile = dockerService.loadDockerfileAsResource(DockerfileMojo.DOCKERFILE_CRAC); 242 String baseImage = getFromImage().orElse(DEFAULT_BASE_IMAGE); 243 244 getLog().info("Using BASE_IMAGE: " + baseImage); 245 getLog().info("Using CHECKPOINT_IMAGE: " + checkpointContainerId); 246 247 BuildImageCmd buildImageCmd = dockerService.buildImageCmd() 248 .withDockerfile(dockerfile) 249 .withBuildArg("PORTS", ports) 250 .withBuildArg("BASE_IMAGE", getFromImage().orElse(DEFAULT_BASE_IMAGE)) 251 .withBuildArg("CHECKPOINT_IMAGE", checkpointContainerId) 252 .withTags(getTags()); 253 getNetworkMode().ifPresent(buildImageCmd::withNetworkMode); 254 dockerService.buildImage(buildImageCmd); 255 256 getLog().warn("**********************************************************"); 257 getLog().warn(" CRaC checkpoint files may contain sensitive information."); 258 getLog().warn("**********************************************************"); 259 } 260 261 private Properties replacementProperties(String readinessCommand, String mainClass) { 262 Properties properties = new Properties(); 263 properties.setProperty("READINESS", readinessCommand); 264 properties.setProperty("MAINCLASS", mainClass); 265 return properties; 266 } 267 268 private void copyScripts(String... scriptNames) throws IOException, MavenFilteringException { 269 var target = new File(mavenProject.getBuild().getDirectory(), "scripts"); 270 if (!target.exists()) { 271 target.mkdirs(); 272 } 273 processScripts(target, replacementProperties(readinessCommand, mainClass), scriptNames); 274 } 275 276 private void processScripts(File target, Properties replacements, String... scriptNames) throws IOException, MavenFilteringException { 277 for (String script : scriptNames) { 278 var localOverride = new File(mavenProject.getBasedir(), script); 279 InputStream resourceStream = DockerCracMojo.class.getResourceAsStream("/cracScripts/" + script); 280 Reader resourceReader = resourceStream == null ? null : new InputStreamReader(resourceStream); 281 try (Reader reader = localOverride.exists() ? Files.newBufferedReader(localOverride.toPath()) : resourceReader) { 282 if (reader == null) { 283 throw new IOException("Could not find script " + script); 284 } 285 var req = new MavenReaderFilterRequest(); 286 req.setFrom(reader); 287 req.setFiltering(true); 288 req.setAdditionalProperties(replacements); 289 Path outputPath = target.toPath().resolve(script); 290 try (Writer writer = Files.newBufferedWriter(outputPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { 291 IOUtils.copy(mavenReaderFilter.filter(req), writer); 292 } 293 Files.setPosixFilePermissions(outputPath, POSIX_FILE_PERMISSIONS); 294 } 295 } 296 } 297}