io.fabric8.maven.docker.assembly.DockerFileBuilder Maven / Gradle / Ivy
The newest version!
package io.fabric8.maven.docker.assembly;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.base.Joiner;
import io.fabric8.maven.docker.config.Arguments;
import io.fabric8.maven.docker.config.HealthCheckConfiguration;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
/**
* Create a dockerfile
*
* @author roland
* @since 17.04.14
*/
public class DockerFileBuilder {
private static final Joiner JOIN_ON_COMMA = Joiner.on("\",\"");
private static final Pattern ENV_VAR_PATTERN = Pattern.compile("^\\$(\\{[a-zA-Z0-9_]+\\}|[a-zA-Z0-9_]+).*");
// Base image to use as from
private String baseImage;
// Maintainer of this image
private String maintainer;
// Workdir
private String workdir = null;
// Basedir to be export
private String basedir = "/maven";
private Arguments entryPoint;
private Arguments cmd;
private Boolean exportTargetDir = null;
// User under which the files should be added
private String assemblyUser;
// User to run as
private String user;
private HealthCheckConfiguration healthCheck;
// List of files to add. Source and destination follow except that destination
// in interpreted as a relative path to the exportDir
// See also http://docs.docker.io/reference/builder/#copy
private List copyEntries = new ArrayList<>();
// list of ports to expose and environments to use
private List ports = new ArrayList<>();
// SHELL executable and params to be used with the runCmds see issue #1156 on github
private Arguments shell;
// list of RUN Commands to run along with image build see issue #191 on github
private List runCmds = new ArrayList<>();
// environment
private Map envEntries = new LinkedHashMap<>();
// image labels
private Map labels = new LinkedHashMap<>();
// exposed volumes
private List volumes = new ArrayList<>();
// whether the Dockerfile should be optimised. i.e. compressing run statements into a single statement
private boolean shouldOptimise = false;
/**
* Create a DockerFile in the given directory
* @param destDir directory where to store the dockerfile
* @return the full path to the docker file
* @throws IOException if writing fails
*/
public File write(File destDir) throws IOException {
File target = new File(destDir,"Dockerfile");
FileUtils.fileWrite(target, content());
return target;
}
/**
* Create a Dockerfile following the format described in the
* Docker reference manual
*
* @return the dockerfile create
* @throws IllegalArgumentException if no src/dest entries have been added
*/
public String content() throws IllegalArgumentException {
StringBuilder b = new StringBuilder();
DockerFileKeyword.FROM.addTo(b, baseImage != null ? baseImage : DockerAssemblyManager.DEFAULT_DATA_BASE_IMAGE);
if (maintainer != null) {
DockerFileKeyword.MAINTAINER.addTo(b, maintainer);
}
addOptimisation();
addEnv(b);
addLabels(b);
addPorts(b);
addCopy(b);
addWorkdir(b);
addShell(b);
addRun(b);
addVolumes(b);
addHealthCheck(b);
addCmd(b);
addEntryPoint(b);
addUser(b);
return b.toString();
}
private void addUser(StringBuilder b) {
if (user != null) {
DockerFileKeyword.USER.addTo(b, user);
}
}
private void addHealthCheck(StringBuilder b) {
if (healthCheck != null) {
StringBuilder healthString = new StringBuilder();
switch (healthCheck.getMode()) {
case cmd:
buildOption(healthString, DockerFileOption.HEALTHCHECK_INTERVAL, healthCheck.getInterval());
buildOption(healthString, DockerFileOption.HEALTHCHECK_TIMEOUT, healthCheck.getTimeout());
buildOption(healthString, DockerFileOption.HEALTHCHECK_START_PERIOD, healthCheck.getStartPeriod());
buildOption(healthString, DockerFileOption.HEALTHCHECK_RETRIES, healthCheck.getRetries());
buildArguments(healthString, DockerFileKeyword.CMD, false, healthCheck.getCmd());
break;
case none:
DockerFileKeyword.NONE.addTo(healthString, false);
break;
default:
throw new IllegalArgumentException("Unsupported health check mode: " + healthCheck.getMode());
}
DockerFileKeyword.HEALTHCHECK.addTo(b, healthString.toString());
}
}
private void addWorkdir(StringBuilder b) {
if (workdir != null) {
DockerFileKeyword.WORKDIR.addTo(b, workdir);
}
}
private void addEntryPoint(StringBuilder b){
if (entryPoint != null) {
buildArguments(b, DockerFileKeyword.ENTRYPOINT, true, entryPoint);
}
}
private void addCmd(StringBuilder b){
if (cmd != null) {
buildArguments(b, DockerFileKeyword.CMD, true, cmd);
}
}
private static void buildArguments(StringBuilder b, DockerFileKeyword key, boolean newline, Arguments arguments) {
String arg;
if (arguments.getShell() != null) {
arg = arguments.getShell();
} else {
arg = "[\"" + JOIN_ON_COMMA.join(arguments.getExec()) + "\"]";
}
key.addTo(b, newline, arg);
}
private static void buildArgumentsAsJsonFormat(StringBuilder b, DockerFileKeyword key, boolean newline, Arguments arguments) {
String arg = "[\"" + JOIN_ON_COMMA.join(arguments.asStrings()) + "\"]";
key.addTo(b, newline, arg);
}
private static void buildOption(StringBuilder b, DockerFileOption option, Object value) {
if (value != null) {
option.addTo(b, value);
}
}
private String userForEntry(CopyEntry copyEntry) {
if (copyEntry.user != null) {
return copyEntry.user;
}
return assemblyUser;
}
private String targetDirForEntry(CopyEntry copyEntry) {
if (copyEntry.target != null) {
return copyEntry.target.equals("/") ? "" : copyEntry.target;
}
return basedir.equals("/") ? "" : basedir;
}
private void addCopy(StringBuilder b) {
for (CopyEntry entry : copyEntries) {
String entryUser = userForEntry(entry);
if (entryUser != null) {
String[] userParts = entryUser.split(":");
if (userParts.length > 2) {
DockerFileKeyword.USER.addTo(b, "root");
}
addCopyEntries(b, entry, "", (userParts.length > 1 ?
userParts[0] + ":" + userParts[1] :
userParts[0]));
if (userParts.length > 2) {
DockerFileKeyword.USER.addTo(b, userParts[2]);
}
} else {
addCopyEntries(b, entry, "", null);
}
}
}
private void addCopyEntries(StringBuilder b, CopyEntry entry, String topLevelDir, String ownerAndGroup) {
String dest = topLevelDir + targetDirForEntry(entry) + "/" + entry.destination;
if (ownerAndGroup == null) {
DockerFileKeyword.COPY.addTo(b, entry.source, dest);
} else {
DockerFileKeyword.COPY.addTo(b, "--chown=" + ownerAndGroup, entry.source, dest);
}
}
private void addEnv(StringBuilder b) {
addMap(b, DockerFileKeyword.ENV, envEntries);
}
private void addLabels(StringBuilder b) {
addMap(b, DockerFileKeyword.LABEL, labels);
}
private void addMap(StringBuilder b,DockerFileKeyword keyword, Map map) {
if (map != null && map.size() > 0) {
String entries[] = new String[map.size()];
int i = 0;
for (Map.Entry entry : map.entrySet()) {
entries[i++] = createKeyValue(entry.getKey(), entry.getValue());
}
keyword.addTo(b, entries);
}
}
/**
* Escape any slashes, quotes, and newlines int the value. If any escaping occurred, quote the value.
* @param key The key
* @param value The value
* @return Escaped and quoted key="value"
*/
private String createKeyValue(String key, String value) {
StringBuilder sb = new StringBuilder();
// no quoting the key; "Keys are alphanumeric strings which may contain periods (.) and hyphens (-)"
sb.append(key).append('=');
if (value == null || value.isEmpty()) {
return sb.append("\"\"").toString();
}
StringBuilder valBuf = new StringBuilder();
boolean toBeQuoted = false;
for (int i = 0; i < value.length(); ++i) {
char c = value.charAt(i);
switch (c) {
case '"':
case '\n':
case '\\':
// escape the character
valBuf.append('\\');
case ' ':
// space needs quotes, too
toBeQuoted = true;
default:
// always append
valBuf.append(c);
}
}
if (toBeQuoted) {
// need to keep quotes
sb.append('"').append(valBuf.toString()).append('"');
} else {
sb.append(value);
}
return sb.toString();
}
private void addPorts(StringBuilder b) {
if (ports.size() > 0) {
String[] portsS = new String[ports.size()];
int i = 0;
for(String port : ports) {
portsS[i++] = validatePortExposure(port);
}
DockerFileKeyword.EXPOSE.addTo(b, portsS);
}
}
private String validatePortExposure(String input) throws IllegalArgumentException {
try {
Matcher matcher = Pattern.compile("(.*?)(?:/(tcp|udp))?$", Pattern.CASE_INSENSITIVE).matcher(input);
// Matches always. If there is a tcp/udp protocol, should end up in the second group
// and get factored out. If it's something invalid, it should get stuck to the first group.
matcher.matches();
Integer.valueOf(matcher.group(1));
return input.toLowerCase();
} catch (NumberFormatException exp) {
throw new IllegalArgumentException("\nInvalid port mapping '" + input + "'\n" +
"Required format: '(/tcp|udp)'\n" +
"See the reference manual for more details");
}
}
private void addOptimisation() {
if (runCmds != null && !runCmds.isEmpty() && shouldOptimise) {
String optimisedRunCmd = StringUtils.join(runCmds.iterator(), " && ");
runCmds.clear();
runCmds.add(optimisedRunCmd);
}
}
private void addShell(StringBuilder b) {
if (shell != null) {
buildArgumentsAsJsonFormat(b, DockerFileKeyword.SHELL, true, shell);
}
}
private void addRun(StringBuilder b) {
for (String run : runCmds) {
DockerFileKeyword.RUN.addTo(b, run);
}
}
private void addVolumes(StringBuilder b) {
for (CopyEntry e : copyEntries) {
if (e.export != null ? e.export : baseImage == null) {
addVolume(b, targetDirForEntry(e));
}
}
if (exportTargetDir != null ? exportTargetDir : baseImage == null) {
addVolume(b, basedir);
}
for (String volume : volumes) {
addVolume(b, volume);
}
}
private void addVolume(StringBuilder buffer, String volume) {
while (volume.endsWith("/")) {
volume = volume.substring(0, volume.length() - 1);
}
// don't export '/'
if (volume.length() > 0) {
DockerFileKeyword.VOLUME.addTo(buffer, "[\"" + volume + "\"]");
}
}
// ==========================================================================
// Builder stuff ....
public DockerFileBuilder() {}
public DockerFileBuilder baseImage(String baseImage) {
if (baseImage != null) {
this.baseImage = baseImage;
}
return this;
}
public DockerFileBuilder maintainer(String maintainer) {
this.maintainer = maintainer;
return this;
}
public DockerFileBuilder workdir(String workdir) {
this.workdir = workdir;
return this;
}
public DockerFileBuilder basedir(String dir) {
if (dir != null) {
if (!dir.startsWith("/") && !ENV_VAR_PATTERN.matcher(dir).matches()) {
throw new IllegalArgumentException("'basedir' must be an absolute path starting with / (and not " +
"'" + basedir + "') or start with an environment variable");
}
basedir = dir;
}
return this;
}
public DockerFileBuilder cmd(Arguments cmd) {
this.cmd = cmd;
return this;
}
public DockerFileBuilder entryPoint(Arguments entryPoint) {
this.entryPoint = entryPoint;
return this;
}
public DockerFileBuilder assemblyUser(String assemblyUser) {
this.assemblyUser = assemblyUser;
return this;
}
public DockerFileBuilder user(String user) {
this.user = user;
return this;
}
public DockerFileBuilder healthCheck(HealthCheckConfiguration healthCheck) {
this.healthCheck = healthCheck;
return this;
}
public DockerFileBuilder add(String source, String destination) {
this.copyEntries.add(new CopyEntry(source, destination));
return this;
}
public DockerFileBuilder add(String source, String destination, String target, String user, Boolean exportTarget) {
this.copyEntries.add(new CopyEntry(source, destination, target, user, exportTarget));
return this;
}
public DockerFileBuilder expose(List ports) {
if (ports != null) {
this.ports.addAll(ports);
}
return this;
}
/**
* Adds the SHELL Command plus params within the build image section
* @param shell
* @return
*/
public DockerFileBuilder shell(Arguments shell) {
this.shell = shell;
return this;
}
/**
* Adds the RUN Commands within the build image section
* @param runCmds
* @return
*/
public DockerFileBuilder run(List runCmds) {
if (runCmds != null) {
for (String cmd : runCmds) {
if (!StringUtils.isEmpty(cmd)) {
this.runCmds.add(cmd);
}
}
}
return this;
}
public DockerFileBuilder exportTargetDir(Boolean exportTargetDir) {
this.exportTargetDir = exportTargetDir;
return this;
}
public DockerFileBuilder env(Map values) {
if (values != null) {
this.envEntries.putAll(values);
validateMap(envEntries);
}
return this;
}
public DockerFileBuilder labels(Map values) {
if (values != null) {
this.labels.putAll(values);
}
return this;
}
public DockerFileBuilder volumes(List volumes) {
if (volumes != null) {
this.volumes.addAll(volumes);
}
return this;
}
public DockerFileBuilder optimise() {
this.shouldOptimise = true;
return this;
}
private void validateMap(Map env) {
for (Map.Entry entry : env.entrySet()) {
if (entry.getValue() == null || entry.getValue().length() == 0) {
throw new IllegalArgumentException("Environment variable '" +
entry.getKey() + "' must not be null or empty if building an image");
}
}
}
// All entries required, destination is relative to exportDir
private static final class CopyEntry {
private String source;
private String destination;
private String target;
private String user;
private Boolean export;
private CopyEntry(String src, String dest) {
source = src;
// Strip leading slashes
destination = dest;
// squeeze slashes
while (destination.startsWith("/")) {
destination = destination.substring(1);
}
}
public CopyEntry(String src, String dest, String target, String user, Boolean exportTarget) {
this(src, dest);
this.target = target;
this.user = user;
this.export = exportTarget;
}
}
}