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