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.services;
17  
18  import com.github.dockerjava.api.DockerClient;
19  import com.github.dockerjava.api.command.BuildImageCmd;
20  import com.github.dockerjava.api.command.BuildImageResultCallback;
21  import com.github.dockerjava.api.command.CreateContainerCmd;
22  import com.github.dockerjava.api.command.CreateContainerResponse;
23  import com.github.dockerjava.api.command.PushImageCmd;
24  import com.github.dockerjava.api.command.StartContainerCmd;
25  import com.github.dockerjava.api.command.WaitContainerCmd;
26  import com.github.dockerjava.api.command.WaitContainerResultCallback;
27  import com.github.dockerjava.api.exception.DockerClientException;
28  import com.github.dockerjava.api.model.Bind;
29  import com.github.dockerjava.api.model.BuildResponseItem;
30  import com.github.dockerjava.api.model.HostConfig;
31  import com.github.dockerjava.core.DefaultDockerClientConfig;
32  import com.github.dockerjava.core.DockerClientConfig;
33  import com.github.dockerjava.core.DockerClientImpl;
34  import com.github.dockerjava.transport.DockerHttpClient;
35  import com.github.dockerjava.zerodep.ZerodepDockerHttpClient;
36  import io.micronaut.build.DockerfileMojo;
37  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
38  import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
39  import org.apache.commons.compress.utils.IOUtils;
40  import org.apache.commons.io.FileUtils;
41  import org.apache.commons.lang3.StringUtils;
42  import org.apache.maven.project.MavenProject;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  import javax.inject.Inject;
47  import javax.inject.Singleton;
48  import java.io.File;
49  import java.io.IOException;
50  import java.io.InputStream;
51  import java.nio.file.Files;
52  import java.util.concurrent.TimeUnit;
53  
54  /**
55   * Provides methods to work with Docker images.
56   *
57   * @author Álvaro Sánchez-Mariscal
58   * @since 1.1
59   */
60  @Singleton
61  public class DockerService {
62  
63      private static final Logger LOG = LoggerFactory.getLogger(DockerService.class);
64  
65      private final DockerClient dockerClient;
66      private final MavenProject mavenProject;
67  
68      @SuppressWarnings("CdiInjectionPointsInspection")
69      @Inject
70      public DockerService(MavenProject mavenProject) {
71          this.mavenProject = mavenProject;
72          DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
73          DockerHttpClient httpClient = new ZerodepDockerHttpClient.Builder()
74                  .dockerHost(config.getDockerHost())
75                  .sslConfig(config.getSSLConfig())
76                  .build();
77          dockerClient = DockerClientImpl.getInstance(config, httpClient);
78      }
79  
80      /**
81       * Creates the {@link BuildImageCmd} by loading the given Dockerfile as classpath resource.
82       */
83      public BuildImageCmd buildImageCmd(String dockerfileName) throws IOException {
84          return dockerClient.buildImageCmd(loadDockerfileAsResource(dockerfileName));
85      }
86  
87      /**
88       * Creates a default {@link BuildImageCmd}.
89       */
90      public BuildImageCmd buildImageCmd() {
91          return dockerClient.buildImageCmd();
92      }
93  
94      /**
95       * Builds the Docker image from the given {@link BuildImageCmd} builder.
96       *
97       * @return The resulting image ID.
98       */
99      public String buildImage(BuildImageCmd builder) {
100         BuildImageResultCallback resultCallback = new BuildImageResultCallback() {
101             @Override
102             public void onNext(BuildResponseItem item) {
103                 super.onNext(item);
104                 if (item.isErrorIndicated() && item.getErrorDetail() != null) {
105                     LOG.error(item.getErrorDetail().getMessage());
106                 } else if (item.getStream() != null) {
107                     String msg = StringUtils.removeEnd(item.getStream(), System.lineSeparator());
108                     LOG.info(msg);
109                 }
110             }
111         };
112 
113         return builder
114                 .exec(resultCallback)
115                 .awaitImageId();
116     }
117 
118     /**
119      * Creates a container based on a given image, and runs it.
120      * @param imageId the image to use
121      * @param timeoutSeconds the timeout in seconds for the container to finish execution
122      * @param binds the bind mounts to use
123      */
124     public void runPrivilegedImageAndWait(String imageId, Integer timeoutSeconds, String checkpointNetworkName, String... binds) throws IOException {
125         try (CreateContainerCmd create = dockerClient.createContainerCmd(imageId)) {
126             HostConfig hostConfig = create.getHostConfig();
127             if (hostConfig == null) {
128                 throw new DockerClientException("When setting binds and privileged, hostConfig was null.  Please check your docker installation and try again");
129             }
130             hostConfig.withPrivileged(true);
131             if (checkpointNetworkName != null) {
132                 hostConfig.withNetworkMode(checkpointNetworkName);
133             }
134             for (String bind : binds) {
135                 hostConfig.withBinds(Bind.parse(bind));
136             }
137             CreateContainerResponse createResponse = create.exec();
138             try (StartContainerCmd start = dockerClient.startContainerCmd(createResponse.getId())) {
139                 start.exec();
140                 LOG.info("Container started: {} {}", createResponse.getId(), start.getContainerId());
141                 try (WaitContainerCmd wait = dockerClient.waitContainerCmd(createResponse.getId())) {
142                     WaitContainerResultCallback waitResult = wait.start();
143                     LOG.info("Waiting {} seconds for completion", timeoutSeconds);
144                     Integer exitcode = waitResult.awaitStatusCode(timeoutSeconds, TimeUnit.SECONDS);
145                     if (exitcode != 0) {
146                         throw new IOException("Image " + imageId + " exited with code " + exitcode);
147                     }
148                 }
149             }
150         }
151     }
152 
153     /**
154      * Copies a file from the specified container path in the given image ID, into a temporal location.
155      */
156     public File copyFromContainer(String imageId, String containerPath) {
157         CreateContainerCmd containerCmd = dockerClient.createContainerCmd(imageId);
158         CreateContainerResponse container = containerCmd.exec();
159         dockerClient.startContainerCmd(container.getId());
160         InputStream nativeImage = dockerClient.copyArchiveFromContainerCmd(container.getId(), containerPath).exec();
161 
162         try (TarArchiveInputStream fin = new TarArchiveInputStream(nativeImage)) {
163             TarArchiveEntry tarEntry = fin.getNextTarEntry();
164             File file = new File(mavenProject.getBuild().getDirectory(), tarEntry.getName());
165             if (!file.getCanonicalFile().toPath().startsWith(mavenProject.getBuild().getDirectory())) {
166                 throw new IOException("Entry is outside of the target directory");
167             }
168 
169             IOUtils.copy(fin, Files.newOutputStream(file.toPath()));
170 
171             return file;
172         } catch (IOException e) {
173             LOG.error("Failed to copy file from container", e);
174         } finally {
175             containerCmd.close();
176         }
177         return null;
178     }
179 
180     /**
181      * Loads the given Dockerfile as classpath resource and copies it into a temporary location in the target directory.
182      */
183     public File loadDockerfileAsResource(String name) throws IOException {
184         return loadDockerfileAsResource(name, DockerfileMojo.DOCKERFILE);
185     }
186 
187     /**
188      * Loads the given Dockerfile as classpath resource and copies it into a temporary location in the target directory.
189      */
190     public File loadDockerfileAsResource(String name, String targetFileName) throws IOException {
191         String path = "/dockerfiles/" + name;
192         InputStream stream = getClass().getResourceAsStream(path);
193         if (stream != null) {
194             File dockerfile = new File(mavenProject.getBuild().getDirectory(), targetFileName);
195             FileUtils.copyInputStreamToFile(stream, dockerfile);
196             return dockerfile;
197         }
198         return null;
199     }
200 
201     /**
202      * Creates a {@link PushImageCmd} from the given image name.
203      */
204     public PushImageCmd pushImageCmd(String imageName) {
205         return dockerClient.pushImageCmd(imageName);
206     }
207 }