Skip to content

Commit e820c21

Browse files
committed
Introduce the notion of a Dockerfile, which can read specifications
and generate the resultant TAR file. Splitting the file down into statements makes it easier to understand and extend, particularly around property replacements. This separates out the functionality from BuildImageCmd so that it can be tested separately, and re-used elsewhere. Signed-off-by: Nigel Magnay <[email protected]>
1 parent dae60a4 commit e820c21

3 files changed

Lines changed: 540 additions & 0 deletions

File tree

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package com.github.dockerjava.core.dockerfile;
2+
3+
import com.github.dockerjava.api.DockerClientException;
4+
import com.github.dockerjava.core.CompressArchiveUtil;
5+
import com.github.dockerjava.core.GoLangFileMatch;
6+
import com.github.dockerjava.core.GoLangFileMatchException;
7+
import com.github.dockerjava.core.GoLangMatchFileFilter;
8+
9+
import org.apache.commons.io.FileUtils;
10+
import org.apache.commons.io.FilenameUtils;
11+
import org.apache.commons.io.filefilter.TrueFileFilter;
12+
13+
import java.io.File;
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.util.ArrayList;
17+
import java.util.Collection;
18+
import java.util.HashMap;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.UUID;
22+
23+
import jersey.repackaged.com.google.common.base.Function;
24+
import jersey.repackaged.com.google.common.base.Objects;
25+
import jersey.repackaged.com.google.common.base.Optional;
26+
import jersey.repackaged.com.google.common.base.Predicate;
27+
import jersey.repackaged.com.google.common.collect.Collections2;
28+
29+
/**
30+
* Parse a Dockerfile.
31+
*/
32+
public class Dockerfile {
33+
34+
public final File dockerFile;
35+
36+
public Dockerfile(File dockerFile) {
37+
38+
if (!dockerFile.exists()) {
39+
throw new IllegalStateException(
40+
String.format("Dockerfile %s does not exist", dockerFile.getAbsolutePath()));
41+
}
42+
43+
if (!dockerFile.isFile()) {
44+
throw new IllegalStateException(
45+
String.format("Dockerfile %s is not a file", dockerFile.getAbsolutePath()));
46+
}
47+
48+
this.dockerFile = dockerFile;
49+
50+
}
51+
52+
private static class LineTransformer
53+
implements Function<String, Optional<? extends DockerfileStatement>> {
54+
55+
private int line = 0;
56+
57+
@Override
58+
public Optional<? extends DockerfileStatement> apply(String input) {
59+
try {
60+
line++;
61+
return DockerfileStatement.createFromLine(input);
62+
63+
} catch (Exception ex) {
64+
throw new DockerClientException("Error on dockerfile line " + line);
65+
}
66+
}
67+
}
68+
69+
/**
70+
* Not needed in modern guava
71+
*/
72+
private static class MissingOptionalFilter
73+
implements Predicate<Optional<? extends DockerfileStatement>> {
74+
75+
@Override
76+
public boolean apply(Optional<? extends DockerfileStatement> optional) {
77+
return (optional.orNull() != null);
78+
}
79+
}
80+
81+
/**
82+
* Not needed in modern guava
83+
*/
84+
private static class OptionalItemTransformer
85+
implements Function<Optional<? extends DockerfileStatement>, DockerfileStatement> {
86+
87+
@Override
88+
public DockerfileStatement apply(Optional<? extends DockerfileStatement> optional) {
89+
return optional.orNull();
90+
}
91+
}
92+
93+
public Collection<DockerfileStatement> getStatements() throws IOException {
94+
Collection<String> dockerFileContent = FileUtils.readLines(dockerFile);
95+
96+
if (dockerFileContent.size() <= 0) {
97+
throw new DockerClientException(String.format(
98+
"Dockerfile %s is empty", dockerFile));
99+
}
100+
101+
Collection<Optional<? extends DockerfileStatement>> optionals = Collections2
102+
.transform(dockerFileContent, new LineTransformer());
103+
104+
// Modern guava would be done here,
105+
// With simply return Optional.presentInstances( optionals );
106+
//
107+
// So this entire function could simply be
108+
// return Optional.presentInstances( Collections2.transform( FileUtils.readLines(dockerFile), new LineTransformer() ) );
109+
//
110+
// Until the dawn of that day, do it manually
111+
112+
return Collections2.transform(Collections2.filter(optionals, new MissingOptionalFilter()),
113+
new OptionalItemTransformer());
114+
115+
}
116+
117+
public List<String> getIgnores() throws IOException {
118+
List<String> ignores = new ArrayList<String>();
119+
File dockerIgnoreFile = new File(getDockerFolder(), ".dockerignore");
120+
if (dockerIgnoreFile.exists()) {
121+
int lineNumber = 0;
122+
List<String> dockerIgnoreFileContent = FileUtils.readLines(dockerIgnoreFile);
123+
for (String pattern : dockerIgnoreFileContent) {
124+
lineNumber++;
125+
pattern = pattern.trim();
126+
if (pattern.isEmpty()) {
127+
continue; // skip empty lines
128+
}
129+
pattern = FilenameUtils.normalize(pattern);
130+
try {
131+
// validate pattern and make sure we aren't excluding Dockerfile
132+
if (GoLangFileMatch.match(pattern, "Dockerfile")) {
133+
throw new DockerClientException(
134+
String.format(
135+
"Dockerfile is excluded by pattern '%s' on line %s in .dockerignore file",
136+
pattern, lineNumber));
137+
}
138+
ignores.add(pattern);
139+
} catch (GoLangFileMatchException e) {
140+
throw new DockerClientException(String.format(
141+
"Invalid pattern '%s' on line %s in .dockerignore file", pattern, lineNumber));
142+
}
143+
}
144+
}
145+
return ignores;
146+
}
147+
148+
149+
public ScannedResult parse() throws IOException {
150+
return new ScannedResult();
151+
}
152+
153+
154+
public File getDockerFolder() {
155+
return dockerFile.getParentFile();
156+
}
157+
158+
159+
/**
160+
* Result of scanning / parsing a docker file.
161+
*/
162+
public class ScannedResult {
163+
164+
final List<String> ignores;
165+
final Map<String, String> environmentMap = new HashMap<String, String>();
166+
final List<File> filesToAdd = new ArrayList<File>();
167+
168+
public InputStream buildDockerFolderTar() {
169+
170+
// ARCHIVE TAR
171+
File dockerFolderTar = null;
172+
173+
try {
174+
String archiveNameWithOutExtension = UUID.randomUUID().toString();
175+
176+
dockerFolderTar = CompressArchiveUtil.archiveTARFiles(getDockerFolder(),
177+
filesToAdd,
178+
archiveNameWithOutExtension);
179+
return FileUtils.openInputStream(dockerFolderTar);
180+
181+
} catch (IOException ex) {
182+
FileUtils.deleteQuietly(dockerFolderTar);
183+
throw new DockerClientException(
184+
"Error occurred while preparing Docker context folder.", ex);
185+
}
186+
}
187+
188+
@Override
189+
public String toString() {
190+
return Objects.toStringHelper(this)
191+
.add("ignores", ignores)
192+
.add("environmentMap", environmentMap)
193+
.add("filesToAdd", filesToAdd)
194+
.toString();
195+
}
196+
197+
public ScannedResult() throws IOException {
198+
199+
ignores = getIgnores();
200+
filesToAdd.add(dockerFile);
201+
202+
for (DockerfileStatement statement : getStatements()) {
203+
if (statement instanceof DockerfileStatement.Env) {
204+
processEnvStatement((DockerfileStatement.Env) statement);
205+
} else if (statement instanceof DockerfileStatement.Add) {
206+
processAddStatement((DockerfileStatement.Add) statement);
207+
}
208+
}
209+
}
210+
211+
private void processAddStatement(DockerfileStatement.Add add) throws IOException {
212+
213+
add = add.transform(environmentMap);
214+
215+
if (add.isFileResource()) {
216+
217+
File dockerFolder = getDockerFolder();
218+
String resource = add.source;
219+
220+
File src = new File(resource);
221+
if (!src.isAbsolute()) {
222+
src = new File(dockerFolder, resource)
223+
.getCanonicalFile();
224+
} else {
225+
throw new DockerClientException(String.format(
226+
"Source file %s must be relative to %s",
227+
src, dockerFolder));
228+
}
229+
230+
// if (!src.exists()) {
231+
// throw new DockerClientException(String.format(
232+
// "Source file %s doesn't exist", src));
233+
// }
234+
if (src.isDirectory()) {
235+
Collection<File> files = FileUtils.listFiles(src,
236+
new GoLangMatchFileFilter(src, ignores),
237+
TrueFileFilter.INSTANCE);
238+
filesToAdd.addAll(files);
239+
} else if (!src.exists()) {
240+
filesToAdd.addAll(resolveWildcards(src, ignores));
241+
} else if (!GoLangFileMatch.match(ignores,
242+
CompressArchiveUtil.relativize(dockerFolder,
243+
src))) {
244+
filesToAdd.add(src);
245+
} else {
246+
throw new DockerClientException(
247+
String.format(
248+
"Source file %s is excluded by .dockerignore file",
249+
src));
250+
}
251+
}
252+
}
253+
254+
private Collection<File> resolveWildcards(File file, List<String> ignores) {
255+
List<File> filesToAdd = new ArrayList<File>();
256+
257+
File parent = file.getParentFile();
258+
if (parent != null) {
259+
if (parent.isDirectory()) {
260+
Collection<File> files = FileUtils.listFiles(parent,
261+
new GoLangMatchFileFilter(parent, ignores),
262+
TrueFileFilter.INSTANCE);
263+
filesToAdd.addAll(files);
264+
} else {
265+
filesToAdd.addAll(resolveWildcards(parent, ignores));
266+
}
267+
} else {
268+
throw new DockerClientException(String.format(
269+
"Source file %s doesn't exist", file));
270+
}
271+
272+
return filesToAdd;
273+
}
274+
275+
private void processEnvStatement(DockerfileStatement.Env env) {
276+
277+
environmentMap.put(env.variable, env.value);
278+
}
279+
280+
}
281+
282+
}

0 commit comments

Comments
 (0)