diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java index 5569208e6..8c6ccfb1a 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java @@ -24,7 +24,6 @@ package org.jenkinsci.plugins.docker.workflow; import com.google.common.base.Optional; -import org.jenkinsci.plugins.docker.workflow.client.DockerClient; import com.google.inject.Inject; import hudson.AbortException; import hudson.EnvVars; @@ -39,30 +38,21 @@ import hudson.model.Run; import hudson.model.TaskListener; import hudson.slaves.WorkspaceList; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.Serializable; -import java.nio.charset.Charset; +import hudson.util.VersionNumber; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; -import java.util.List; -import java.util.Map; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.TreeSet; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nonnull; - -import hudson.util.VersionNumber; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import javax.annotation.CheckForNull; import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints; import org.jenkinsci.plugins.docker.commons.tools.DockerTool; +import org.jenkinsci.plugins.docker.workflow.client.DockerClient; +import org.jenkinsci.plugins.docker.workflow.client.WindowsDockerClient; import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; @@ -73,6 +63,17 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + public class WithContainerStep extends AbstractStepImpl { private static final Logger LOGGER = Logger.getLogger(WithContainerStep.class.getName()); @@ -111,7 +112,6 @@ private static void destroy(String container, Launcher launcher, Node node, EnvV // TODO switch to GeneralNonBlockingStepExecution public static class Execution extends AbstractStepExecutionImpl { - private static final long serialVersionUID = 1; @Inject(optional=true) private transient WithContainerStep step; @StepContextParameter private transient Launcher launcher; @@ -125,6 +125,9 @@ public static class Execution extends AbstractStepExecutionImpl { private String container; private String toolName; + public Execution() { + } + @Override public boolean start() throws Exception { EnvVars envReduced = new EnvVars(env); EnvVars envHost = computer.getEnvironment(); @@ -136,9 +139,11 @@ public static class Execution extends AbstractStepExecutionImpl { LOGGER.log(Level.FINE, "reduced environment: {0}", envReduced); workspace.mkdirs(); // otherwise it may be owned by root when created for -v - String ws = workspace.getRemote(); + String ws = getPath(workspace); toolName = step.toolName; - DockerClient dockerClient = new DockerClient(launcher, node, toolName); + DockerClient dockerClient = launcher.isUnix() + ? new DockerClient(launcher, node, toolName) + : new WindowsDockerClient(launcher, node, toolName); VersionNumber dockerVersion = dockerClient.version(); if (dockerVersion != null) { @@ -146,6 +151,9 @@ public static class Execution extends AbstractStepExecutionImpl { throw new AbortException("The docker version is less than v1.7. Pipeline functions requiring 'docker exec' (e.g. 'docker.inside') or SELinux labeling will not work."); } else if (dockerVersion.isOlderThan(new VersionNumber("1.8"))) { listener.error("The docker version is less than v1.8. Running a 'docker.inside' from inside a container will not work."); + } else if (dockerVersion.isOlderThan(new VersionNumber("1.13"))) { + if (!launcher.isUnix()) + throw new AbortException("The docker version is less than v1.13. Running a 'docker.inside' from inside a Windows container will not work."); } } else { listener.error("Failed to parse docker version. Please note there is a minimum docker version requirement of v1.7."); @@ -153,7 +161,7 @@ public static class Execution extends AbstractStepExecutionImpl { FilePath tempDir = tempDir(workspace); tempDir.mkdirs(); - String tmp = tempDir.getRemote(); + String tmp = getPath(tempDir); Map volumes = new LinkedHashMap(); Collection volumesFromContainers = new LinkedHashSet(); @@ -166,7 +174,11 @@ public static class Execution extends AbstractStepExecutionImpl { // check if there is any volume which contains the directory boolean found = false; for (String vol : mountedVolumes) { - if (dir.startsWith(vol)) { + boolean dirStartsWithVol = launcher.isUnix() + ? dir.startsWith(vol) // Linux + : dir.toLowerCase().startsWith(vol.toLowerCase()); // Windows + + if (dirStartsWithVol) { volumesFromContainers.add(containerId.get()); found = true; break; @@ -183,9 +195,10 @@ public static class Execution extends AbstractStepExecutionImpl { volumes.put(tmp, tmp); } - container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ "cat"); + String command = launcher.isUnix() ? "cat" : "cmd.exe"; + container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ command); final List ps = dockerClient.listProcess(env, container); - if (!ps.contains("cat")) { + if (!ps.contains(command)) { listener.error( "The container started but didn't run the expected command. " + "Please double check your ENTRYPOINT does execute the command passed as docker run argument, " + @@ -202,6 +215,15 @@ public static class Execution extends AbstractStepExecutionImpl { return false; } + private String getPath(FilePath filePath) + throws IOException, InterruptedException { + if (launcher.isUnix()) { + return filePath.getRemote(); + } else { + return filePath.toURI().getPath().substring(1).replace("\\", "/"); + } + } + // TODO use 1.652 use WorkspaceList.tempDir private static FilePath tempDir(FilePath ws) { return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp"); diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java index 37c4a303b..6562606fb 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java @@ -31,10 +31,6 @@ import hudson.model.Node; import hudson.util.ArgumentListBuilder; import hudson.util.VersionNumber; -import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord; - -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -44,18 +40,22 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; -import java.util.Map; import java.util.List; -import java.util.Arrays; +import java.util.Map; import java.util.StringTokenizer; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord; import org.jenkinsci.plugins.docker.commons.tools.DockerTool; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -106,7 +106,12 @@ public DockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckF public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNull String args, @CheckForNull String workdir, @Nonnull Map volumes, @Nonnull Collection volumesFromContainers, @Nonnull EnvVars containerEnv, @Nonnull String user, @Nonnull String... command) throws IOException, InterruptedException { ArgumentListBuilder argb = new ArgumentListBuilder(); - argb.add("run", "-t", "-d", "-u", user); + argb.add("run", "-t", "-d"); + + // Username might be empty because we are running on Windows + if (StringUtils.isNotEmpty(user)) { + argb.add("-u", user); + } if (args != null) { argb.addTokenized(args); } @@ -306,6 +311,10 @@ private LaunchResult launch(@CheckForNull @Nonnull EnvVars launchEnv, boolean qu * @return a {@link String} containing the uid:gid. */ public String whoAmI() throws IOException, InterruptedException { + if (!launcher.isUnix()) { + // Windows does not support username + return ""; + } ByteArrayOutputStream userId = new ByteArrayOutputStream(); launcher.launch().cmds("id", "-u").quiet(true).stdout(userId).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener()); @@ -367,6 +376,6 @@ public List getVolumes(@Nonnull EnvVars launchEnv, String containerID) t if (volumes.isEmpty()) { return Collections.emptyList(); } - return Arrays.asList(volumes.split("\\n")); + return Arrays.asList(volumes.replace("\\", "/").split("\\n")); } } diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java new file mode 100644 index 000000000..77b117e9d --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java @@ -0,0 +1,137 @@ +package org.jenkinsci.plugins.docker.workflow.client; + +import com.google.common.base.Optional; +import hudson.EnvVars; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Node; +import hudson.util.ArgumentListBuilder; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.*; +import java.nio.charset.Charset; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class WindowsDockerClient extends DockerClient { + private static final Logger LOGGER = Logger.getLogger(WindowsDockerClient.class.getName()); + + private final Launcher launcher; + private final Node node; + + public WindowsDockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckForNull String toolName) { + super(launcher, node, toolName); + this.launcher = launcher; + this.node = node; + } + + @Override + public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNull String args, @CheckForNull String workdir, @Nonnull Map volumes, @Nonnull Collection volumesFromContainers, @Nonnull EnvVars containerEnv, @Nonnull String user, @Nonnull String... command) throws IOException, InterruptedException { + ArgumentListBuilder argb = new ArgumentListBuilder("docker", "run", "-d", "-t"); + if (args != null) { + argb.addTokenized(args); + } + + if (workdir != null) { + argb.add("-w", workdir); + } + for (Map.Entry volume : volumes.entrySet()) { + argb.add("-v", volume.getKey() + ":" + volume.getValue()); + } + for (String containerId : volumesFromContainers) { + argb.add("--volumes-from", containerId); + } + for (Map.Entry variable : containerEnv.entrySet()) { + argb.add("-e"); + argb.addMasked(variable.getKey()+"="+variable.getValue()); + } + argb.add(image).add(command); + + LaunchResult result = launch(launchEnv, false, null, argb); + if (result.getStatus() == 0) { + return result.getOut(); + } else { + throw new IOException(String.format("Failed to run image '%s'. Error: %s", image, result.getErr())); + } + } + + @Override + public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { + LaunchResult result = launch(launchEnv, false, null, "docker", "top", containerId); + if (result.getStatus() != 0) { + throw new IOException(String.format("Failed to run top '%s'. Error: %s", containerId, result.getErr())); + } + List processes = new ArrayList<>(); + try (Reader r = new StringReader(result.getOut()); + BufferedReader in = new BufferedReader(r)) { + String line; + in.readLine(); // ps header + while ((line = in.readLine()) != null) { + final StringTokenizer stringTokenizer = new StringTokenizer(line, " "); + if (stringTokenizer.countTokens() < 1) { + throw new IOException("Unexpected `docker top` output : "+line); + } + + processes.add(stringTokenizer.nextToken()); // COMMAND + } + } + return processes; + } + + @Override + public Optional getContainerIdIfContainerized() throws IOException, InterruptedException { + if (node == null || + launch(new EnvVars(), true, null, "sc.exe", "query", "cexecsvc").getStatus() != 0) { + return Optional.absent(); + } + + LaunchResult getComputerName = launch(new EnvVars(), true, null, "hostname"); + if(getComputerName.getStatus() != 0) { + throw new IOException("Failed to get hostname."); + } + + String shortID = getComputerName.getOut().toLowerCase(); + LaunchResult getLongIdResult = launch(new EnvVars(), true, null, "docker", "inspect", shortID, "--format={{.Id}}"); + if(getLongIdResult.getStatus() != 0) { + LOGGER.log(Level.INFO, "Running inside of a container but cannot determine container ID from current environment."); + return Optional.absent(); + } + + return Optional.of(getLongIdResult.getOut()); + } + + @Override + public String whoAmI() throws IOException, InterruptedException { + try (ByteArrayOutputStream userId = new ByteArrayOutputStream()) { + launcher.launch().cmds("whoami").quiet(true).stdout(userId).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener()); + return userId.toString(Charset.defaultCharset().name()).trim(); + } + } + + private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, String... args) throws IOException, InterruptedException { + return launch(env, quiet, workDir, new ArgumentListBuilder(args)); + } + private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, ArgumentListBuilder argb) throws IOException, InterruptedException { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Executing command \"{0}\"", argb); + } + + Launcher.ProcStarter procStarter = launcher.launch(); + if (workDir != null) { + procStarter.pwd(workDir); + } + + LaunchResult result = new LaunchResult(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + result.setStatus(procStarter.quiet(quiet).cmds(argb).envs(env).stdout(out).stderr(err).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener())); + final String charsetName = Charset.defaultCharset().name(); + result.setOut(out.toString(charsetName)); + result.setErr(err.toString(charsetName)); + + return result; + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/Docker.groovy b/src/main/resources/org/jenkinsci/plugins/docker/workflow/Docker.groovy index b88ad6973..71308f1f1 100644 --- a/src/main/resources/org/jenkinsci/plugins/docker/workflow/Docker.groovy +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/Docker.groovy @@ -75,11 +75,17 @@ class Docker implements Serializable { new Image(this, id) } + String shell() { + node { + script.isUnix() ? "sh" : "bat" + } + } + public Image build(String image, String args = '.') { node { def commandLine = "docker build -t ${image} ${args}" - script.sh commandLine + script."${shell()}" commandLine this.image(image) } } @@ -107,11 +113,11 @@ class Docker implements Serializable { public V inside(String args = '', Closure body) { docker.node { def toRun = imageName() - if (toRun != id && docker.script.sh(script: "docker inspect -f . ${id}", returnStatus: true) == 0) { + if (toRun != id && docker.script."${docker.shell()}"(script: "docker inspect -f . ${id}", returnStatus: true) == 0) { // Can run it without registry prefix, because it was locally built. toRun = id } else { - if (docker.script.sh(script: "docker inspect -f . ${toRun}", returnStatus: true) != 0) { + if (docker.script."${docker.shell()}"(script: "docker inspect -f . ${toRun}", returnStatus: true) != 0) { // Not yet present locally. // withDockerContainer requires the image to be available locally, since its start phase is not a durable task. pull() @@ -125,13 +131,13 @@ class Docker implements Serializable { public void pull() { docker.node { - docker.script.sh "docker pull ${imageName()}" + docker.script."${docker.shell()}" "docker pull ${imageName()}" } } public Container run(String args = '', String command = "") { docker.node { - def container = docker.script.sh(script: "docker run -d${args != '' ? ' ' + args : ''} ${id}${command != '' ? ' ' + command : ''}", returnStdout: true).trim() + def container = docker.script."${docker.shell()}"(script: "docker run -d${args != '' ? ' ' + args : ''} ${id}${command != '' ? ' ' + command : ''}", returnStdout: true).trim() new Container(docker, container) } } @@ -150,7 +156,7 @@ class Docker implements Serializable { public void tag(String tagName = parsedId.tag, boolean force = true) { docker.node { def taggedImageName = toQualifiedImageName(parsedId.userAndRepo + ':' + tagName) - docker.script.sh "docker tag ${id} ${taggedImageName}" + docker.script."${docker.shell()}" "docker tag ${id} ${taggedImageName}" return taggedImageName; } } @@ -160,7 +166,7 @@ class Docker implements Serializable { // The image may have already been tagged, so the tagging may be a no-op. // That's ok since tagging is cheap. def taggedImageName = tag(tagName, force) - docker.script.sh "docker push ${taggedImageName}" + docker.script."${docker.shell()}" "docker push ${taggedImageName}" } } @@ -177,11 +183,11 @@ class Docker implements Serializable { } public void stop() { - docker.script.sh "docker stop ${id} && docker rm -f ${id}" + docker.script."${docker.shell()}" "docker stop ${id} && docker rm -f ${id}" } public String port(int port) { - docker.script.sh(script: "docker port ${id} ${port}", returnStdout: true).trim() + docker.script."${docker.shell()}"(script: "docker port ${id} ${port}", returnStdout: true).trim() } } diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java index 97eaffd43..486a433d5 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java @@ -23,6 +23,7 @@ */ package org.jenkinsci.plugins.docker.workflow; +import hudson.EnvVars; import org.jenkinsci.plugins.docker.workflow.client.DockerClient; import hudson.Launcher; import hudson.util.StreamTaskListener; @@ -64,6 +65,25 @@ public static void assumeNotWindows() throws Exception { Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("windows")); } + public static EnvVars newDockerLaunchEnv() { + // Create the KeyMaterial for connecting to the docker host/server. + // E.g. currently need to add something like the following to your env + // -DDOCKER_HOST_FOR_TEST="tcp://192.168.x.y:2376" + // -DDOCKER_HOST_KEY_DIR_FOR_TEST="/Users/tfennelly/.boot2docker/certs/boot2docker-vm" + final String docker_host_for_test = System.getProperty("DOCKER_HOST_FOR_TEST"); + final String docker_host_key_dir_for_test = System.getProperty("DOCKER_HOST_KEY_DIR_FOR_TEST"); + + EnvVars env = new EnvVars(); + if (docker_host_for_test != null) { + env.put("DOCKER_HOST", docker_host_for_test); + } + if (docker_host_key_dir_for_test != null) { + env.put("DOCKER_TLS_VERIFY", "1"); + env.put("DOCKER_CERT_PATH", docker_host_key_dir_for_test); + } + return env; + } + private DockerTestUtil() {} } diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/client/DockerClientTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/client/DockerClientTest.java index 3052aef0c..2351e8eef 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/client/DockerClientTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/client/DockerClientTest.java @@ -57,7 +57,7 @@ public void setup() throws Exception { @Test public void test_run() throws IOException, InterruptedException { - EnvVars launchEnv = newLaunchEnv(); + EnvVars launchEnv = DockerTestUtil.newDockerLaunchEnv(); String containerId = dockerClient.run(launchEnv, "learn/tutorial", null, null, Collections.emptyMap(), Collections.emptyList(), new EnvVars(), dockerClient.whoAmI(), "cat"); @@ -87,24 +87,4 @@ public void test_valid_version() { public void test_invalid_version() { Assert.assertNull(DockerClient.parseVersionNumber("xxx")); } - - - private EnvVars newLaunchEnv() { - // Create the KeyMaterial for connecting to the docker host/server. - // E.g. currently need to add something like the following to your env - // -DDOCKER_HOST_FOR_TEST="tcp://192.168.x.y:2376" - // -DDOCKER_HOST_KEY_DIR_FOR_TEST="/Users/tfennelly/.boot2docker/certs/boot2docker-vm" - final String docker_host_for_test = System.getProperty("DOCKER_HOST_FOR_TEST"); - final String docker_host_key_dir_for_test = System.getProperty("DOCKER_HOST_KEY_DIR_FOR_TEST"); - - EnvVars env = new EnvVars(); - if (docker_host_for_test != null) { - env.put("DOCKER_HOST", docker_host_for_test); - } - if (docker_host_key_dir_for_test != null) { - env.put("DOCKER_TLS_VERIFY", "1"); - env.put("DOCKER_CERT_PATH", docker_host_key_dir_for_test); - } - return env; - } } diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClientTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClientTest.java new file mode 100644 index 000000000..c390e0735 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClientTest.java @@ -0,0 +1,57 @@ +package org.jenkinsci.plugins.docker.workflow.client; + +import hudson.EnvVars; +import hudson.Launcher; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; +import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord; +import org.jenkinsci.plugins.docker.workflow.DockerTestUtil; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; + +public class WindowsDockerClientTest { + + private DockerClient dockerClient; + + @Before + public void setup() throws Exception { + DockerTestUtil.assumeDocker(); + + TaskListener taskListener = StreamTaskListener.fromStderr(); + Launcher.LocalLauncher launcher = new Launcher.LocalLauncher(taskListener); + + dockerClient = new WindowsDockerClient(launcher, null, null); + } + + @Test + public void test_run() throws IOException, InterruptedException { + EnvVars launchEnv = DockerTestUtil.newDockerLaunchEnv(); + String containerId = dockerClient.run( + launchEnv, + "learn/tutorial", + null, + null, + Collections.emptyMap(), + Collections.emptyList(), + new EnvVars(), + dockerClient.whoAmI(), + "cat"); + + Assert.assertEquals(64, containerId.length()); + ContainerRecord containerRecord = dockerClient.getContainerRecord(launchEnv, containerId); + Assert.assertEquals(dockerClient.inspect(launchEnv, "learn/tutorial", ".Id"), containerRecord.getImageId()); + Assert.assertTrue(containerRecord.getContainerName().length() > 0); + Assert.assertTrue(containerRecord.getHost().length() > 0); + Assert.assertTrue(containerRecord.getCreated() > 1000000000000L); + Assert.assertEquals(Collections.emptyList(), dockerClient.getVolumes(launchEnv, containerId)); + + // Also test that the stop works and cleans up after itself + Assert.assertNotNull(dockerClient.inspect(launchEnv, containerId, ".Name")); + dockerClient.stop(launchEnv, containerId); + Assert.assertNull(dockerClient.inspect(launchEnv, containerId, ".Name")); + } +}