View Javadoc
1   /*
2    * Copyright 2017-2022 original authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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   * <p>Implementation of the <code>docker-crac</code> packaging.</p>
53   * <p><strong>WARNING</strong>: this goal is not intended to be executed directly. Instead, specify the packaging type
54   * using the <code>packaging</code> property, eg:</p>
55   *
56   * <pre>mvn package -Dpackaging=docker-crac</pre>
57   * <p>
58   * This is a two stage process. First a docker image is built that runs the application under a CRaC enabled JDK. Then
59   * the application is warmed up via a shell script. And then a checkpoint is taken via a signal using jcmd.
60   * <p>
61   * The second stage takes this checkpoint, and creates the final image containing it plus a run script which passes the
62   * correct flags to the CRaC enabled JDK.
63   *
64   * @author Tim Yates
65   * @since 3.5.0
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         // We need to make this folder first, or else Docker on linux will make it as root and break clean on CI
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 }