/**
* Copyright 2014-2017 Linagora, Université Joseph Fourier, Floralis
*
* The present code is developed in the scope of the joint LINAGORA -
* Université Joseph Fourier - Floralis research program and is designated
* as a "Result" pursuant to the terms and conditions of the LINAGORA
* - Université Joseph Fourier - Floralis research program. Each copyright
* holder of Results enumerated here above fully & independently holds complete
* ownership of the complete Intellectual Property rights applicable to the whole
* of said Results, and may freely exploit it in any manner which does not infringe
* the moral rights of the other copyright holders.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.roboconf.target.docker.internal;
import static net.roboconf.target.docker.internal.DockerUtils.extractBoolean;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.command.InspectExecResponse;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import net.roboconf.core.internal.tests.TestUtils;
import net.roboconf.core.model.beans.Instance;
import net.roboconf.core.model.helpers.InstanceHelpers;
import net.roboconf.core.utils.Utils;
import net.roboconf.target.api.TargetException;
import net.roboconf.target.api.TargetHandlerParameters;
/**
* Test correct Docker image generation and container configuration.
* <p>
* WARNING: these tests may last very long....
* </p>
*
* @author Pierre Bourret - Université Joseph Fourier
*/
@Ignore
public class DockerHandlerWithPackagesTest {
private static final Logger LOGGER = Logger.getLogger(DockerHandlerWithPackagesTest.class.getName());
private static final String APPLICATION_NAME = "roboconf_test";
private static final String FAKE_AGENT_LOCATION = "/usr/local/roboconf-agent/roboconf-fake-agent.txt";
private static final String FAKE_AGENT_CONTENT = "INSTALLED!";
private static final Map<String,String> MESSAGING_CONFIGURATION;
static {
Map<String,String> basis = new HashMap<> ();
basis.put("net.roboconf.messaging.type", "telepathy");
basis.put("mindControl", "false");
basis.put("psychosisProtection", "active");
MESSAGING_CONFIGURATION = Collections.unmodifiableMap( basis );
}
@Rule
public final TemporaryFolder tmpFolder = new TemporaryFolder();
private final Map<String,String> targetProperties = new LinkedHashMap<> ();
private final DockerHandler dockerHandler = new DockerHandler();
private final Instance instance = new Instance("test-" + UUID.randomUUID().toString());
private final String instancePath = InstanceHelpers.computeInstancePath(this.instance);
private DockerClient dockerClient;
private String dockerImageId, dockerContainerId;
private File agentTarGz, agentZip;
/**
* Initializes the test environment.
* @throws Exception if something bad happened.
*/
@Before
public void initDockerClient() throws Exception {
LOGGER.warning( "This test may take quite A LOT of TIME!!!!" );
// Checks Docker is installed.
try {
DockerTestUtils.checkDockerIsInstalled();
} catch( IOException | InterruptedException e ) {
LOGGER.warning("Tests are skipped because Docker is not installed.");
Utils.logException(LOGGER, e);
Assume.assumeNoException(e);
}
// Load test files
this.agentTarGz = TestUtils.findTestFile( "/archives/roboconf-fake-agent.tar.gz" );
this.agentZip = TestUtils.findTestFile( "/archives/roboconf-fake-agent.zip" );
// Load the Docker target properties.
final Properties targetProperties = Utils.readPropertiesFile( TestUtils.findTestFile( "/conf/docker.properties" ));
for (final Map.Entry<Object, Object> e : targetProperties.entrySet()) {
this.targetProperties.put(e.getKey().toString(), e.getValue().toString());
}
// Add a infinite loop command, so the container stays while we test it.
this.targetProperties.put(DockerHandler.RUN_EXEC, "[ \"tail\", \"-f\", \"/dev/null\" ]");
// Generated a unique Docker image id.
this.dockerImageId = "roboconf.test.generated." + UUID.randomUUID().toString().replace('-', '.');
this.targetProperties.put(DockerHandler.IMAGE_ID, this.dockerImageId);
// Create a client.
try {
this.dockerClient = DockerClientBuilder.getInstance(
DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost( this.targetProperties.get( DockerHandler.ENDPOINT ))
.build())
.build();
} catch( Exception e ) {
LOGGER.warning("Tests are skipped because Docker is misconfigured.");
Utils.logException(LOGGER, e);
Assume.assumeNoException(e);
}
// Start the Docker target handler.
this.dockerHandler.start();
}
/**
* Cleanup the test environment.
*/
@After
public void cleanupDocker() throws Exception {
final List<Exception> exceptions = new ArrayList<>();
// Stop the docker target handler.
try {
this.dockerHandler.stop();
} catch (final Exception e) {
// We must keep going on, save the exception and continue.
exceptions.add(e);
}
if (this.dockerClient != null) {
// Kill the container, if any.
if (this.dockerContainerId != null) {
final InspectContainerResponse.ContainerState state =
DockerUtils.getContainerState( this.dockerContainerId, this.dockerClient);
if( state != null
&& ( extractBoolean( state.getRunning()) || extractBoolean( state.getPaused()))) {
try {
this.dockerClient.killContainerCmd(this.dockerContainerId) .exec();
} catch (final Exception e) {
// We must keep going on, save the exception and continue.
exceptions.add(e);
}
}
}
// Delete the built image, if any.
try {
DockerUtils.deleteImageIfItExists(this.dockerImageId, this.dockerClient);
} catch (final Exception e) {
// We must keep going on, save the exception and continue.
exceptions.add(e);
}
// Finally close the client.
try {
this.dockerClient.close();
} catch (final Exception e) {
// We must keep going on, save the exception and continue.
exceptions.add(e);
}
}
// Log encountered exceptions, if any.
if (!exceptions.isEmpty()) {
for (final Exception e : exceptions) {
LOGGER.severe("Exception while cleaning test up");
Utils.logException(LOGGER, Level.SEVERE, e);
}
// Throw the first exception, so the whole thing fails!
throw exceptions.get(0);
}
}
@Test
public void testAgentTarGz_withAdditionalPackagesOnly() throws Exception {
// Configure the container:
// - we use the TarGz agent archive,
// - we clear the JRE packages property, so the default is used.
// - we add additional packages: vim & net-tools.
this.targetProperties.put(DockerHandler.AGENT_PACKAGE_URL, this.agentTarGz.getAbsolutePath());
this.targetProperties.remove(DockerHandler.AGENT_JRE_AND_PACKAGES);
this.targetProperties.put(DockerHandler.ADDITIONAL_PACKAGES, "vim net-tools");
runAndTestDockerContainer(Collections.<String>emptyList(), DockerHandler.AGENT_JRE_AND_PACKAGES_DEFAULT, "vim", "net-tools");
}
@Test
public void testAgentZip_withAdditionalPackagesOnly() throws Exception {
// Configure the container:
// - we use the Zip agent archive,
// - we clear the JRE packages property, so the default is used.
// - we add additional packages: vim & net-tools.
this.targetProperties.put(DockerHandler.AGENT_PACKAGE_URL, this.agentZip.getAbsolutePath());
this.targetProperties.remove(DockerHandler.AGENT_JRE_AND_PACKAGES);
this.targetProperties.put(DockerHandler.ADDITIONAL_PACKAGES, "vim net-tools");
runAndTestDockerContainer(Collections.<String>emptyList(), DockerHandler.AGENT_JRE_AND_PACKAGES_DEFAULT, "unzip", "vim", "net-tools");
}
@Test
public void testAgentTarGz_withAlternateJreAndAdditionalPackages() throws Exception {
// Configure the container:
// - we use the TarGz agent archive,
// - we set the JRE packages property to use JamVM.
// - we add additional packages: vim & net-tools.
this.targetProperties.put(DockerHandler.AGENT_PACKAGE_URL, this.agentTarGz.getAbsolutePath());
this.targetProperties.put(DockerHandler.AGENT_JRE_AND_PACKAGES, "icedtea-7-jre-jamvm");
this.targetProperties.put(DockerHandler.ADDITIONAL_PACKAGES, "vim net-tools");
runAndTestDockerContainer(Collections.<String>emptyList(), "icedtea-7-jre-jamvm", "vim", "net-tools");
}
@Test
public void testAgentZip_withAlternateJreOnly() throws Exception {
// Configure the container:
// - we use the Zip agent archive,
// - we set the JRE packages property to use JamVM.
// - we use no additional packages.
this.targetProperties.put(DockerHandler.AGENT_PACKAGE_URL, this.agentZip.getAbsolutePath());
this.targetProperties.put(DockerHandler.AGENT_JRE_AND_PACKAGES, "icedtea-7-jre-jamvm");
this.targetProperties.remove(DockerHandler.ADDITIONAL_PACKAGES);
runAndTestDockerContainer(Collections.<String>emptyList(), "icedtea-7-jre-jamvm");
}
@Test
public void testAgentZip_withAdditionalDeploys() throws Exception {
// Create a dummy file in the tmp folder.
final File dummy = this.tmpFolder.newFile("DUMMY.TXT");
// Configure the container:
// - we use the Zip agent archive,
// - we clear the JRE packages property, so the default is used.
// - we use no additional packages.
// - we two additional deploy URLs, that will be copied in the (container's) Karaf deploy directory:
// - a remote URL (Apache license v2: LICENSE-2.0.txt)
// - a local file (DUMMY.TXT)
this.targetProperties.put(DockerHandler.AGENT_PACKAGE_URL, this.agentZip.getAbsolutePath());
this.targetProperties.remove(DockerHandler.AGENT_JRE_AND_PACKAGES);
this.targetProperties.remove(DockerHandler.ADDITIONAL_PACKAGES);
this.targetProperties.put(DockerHandler.ADDITIONAL_DEPLOY,
"http://www.apache.org/licenses/LICENSE-2.0.txt " + dummy.toURI());
List<String> values = new ArrayList<>( 2 );
values.add("/usr/local/roboconf-agent/deploy/LICENSE-2.0.txt");
values.add("/usr/local/roboconf-agent/deploy/DUMMY.TXT");
runAndTestDockerContainer( values );
}
/**
* Creates, configures, runs and tests a Docker container.
*
* @param filesToCheck a list of files that must be present on the Docker container.
* @param packages the packages that must be installed on the Docker container.
* @throws Exception if anything bad happens during the run & tests.
*/
private void runAndTestDockerContainer( final List<String> filesToCheck, final String... packages ) throws Exception {
// Create the machine.
TargetHandlerParameters parameters =
new TargetHandlerParameters().targetProperties( this.targetProperties )
.messagingProperties( MESSAGING_CONFIGURATION )
.scopedInstancePath( this.instancePath )
.applicationName( APPLICATION_NAME )
.domain( "domain" );
final String machineId = this.dockerHandler.createMachine( parameters );
Assert.assertNotNull(machineId);
Assert.assertNull(this.instance.data.get(Instance.MACHINE_ID));
// Configure the machine.
this.dockerHandler.configureMachine( parameters, machineId, this.instance);
// Now wait until the Docker target updates the machine id, or the timeout expires...
this.dockerContainerId = DockerTestUtils.waitForMachineId(
machineId,
this.instance.data,
DockerTestUtils.DOCKER_CONFIGURE_TIMEOUT);
Assert.assertNotNull(this.dockerContainerId);
// Ensure the machine is running
Assert.assertTrue(this.dockerHandler.isMachineRunning( parameters, this.dockerContainerId ));
// Now perform the tests...
checkAgentIsUnpacked();
for (final String file : filesToCheck)
checkFileIsPresent(file);
for (final String p : packages)
checkPackageIsInstalled(p);
// Terminate the container.
this.dockerHandler.terminateMachine( parameters, this.dockerContainerId );
Assert.assertFalse(this.dockerHandler.isMachineRunning( parameters, this.dockerContainerId ));
}
private void checkAgentIsUnpacked() throws Exception {
final CommandResult result = execDockerCommand("cat", FAKE_AGENT_LOCATION);
Assert.assertEquals("Fake agent is not installed", 0, result.exitCode);
Assert.assertTrue("Fake agent is not installed", result.output.contains(FAKE_AGENT_CONTENT));
}
/**
* Check that a given Debian package is installed on the given Docker container.
*
* @param packageName the name of the package to check.
* @throws TargetException if the container cannot be reached.
*/
private void checkPackageIsInstalled( String packageName ) throws Exception {
// Execute the package checker command.
// As we use pipes, we need to bash -c the whole quoted command.
Assert.assertEquals("package '" + packageName + "' is not installed on container " + this.dockerContainerId,
0,
execDockerCommand(
"bash", "-c", "dpkg --get-selections | grep ^" + packageName + " "
).exitCode);
}
/**
* Check that a given file is present on the given Docker container.
*
* @param path the path of the file to check.
* @throws TargetException if the container cannot be reached.
*/
private void checkFileIsPresent( String path ) throws Exception {
// Execute the package checker command.
// As we use pipes, we need to bash -c the whole quoted command.
Assert.assertEquals( "file '" + path + "' is not present on container " + this.dockerContainerId, 0,
execDockerCommand( "test", "-f", path ).exitCode );
}
/**
* The result of a Docker exec.
*/
private static class CommandResult {
final int exitCode;
final String output;
CommandResult( final int exitCode, final String output ) {
this.exitCode = exitCode;
this.output = output;
}
}
/**
* Executes a command on the tested docker container.
* @param commandLine the command line to run.
* @return the result of the command execution.
* @throws Exception if something bas happened during the command execution
* (excluding the command itself, see {@code result}).
*/
private CommandResult execDockerCommand( String... commandLine ) throws Exception {
// Create the command and get its execId (execCreateCmd)
final String execId = this.dockerClient.createContainerCmd( this.dockerContainerId )
.withCmd( commandLine )
.withAttachStdout( true )
.exec()
.getId();
// Start the command (execStartCmd), and get the output.
ByteArrayOutputStream stdOutAndErr = new ByteArrayOutputStream();
this.dockerClient.execStartCmd( this.dockerContainerId )
.withExecId(execId)
.exec( new ExecStartResultCallback( stdOutAndErr, stdOutAndErr )).awaitCompletion();
// Wait until the command has finished...
InspectExecResponse cmd;
do {
cmd = this.dockerClient.inspectExecCmd(execId).exec();
} while (cmd.isRunning());
// Now return...
return new CommandResult(
cmd.getExitCode(),
stdOutAndErr.toString("UTF-8"));
}
}