diff --git a/src/main/java/com/spotify/docker/BuildMojo.java b/src/main/java/com/spotify/docker/BuildMojo.java index 9ca7a039..aaf783f2 100644 --- a/src/main/java/com/spotify/docker/BuildMojo.java +++ b/src/main/java/com/spotify/docker/BuildMojo.java @@ -32,6 +32,7 @@ import com.spotify.docker.client.AnsiProgressHandler; import com.spotify.docker.client.DockerClient; import com.spotify.docker.client.DockerException; +import com.spotify.docker.client.ProgressHandler; import com.typesafe.config.Config; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigFactory; @@ -52,6 +53,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -92,6 +94,12 @@ public class BuildMojo extends AbstractDockerMojo { */ private static final char WINDOWS_SEPARATOR = '\\'; + /** + * File to log output + */ + @Parameter(property = "logOutput") + private String logOutput; + /** * Directory containing the Dockerfile. If the value is not set, the plugin will generate a * Dockerfile using the required baseImage value, plus the optional entryPoint, cmd and maintainer @@ -328,7 +336,11 @@ protected void execute(final DockerClient docker) copyResources(destination); } - buildImage(docker, destination, buildParams()); + if (logOutput == null) { + buildImage(docker, destination, buildParams()); + } else { + buildImage(docker, destination, new File(logOutput), buildParams()); + } tagImage(docker, forceTags); final DockerBuildInformation buildInfo = new DockerBuildInformation(imageName, getLog()); @@ -540,12 +552,45 @@ private void validateParameters() throws MojoExecutionException { } private void buildImage(final DockerClient docker, final String buildDir, + final ProgressHandler progressHandler, final DockerClient.BuildParam... buildParams) throws MojoExecutionException, DockerException, IOException, InterruptedException { getLog().info("Building image " + imageName); - docker.build(Paths.get(buildDir), imageName, new AnsiProgressHandler(), buildParams); + docker.build(Paths.get(buildDir), imageName, progressHandler, buildParams); getLog().info("Built " + imageName); } + + private void buildImage(final DockerClient docker, final String buildDir, + final File output, + final DockerClient.BuildParam... buildParams) + throws MojoExecutionException, DockerException, IOException, InterruptedException { + + if (output.isDirectory() || (output.exists() && !output.canWrite())) { + throw new MojoExecutionException("The specified output file is a directory or cannot " + + "be written"); + } + + File parent = output.getParentFile(); + if (parent.isFile()) { + throw new MojoExecutionException("The specified output file's parent is a file"); + } + + if (!parent.exists() && !parent.mkdirs()) { + throw new MojoExecutionException("The specified output file's parent cannot be made a" + + " directory"); + } + + try (PrintStream printStream = + new PrintStream(new FileOutputStream(output, true), true, "UTF-8")) { + buildImage(docker, buildDir, new AnsiProgressHandler(printStream), buildParams); + } + } + + private void buildImage(final DockerClient docker, final String buildDir, + final DockerClient.BuildParam... buildParams) + throws MojoExecutionException, DockerException, IOException, InterruptedException { + buildImage(docker, buildDir, new AnsiProgressHandler(), buildParams); + } private void tagImage(final DockerClient docker, boolean forceTags) throws DockerException, InterruptedException, MojoExecutionException { diff --git a/src/test/java/com/spotify/docker/BuildMojoTest.java b/src/test/java/com/spotify/docker/BuildMojoTest.java index 7c2f4319..2fe12622 100644 --- a/src/test/java/com/spotify/docker/BuildMojoTest.java +++ b/src/test/java/com/spotify/docker/BuildMojoTest.java @@ -28,14 +28,17 @@ import com.spotify.docker.client.AnsiProgressHandler; import com.spotify.docker.client.DockerClient; import com.spotify.docker.client.DockerClient.BuildParam; +import com.spotify.docker.client.DockerException; import com.spotify.docker.client.ProgressHandler; import com.spotify.docker.client.messages.ProgressMessage; +import org.apache.commons.lang.StringUtils; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.testing.AbstractMojoTestCase; import org.apache.maven.project.MavenProject; +import org.mockito.ArgumentMatcher; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -56,10 +59,12 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class BuildMojoTest extends AbstractMojoTestCase { @@ -448,6 +453,112 @@ public void testNoCache() throws Exception { eq(BuildParam.noCache())); } + public void testLogOutputToFileButParentIsFile() throws Exception { + testLogOutputToFileButFileCannotBeWritten(false); + } + + public void testLogOutputToFileButFileIsDirectory() throws Exception { + testLogOutputToFileButFileCannotBeWritten(true); + } + + public void testLogOutputToFileButFileCannotBeWritten(boolean dir) throws Exception { + final File pom = getTestFile("src/test/resources/pom-build-log-output.xml"); + assertNotNull("Null pom.xml", pom); + assertTrue("pom.xml does not exist", pom.exists()); + + // Make sure initially the file to be logged does not exist + final String outputFileName = "target/docker/outputDir/file-to-log-output.log"; + final File outputFile = getTestFile(outputFileName); + assertNotNull("Null output file", outputFile); + assertFalse("output file already exists", outputFile.exists()); + + if (dir) { + // Force it being a directory + assertTrue("Cannot create directory ", outputFile.mkdirs()); + } else { + // Force parent is be a file + File parent = outputFile.getParentFile(); + parent.getParentFile().mkdirs(); + assertTrue("Cannot create parent file ", parent.createNewFile()); + } + + final BuildMojo mojo = setupMojo(pom); + final DockerClient docker = mock(DockerClient.class); + try { + mojo.execute(docker); + fail("mojo should have thrown exception because output file cannot be written to"); + } catch (MojoExecutionException e) { + final String message; + if (dir) { + message = "The specified output file is a directory or cannot be written"; + } else { + message = "The specified output file's parent is a file"; + } + assertTrue(String.format("Exception message should have contained '%s'", message), + e.getMessage().contains(message)); + } + } + + public void testLogOutputToNewFile() throws Exception { + testLogOutputToFile(true); + } + + public void testLogOutputToExistingFile() throws Exception { + testLogOutputToFile(false); + } + + private void testLogOutputToFile(boolean newFile) throws Exception { + final File pom = getTestFile("src/test/resources/pom-build-log-output.xml"); + assertNotNull("Null pom.xml", pom); + assertTrue("pom.xml does not exist", pom.exists()); + + // Make sure initially the file to be logged does not exist + final String outputFileName = "target/docker/outputDir/file-to-log-output.log"; + final File outputFile = getTestFile(outputFileName); + assertNotNull("Null output file", outputFile); + assertFalse("output file already exists", outputFile.exists()); + + final BuildMojo mojo = setupMojo(pom); + final DockerClient docker = mock(DockerClient.class); + + // A matcher that grabs the instantiated AnsiProgressHandler and logs a message + final String testMessage = "Testing progress is logged to file"; + ArgumentMatcher matcher = new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + assertTrue(AnsiProgressHandler.class.isInstance(argument)); + AnsiProgressHandler handler = AnsiProgressHandler.class.cast(argument); + ProgressMessage message = new ProgressMessage(); + message.status(testMessage); + try { + handler.progress(message); + } catch (DockerException e) { + fail("Unexpected error"); + } + return true; + } + + }; + + if (! newFile) { + File parent = outputFile.getParentFile(); + assertTrue("Cannot create parent directory", parent.exists() || parent.mkdirs()); + assertTrue("Cannot create output file", outputFile.createNewFile()); + } + + when(docker.build(eq(Paths.get("target/docker")), eq("busybox"), argThat(matcher))).thenReturn(StringUtils.EMPTY); + + mojo.execute(docker); + + verify(docker).build(eq(Paths.get("target/docker")), eq("busybox"), any(AnsiProgressHandler.class)); + + // Make sure output file exists and message is logged + assertFileExists(outputFileName); + byte[] encoded = Files.readAllBytes(Paths.get(outputFileName)); + assertEquals(testMessage + System.lineSeparator(), new String(encoded, "UTF-8")); + } + private BuildMojo setupMojo(final File pom) throws Exception { final MavenProject project = new ProjectStub(pom); final MavenSession session = newMavenSession(project); diff --git a/src/test/resources/pom-build-log-output.xml b/src/test/resources/pom-build-log-output.xml new file mode 100644 index 00000000..5fa17d89 --- /dev/null +++ b/src/test/resources/pom-build-log-output.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + Docker Maven Plugin Test Pom + com.spotify + docker-maven-plugin-test + 0.0.1-SNAPSHOT + jar + + + + + com.spotify + docker-maven-plugin + 0.1-SNAPSHOT + + + + http://host:2375 + src/test/resources/dockerDirectory + busybox + + target/docker/outputDir/file-to-log-output.log + + + + +