Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support relative paths in volumes statements in docker-compose.yaml #848

Merged
merged 12 commits into from
Sep 11, 2017
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# ChangeLog

* **0.22-SNAPSHOT**
- Support relative paths when binding volumes in `docker-compose.yml` (#846)

* **0.22.1** (2017-08-28)
- Allow Docker compose version "2", too ([#829](https://github.com/fabric8io/docker-maven-plugin/issues/829))
- Allow a registry to be set programmatically ([#853](https://github.com/fabric8io/docker-maven-plugin/issues/853))
Expand Down
2 changes: 1 addition & 1 deletion src/main/asciidoc/inc/external/_docker_compose.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The following options can be provided:
| Element | Description | Default

| *basedir*
| Basedir where to find the compose file and which is also used as the current directory when examing the compose file
| Basedir where to find the compose file and which is also used as the current directory when examing the compose file. Any relative volume bindings will be resolved relative to this directory.
| `${basedir}/src/main/docker`

| *composeFile*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.fabric8.maven.docker.config.handler.compose;

import io.fabric8.maven.docker.util.DockerPathUtil;
import org.apache.maven.project.MavenProject;

import java.io.File;

/**
* Path-resolution methods
*/
class ComposeUtils {

/**
* Resolves a docker-compose file against the supplied base directory. The returned {@code File} is guaranteed to
* be {@link File#isAbsolute() absolute}.
* <p>
* If {@code composeFile} is {@link File#isAbsolute() absolute}, then it is returned unmodified. Otherwise, the
* {@code composeFile} is returned as an absolute {@code File} using the {@link #resolveAbsolutely(String,
* MavenProject) resolved} {@code baseDir} as its parent.
* </p>
*
* @param baseDir the base directory containing the docker-compose file (ignored if {@code composeFile} is absolute)
* @param composeFile the path of the docker-compose file, may be absolute
* @param project the {@code MavenProject} used to resolve the {@code baseDir}
* @return an absolute {@code File} reference to the {@code composeFile}
*/
static File resolveComposeFileAbsolutely(String baseDir, String composeFile, MavenProject project) {
File yamlFile = new File(composeFile);
return yamlFile.isAbsolute() ? yamlFile : new File(resolveAbsolutely(baseDir, project),composeFile);
}

/**
* Resolves the supplied resource (a path or directory on the filesystem) relative the Maven {@link
* MavenProject#getBasedir() base directory}. The returned {@code File} is guaranteed to be {@link
* File#isAbsolute() absolute}. The returned file is <em>not</em> guaranteed to exist.
* <p>
* If {@code pathToResolve} is {@link File#isAbsolute() absolute}, then it is returned unmodified. Otherwise, the
* {@code pathToResolve} is returned as an absolute {@code File} using the {@link MavenProject#getBasedir() Maven
* Project base directory} as its parent.
* </p>
*
* @param pathToResolve represents a filesystem resource, which may be an absolute path
* @param project the Maven project used to resolve non-absolute path resources, may be {@code null} if
* {@code pathToResolve} is {@link File#isAbsolute() absolute}
* @return an absolute {@code File} reference to {@code pathToResolve}; <em>not</em> guaranteed to exist
* @throws IllegalArgumentException if {@code pathToResolve} is relative, and {@code project} is {@code null} or
* provides a relative {@link MavenProject#getBasedir() base directory}
*/
static File resolveAbsolutely(String pathToResolve, MavenProject project) {
// avoid an NPE if the Maven project is not needed by DockerPathUtil
return DockerPathUtil.resolveAbsolutely(pathToResolve,
(project == null) ? null : project.getBasedir().getAbsolutePath());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import org.apache.maven.shared.filtering.MavenReaderFilterRequest;
import org.yaml.snakeyaml.Yaml;

import static io.fabric8.maven.docker.config.handler.compose.ComposeUtils.resolveAbsolutely;
import static io.fabric8.maven.docker.config.handler.compose.ComposeUtils.resolveComposeFileAbsolutely;


/**
* Docker Compose handler for allowing a docker-compose file to be used
Expand All @@ -39,7 +42,7 @@ public List<ImageConfiguration> resolve(ImageConfiguration unresolvedConfig, Mav
List<ImageConfiguration> resolved = new ArrayList<>();

DockerComposeConfiguration handlerConfig = new DockerComposeConfiguration(unresolvedConfig.getExternalConfig());
File composeFile = resolveComposeFile(handlerConfig.getBasedir(), handlerConfig.getComposeFile(), project);
File composeFile = resolveComposeFileAbsolutely(handlerConfig.getBasedir(), handlerConfig.getComposeFile(), project);

for (Object composeO : getComposeConfigurations(composeFile, project, session)) {
Map<String, Object> compose = (Map<String, Object>) composeO;
Expand All @@ -49,7 +52,7 @@ public List<ImageConfiguration> resolve(ImageConfiguration unresolvedConfig, Mav
String serviceName = entry.getKey();
Map<String, Object> serviceDefinition = (Map<String, Object>) entry.getValue();

DockerComposeServiceWrapper mapper = new DockerComposeServiceWrapper(serviceName, composeFile, serviceDefinition, unresolvedConfig);
DockerComposeServiceWrapper mapper = new DockerComposeServiceWrapper(serviceName, composeFile, serviceDefinition, unresolvedConfig, resolveAbsolutely(handlerConfig.getBasedir(), project));
resolved.add(buildImageConfiguration(mapper, composeFile.getParentFile(), unresolvedConfig, handlerConfig));
}
}
Expand Down Expand Up @@ -211,8 +214,4 @@ private RunImageConfiguration createRunConfiguration(DockerComposeServiceWrapper
.build();
}

private File resolveComposeFile(String baseDir, String compose, MavenProject project) {
File yamlFile = new File(compose);
return yamlFile.isAbsolute() ? yamlFile : new File(new File(project.getBasedir(),baseDir),compose);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.*;

import io.fabric8.maven.docker.config.*;
import io.fabric8.maven.docker.util.VolumeBindingUtil;


class DockerComposeServiceWrapper {
Expand All @@ -12,13 +13,20 @@ class DockerComposeServiceWrapper {
private final String name;
private final File composeFile;
private final ImageConfiguration enclosingImageConfig;
private final File baseDir;

DockerComposeServiceWrapper(String serviceName, File composeFile, Map<String, Object> serviceDefinition,
ImageConfiguration enclosingImageConfig) {
ImageConfiguration enclosingImageConfig, File baseDir) {
this.name = serviceName;
this.composeFile = composeFile;
this.configuration = serviceDefinition;
this.enclosingImageConfig = enclosingImageConfig;

if (!baseDir.isAbsolute()) {
throw new IllegalArgumentException(
"Expected the base directory '" + baseDir + "' to be an absolute path.");
}
this.baseDir = baseDir;
}

String getAlias() {
Expand Down Expand Up @@ -249,7 +257,14 @@ RunVolumeConfiguration getVolumeConfig() {
builder.from(volumesFrom);
added = true;
}
return added ? builder.build() : null;

if (added) {
RunVolumeConfiguration configuration = builder.build();
VolumeBindingUtil.resolveRelativeVolumeBindings(baseDir, configuration);
return configuration;
}

return null;
}

String getDomainname() {
Expand Down Expand Up @@ -396,4 +411,5 @@ private Map<String, String> convertToMap(List<String> list) {
private void throwIllegalArgumentException(String msg) {
throw new IllegalArgumentException(String.format("%s: %s - ", composeFile, name) + msg);
}

}
50 changes: 50 additions & 0 deletions src/main/java/io/fabric8/maven/docker/util/DockerPathUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.fabric8.maven.docker.util;

import java.io.File;

/**
* Docker path resolution and manipulation utility methods.
* <p>
* This class provides methods for manipulating paths <em>as they appear in docker-compose or Dockerfiles</em>. This
* class does not provide for generic path manipulation across platforms or file systems. Paths that appear in Docker
* configurations use forward slash as a separator character, so this class makes no provisions for handling Windows
* platform path semantics (e.g. the presence of drive letters or backward slash).
* </p>
*/
public class DockerPathUtil {

/**
* Resolves the supplied resource (a path or directory on the filesystem) relative the supplied {@code
* baseDir}. The returned {@code File} is guaranteed to be {@link File#isAbsolute() absolute}. The returned file
* is <em>not</em> guaranteed to exist.
* <p>
* If the supplied {@code pathToResolve} is already {@link File#isAbsolute() absolute}, then it is returned
* <em>unmodified</em>. Otherwise, the {@code pathToResolve} is returned as an absolute {@code File} using the
* supplied {@code baseDir} as its parent.
* </p>
*
* @param pathToResolve represents a filesystem resource, which may be an absolute path
* @param baseDir the absolute path used to resolve non-absolute path resources; <em>must</em> be absolute
* @return an absolute {@code File} reference to {@code pathToResolve}; <em>not</em> guaranteed to exist
* @throws IllegalArgumentException if the supplied {@code baseDir} does not represent an absolute path
*/
public static File resolveAbsolutely(String pathToResolve, String baseDir) {
File fileToResolve = new File(pathToResolve);

if (fileToResolve.isAbsolute()) {
return fileToResolve;
}

if (baseDir == null) {
throw new IllegalArgumentException("Cannot resolve relative path '" + pathToResolve + "' with a " +
"null base directory.");
}

File baseDirAsFile = new File(baseDir);
if (!baseDirAsFile.isAbsolute()) {
throw new IllegalArgumentException("Base directory '" + baseDirAsFile + "' must be absolute");
}

return new File(baseDirAsFile, pathToResolve);
}
}
133 changes: 133 additions & 0 deletions src/main/java/io/fabric8/maven/docker/util/VolumeBindingUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package io.fabric8.maven.docker.util;

import io.fabric8.maven.docker.config.RunVolumeConfiguration;

import java.io.File;
import java.util.List;

/**
* Utility methods for working with Docker volume bindings.
*/
public class VolumeBindingUtil {

/**
* Resolves relative paths in the supplied {@code bindingString}, and returns a binding string that has relative
* paths replaced with absolute paths. If the supplied {@code bindingString} does not contain a relative path, it
* is returned unmodified.
* <h3>Discussion:</h3>
* <p>
* Volumes may be defined inside of {@code service} blocks <a href="https://docs.docker.com/compose/compose-file/compose-file-v2/#volumes-volume_driver">
* as documented here</a>:
* </p>
* <pre>
* volumes:
* # Just specify a path and let the Engine create a volume
* - /var/lib/mysql
*
* # Specify an absolute path mapping
* - /opt/data:/var/lib/mysql
*
* # Path on the host, relative to the Compose file
* - ./cache:/tmp/cache
*
* # User-relative path
* - ~/configs:/etc/configs/:ro
*
* # Named volume
* - datavolume:/var/lib/mysql"
* </pre>
* <p>
* This method only operates on volume strings that are relative: beginning with {@code ./} or {@code ~/}. Relative
* paths beginning with {@code ./} are absolutized relative to the supplied {@code baseDir}, which <em>must</em> be
* absolute. Paths beginning with {@code ~/} are interpreted relative to {@code new File(System.getProperty(
* "user.home"))}, and {@code baseDir} is ignored.
* </p>
* <p>
* Volume strings that do not begin with a {@code ./} or {@code ~/} are returned as-is.
* </p>
* <h3>Examples:</h3>
* <p>
* Given {@code baseDir} equal to "/path/to/basedir" and a {@code bindingString} string equal to
* "./reldir:/some/other/dir", this method returns {@code /path/to/basedir/reldir:/some/other/dir}
* </p>
* <p>
* Given {@code baseDir} equal to "/path/to/basedir" and a {@code bindingString} string equal to
* "~/reldir:/some/other/dir", this method returns {@code /path/to/user/home/reldir:/some/other/dir}
* </p>
*
* @param baseDir the base directory used to resolve paths beginning with {@code ./}; <em>must</em> be absolute
* @param bindingString the volume string from the docker-compose file
* @return the volume string, with any relative paths resolved as absolute paths
* @throws IllegalArgumentException if the supplied {@code baseDir} is not absolute
*/
public static String resolveRelativeVolumeBinding(File baseDir, String bindingString) {

if (!baseDir.isAbsolute()) {
throw new IllegalArgumentException("Base directory '" + baseDir + "' must be absolute.");
}

// a 'services:' -> service -> 'volumes:' may be formatted as:
// (https://docs.docker.com/compose/compose-file/compose-file-v2/#volumes-volume_driver)
//
// volumes:
// # Just specify a path and let the Engine create a volume
// - /var/lib/mysql
//
// # Specify an absolute path mapping
// - /opt/data:/var/lib/mysql
//
// # Path on the host, relative to the Compose file
// - ./cache:/tmp/cache
//
// # User-relative path
// - ~/configs:/etc/configs/:ro
//
// # Named volume
// - datavolume:/var/lib/mysql

String[] pathParts = bindingString.split(":");
String localPath = pathParts[0];
String serverPath = (pathParts.length > 1) ? pathParts[1] : "";

// only perform resolution if the localPath begins with '~/' or './'

if (localPath.startsWith("./")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a better option would be using the File#getCanonicalPath method to resolve relative paths. So that also ../ is resolved correctly.

localPath = new File(baseDir, localPath.substring(2)).getAbsolutePath();
}

if (localPath.startsWith("~/")) {
localPath = new File(System.getProperty("user.home"), localPath.substring(2)).getAbsolutePath();
}

if (serverPath.length() > 0) {
return String.format("%s:%s", localPath, serverPath);
}

return localPath;
}

/**
* Iterates over each {@link RunVolumeConfiguration#getBind() binding} in the {@code volumeConfiguration}, and
* resolves any relative paths in the binding strings using {@link #resolveRelativeVolumeBinding(File, String)}.
*
* @param baseDir the base directory used to resolve paths beginning with {@code ./}; <em>must</em> be absolute
* @param volumeConfiguration the volume configuration that may contain volume binding specifications
* @throws IllegalArgumentException if the supplied {@code baseDir} is not absolute
*/
public static void resolveRelativeVolumeBindings(File baseDir, RunVolumeConfiguration volumeConfiguration) {

if (!baseDir.isAbsolute()) {
throw new IllegalArgumentException("Base directory '" + baseDir + "' must be absolute.");
}

List<String> bindings = volumeConfiguration.getBind();

if (bindings.isEmpty()) {
return;
}

for (int i = 0; i < bindings.size(); i++) {
bindings.set(i, resolveRelativeVolumeBinding(baseDir, bindings.get(i)));
}
}
}
Loading