Skip to content

Commit

Permalink
Support relative paths in volumes statements in docker-compose.yaml
Browse files Browse the repository at this point in the history
* Moves path resolution logic to a utility class
* Includes tests
* Adds mockito-core as a test dependency

Addresses fabric8io#846

Signed-off-by: Elliot Metsger <emetsger@jhu.edu>
  • Loading branch information
emetsger committed Aug 23, 2017
1 parent e50a7a5 commit 29aeb82
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 7 deletions.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.8.47</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.fabric8.maven.docker.config.handler.compose;

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
* @return an absolute {@code File} reference to {@code pathToResolve}; <em>not</em> guaranteed to exist
*/
static File resolveAbsolutely(String pathToResolve, MavenProject project) {
File baseDirAsFile = new File(pathToResolve);
return baseDirAsFile.isAbsolute() ? baseDirAsFile : new File(project.getBasedir(), pathToResolve);
}
}
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 @@ -207,8 +210,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 @@ -12,13 +12,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 @@ -241,6 +248,9 @@ RunVolumeConfiguration getVolumeConfig() {
List<String> volumes = asList("volumes");
boolean added = false;
if (volumes.size() > 0) {
for (int i = 0; i < volumes.size(); i++) {
volumes.set(i, resolveRelativeVolumeBinding(baseDir, volumes.get(i)));
}
builder.bind(volumes);
added = true;
}
Expand Down Expand Up @@ -396,4 +406,97 @@ private Map<String, String> convertToMap(List<String> list) {
private void throwIllegalArgumentException(String msg) {
throw new IllegalArgumentException(String.format("%s: %s - ", composeFile, name) + msg);
}

/**
* Examines the supplied {@code volume} string, and resolves relative paths. The returned string is guaranteed to
* be absolute, but <em>not</em> guaranteed to exist.
* <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 base directory, 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 volume} 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 volume} 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 volume the volume string from the docker-compose file
* @return the volume string, with any relative paths resolved as absolute paths
*/
static String resolveRelativeVolumeBinding(File baseDir, String volume) {

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 = volume.split(":");
String localPath = pathParts[0];
String serverPath = (pathParts.length > 1) ? pathParts[1] : "";

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

if (localPath.startsWith("./")) {
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.fabric8.maven.docker.config.handler.compose;

import org.apache.maven.project.MavenProject;
import org.junit.Test;

import java.io.File;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
*
*/
public class ComposeUtilsTest {


@Test
public void resolveComposeFileWithAbsoluteComposeFile() throws Exception {
String absComposeFile = "/absolute/path/to/docker-compose.yaml";

assertEquals(new File(absComposeFile),
ComposeUtils.resolveComposeFileAbsolutely(null, absComposeFile, null));
}

@Test
public void resolveComposeFileWithRelativeComposeFileAndAbsoluteBaseDir() throws Exception {
String relComposeFile = "relative/path/to/docker-compose.yaml";
String absBaseDir = "/basedir/";

assertEquals(new File(absBaseDir, relComposeFile),
ComposeUtils.resolveComposeFileAbsolutely(absBaseDir, relComposeFile, null));
}

@Test
public void resolveComposeFileWithRelativeComposeFileAndRelativeBaseDir() throws Exception {
String relComposeFile = "relative/path/to/docker-compose.yaml";
String relBaseDir = "basedir/";
String absMavenProjectDir = "/absoute/path/to/maven/project";

MavenProject project = mock(MavenProject.class);
when(project.getBasedir()).thenReturn(new File(absMavenProjectDir));


assertEquals(new File(new File(absMavenProjectDir, relBaseDir), relComposeFile),
ComposeUtils.resolveComposeFileAbsolutely(relBaseDir, relComposeFile, project));
verify(project).getBasedir();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.fabric8.maven.docker.config.handler.compose;

import org.junit.Test;

import java.io.File;

import static io.fabric8.maven.docker.config.handler.compose.DockerComposeServiceWrapper.resolveRelativeVolumeBinding;
import static org.junit.Assert.assertEquals;

/**
*
*/
public class DockerComposeServiceWrapperTest {

private File baseDir = new File("/absolute/basedir");
private String relativePath = "./rel";
private String userPath = "~/rel";
private String bindingPath = "/path/to/container/dir";

@Test(expected = IllegalArgumentException.class)
public void relativeBaseDir() throws Exception {
resolveRelativeVolumeBinding(new File("relative/"), null);
}

@Test
public void testResolveRelativeVolumePath() throws Exception {
String volumeString = String.format("%s:%s", relativePath, bindingPath);

// './rel:/path/to/container/dir' to '/absolute/basedir/rel:/path/to/container/dir'
String relativizedVolumeString = resolveRelativeVolumeBinding(baseDir, volumeString);

assertEquals(String.format("%s:%s", new File(baseDir, relativePath.substring(2)), bindingPath),
relativizedVolumeString);
}

@Test
public void testResolveUserVolumePath() throws Exception {
String volumeString = String.format("%s:%s", userPath, bindingPath);

// '~/rel:/path/to/container/dir' to '/user/home/rel:/path/to/container/dir'
String relativizedVolumeString = resolveRelativeVolumeBinding(baseDir, volumeString);

assertEquals(String.format("%s:%s",
new File(System.getProperty("user.home"), relativePath.substring(2)), bindingPath),
relativizedVolumeString);
}

@Test
public void testResolveNamedVolume() throws Exception {
String volumeName = "volname";
String volumeString = String.format("%s:%s", volumeName, bindingPath);

// volumeString should be untouched
assertEquals(volumeString, resolveRelativeVolumeBinding(baseDir, volumeString));
}

@Test
public void testResolveAbsolutePathMapping() throws Exception {
String absolutePath = "/absolute/path";
String volumeString = String.format("%s:%s", absolutePath, bindingPath);

// volumeString should be untouched
assertEquals(volumeString, resolveRelativeVolumeBinding(baseDir, volumeString));
}

@Test
public void testResolveSinglePath() throws Exception {
String absolutePath = "/absolute/path";

// volumeString should be untouched
assertEquals(absolutePath, resolveRelativeVolumeBinding(baseDir, absolutePath));
}
}

0 comments on commit 29aeb82

Please sign in to comment.