1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.micronaut.build;
17
18 import com.github.dockerjava.api.command.BuildImageCmd;
19 import com.google.cloud.tools.jib.api.ImageReference;
20 import com.google.cloud.tools.jib.api.InvalidImageReferenceException;
21 import io.micronaut.build.services.ApplicationConfigurationService;
22 import io.micronaut.build.services.DockerService;
23 import io.micronaut.build.services.JibConfigurationService;
24 import io.micronaut.core.annotation.Experimental;
25 import org.apache.commons.io.IOUtils;
26 import org.apache.maven.plugin.MojoExecutionException;
27 import org.apache.maven.plugins.annotations.Mojo;
28 import org.apache.maven.plugins.annotations.Parameter;
29 import org.apache.maven.plugins.annotations.ResolutionScope;
30 import org.apache.maven.project.MavenProject;
31 import org.apache.maven.shared.filtering.MavenFilteringException;
32 import org.apache.maven.shared.filtering.MavenReaderFilter;
33 import org.apache.maven.shared.filtering.MavenReaderFilterRequest;
34
35 import javax.inject.Inject;
36 import java.io.File;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.InputStreamReader;
40 import java.io.Reader;
41 import java.io.Writer;
42 import java.nio.file.Files;
43 import java.nio.file.Path;
44 import java.nio.file.StandardOpenOption;
45 import java.nio.file.attribute.PosixFilePermission;
46 import java.util.Collections;
47 import java.util.EnumSet;
48 import java.util.Properties;
49 import java.util.Set;
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 @Experimental
68 @Mojo(name = DockerCracMojo.DOCKER_CRAC_PACKAGING, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
69 public class DockerCracMojo extends AbstractDockerMojo {
70
71 public static final String DOCKER_CRAC_PACKAGING = "docker-crac";
72 public static final String CHECKPOINT_SCRIPT_NAME = "checkpoint.sh";
73 public static final String WARMUP_SCRIPT_NAME = "warmup.sh";
74 public static final String RUN_SCRIPT_NAME = "run.sh";
75 public static final String DEFAULT_READINESS_COMMAND = "curl --output /dev/null --silent --head http://localhost:8080";
76 public static final String CRAC_READINESS_PROPERTY = "crac.readiness";
77 public static final String DEFAULT_CRAC_CHECKPOINT_TIMEOUT = "60";
78 public static final String CRAC_CHECKPOINT_NETWORK_PROPERTY = "crac.checkpoint.network";
79 public static final String CRAC_CHECKPOINT_TIMEOUT_PROPERTY = "crac.checkpoint.timeout";
80
81 public static final String CRAC_JAVA_VERSION = "crac.java.version";
82 public static final String DEFAULT_CRAC_JAVA_VERSION = "17";
83
84 public static final String CRAC_ARCHITECTURE = "crac.arch";
85
86 public static final String DEFAULT_BASE_IMAGE = "ubuntu:22.04";
87
88 public static final String ARM_ARCH = "aarch64";
89 public static final String X86_64_ARCH = "amd64";
90
91 private static final EnumSet<PosixFilePermission> POSIX_FILE_PERMISSIONS = EnumSet.of(
92 PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE,
93 PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE,
94 PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_EXECUTE
95 );
96 private final MavenReaderFilter mavenReaderFilter;
97
98 @Parameter(property = DockerCracMojo.CRAC_READINESS_PROPERTY, defaultValue = DockerCracMojo.DEFAULT_READINESS_COMMAND)
99 private String readinessCommand;
100
101 @Parameter(property = DockerCracMojo.CRAC_CHECKPOINT_TIMEOUT_PROPERTY, defaultValue = DockerCracMojo.DEFAULT_CRAC_CHECKPOINT_TIMEOUT)
102 private Integer checkpointTimeoutSeconds;
103
104 @Parameter(property = DockerCracMojo.CRAC_CHECKPOINT_NETWORK_PROPERTY)
105 private String checkpointNetworkName;
106
107 @Parameter(property = DockerCracMojo.CRAC_JAVA_VERSION, defaultValue = DockerCracMojo.DEFAULT_CRAC_JAVA_VERSION)
108 private String cracJavaVersion;
109
110 @Parameter(property = DockerCracMojo.CRAC_ARCHITECTURE)
111 private String cracArchitecture;
112
113 @SuppressWarnings("CdiInjectionPointsInspection")
114 @Inject
115 public DockerCracMojo(
116 MavenProject mavenProject,
117 JibConfigurationService jibConfigurationService,
118 ApplicationConfigurationService applicationConfigurationService,
119 DockerService dockerService,
120 MavenReaderFilter mavenReaderFilter
121 ) {
122 super(mavenProject, jibConfigurationService, applicationConfigurationService, dockerService);
123 this.mavenReaderFilter = mavenReaderFilter;
124 }
125
126 @Override
127 public void execute() throws MojoExecutionException {
128 try {
129 copyDependencies();
130
131 MicronautRuntime runtime = MicronautRuntime.valueOf(micronautRuntime.toUpperCase());
132
133 switch (runtime.getBuildStrategy()) {
134 case LAMBDA:
135 throw new MojoExecutionException("Lambda Functions are currently unsupported");
136
137 case ORACLE_FUNCTION:
138 throw new MojoExecutionException("Oracle Functions are currently unsupported");
139
140 case DEFAULT:
141 default:
142 buildDockerCrac();
143 break;
144 }
145 } catch (InvalidImageReferenceException iire) {
146 String message = "Invalid image reference "
147 + iire.getInvalidReference()
148 + ", perhaps you should check that the reference is formatted correctly according to " +
149 "https://docs.docker.com/engine/reference/commandline/tag/#extended-description" +
150 "\nFor example, slash-separated name components cannot have uppercase letters";
151 throw new MojoExecutionException(message);
152 } catch (IOException | IllegalArgumentException | MavenFilteringException e) {
153 throw new MojoExecutionException(e.getMessage(), e);
154 }
155 }
156
157 private void buildDockerCrac() throws IOException, InvalidImageReferenceException, MavenFilteringException {
158 String checkpointImage = buildCheckpointDockerfile();
159 getLog().info("CRaC Checkpoint image: " + checkpointImage);
160 File checkpointDir = new File(mavenProject.getBuild().getDirectory(), "cr");
161
162 checkpointDir.mkdirs();
163 dockerService.runPrivilegedImageAndWait(
164 checkpointImage,
165 checkpointTimeoutSeconds,
166 checkpointNetworkName,
167 checkpointDir.getAbsolutePath() + ":/home/app/cr"
168 );
169 buildFinalDockerfile(checkpointImage);
170 }
171
172 private String limitArchitecture(String architecture) {
173 if (architecture == null) {
174 return null;
175 }
176 if (ARM_ARCH.equals(architecture)) {
177 return architecture;
178 }
179 return X86_64_ARCH;
180 }
181
182 private String buildCheckpointDockerfile() throws IOException, MavenFilteringException {
183 String name = mavenProject.getArtifactId() + "-crac-checkpoint";
184 Set<String> checkpointTags = Collections.singleton(name);
185 copyScripts(CHECKPOINT_SCRIPT_NAME, WARMUP_SCRIPT_NAME, RUN_SCRIPT_NAME);
186 File dockerfile = dockerService.loadDockerfileAsResource(DockerfileMojo.DOCKERFILE_CRAC_CHECKPOINT);
187
188 String systemArchitecture = limitArchitecture(System.getProperty("os.arch"));
189 String filteredCracArchitecture = limitArchitecture(cracArchitecture);
190 String finalArchitecture = filteredCracArchitecture == null ? systemArchitecture : filteredCracArchitecture;
191
192 BuildImageCmd buildImageCmd = dockerService.buildImageCmd()
193 .withDockerfile(dockerfile)
194 .withBuildArg("BASE_IMAGE", getFromImage().orElse(DEFAULT_BASE_IMAGE))
195 .withBuildArg("CRAC_ARCH", finalArchitecture)
196 .withBuildArg("CRAC_JDK_VERSION", cracJavaVersion)
197 .withTags(checkpointTags);
198 dockerService.buildImage(buildImageCmd);
199 return name;
200 }
201
202 private void buildFinalDockerfile(String checkpointContainerId) throws IOException, InvalidImageReferenceException, MavenFilteringException {
203 Set<String> tags = getTags();
204 for (String tag : tags) {
205 ImageReference.parse(tag);
206 }
207 copyScripts(RUN_SCRIPT_NAME);
208 File dockerfile = dockerService.loadDockerfileAsResource(DockerfileMojo.DOCKERFILE_CRAC);
209 BuildImageCmd buildImageCmd = dockerService.buildImageCmd()
210 .withDockerfile(dockerfile)
211 .withBuildArg("BASE_IMAGE", getFromImage().orElse(DEFAULT_BASE_IMAGE))
212 .withBuildArg("CHECKPOINT_IMAGE", checkpointContainerId)
213 .withTags(getTags());
214 dockerService.buildImage(buildImageCmd);
215
216 getLog().warn("**********************************************************");
217 getLog().warn(" CRaC checkpoint files may contain sensitive information.");
218 getLog().warn("**********************************************************");
219 }
220
221 private Properties replacementProperties(String readinessCommand, String mainClass) {
222 Properties properties = new Properties();
223 properties.setProperty("READINESS", readinessCommand);
224 properties.setProperty("MAINCLASS", mainClass);
225 return properties;
226 }
227
228 private void copyScripts(String... scriptNames) throws IOException, MavenFilteringException {
229 File target = new File(mavenProject.getBuild().getDirectory(), "scripts");
230 if (!target.exists()) {
231 target.mkdirs();
232 }
233 processScripts(target, replacementProperties(readinessCommand, mainClass), scriptNames);
234 }
235
236 private void processScripts(File target, Properties replacements, String... scriptNames) throws IOException, MavenFilteringException {
237 for (String script : scriptNames) {
238 File localOverride = new File(mavenProject.getBasedir(), script);
239 InputStream resourceStream = DockerCracMojo.class.getResourceAsStream("/cracScripts/" + script);
240 Reader resourceReader = resourceStream == null ? null : new InputStreamReader(resourceStream);
241 try (Reader reader = localOverride.exists() ? Files.newBufferedReader(localOverride.toPath()) : resourceReader) {
242 if (reader == null) {
243 throw new IOException("Could not find script " + script);
244 }
245 MavenReaderFilterRequest req = new MavenReaderFilterRequest();
246 req.setFrom(reader);
247 req.setFiltering(true);
248 req.setAdditionalProperties(replacements);
249 Path outputPath = target.toPath().resolve(script);
250 try (Writer writer = Files.newBufferedWriter(outputPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
251 IOUtils.copy(mavenReaderFilter.filter(req), writer);
252 }
253 Files.setPosixFilePermissions(outputPath, POSIX_FILE_PERMISSIONS);
254 }
255 }
256 }
257 }