package org.arquillian.cube.docker.impl.client.containerobject;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.arquillian.cube.ContainerObjectConfiguration;
import org.arquillian.cube.CubeController;
import org.arquillian.cube.CubeIp;
import org.arquillian.cube.HostPort;
import org.arquillian.cube.containerobject.Cube;
import org.arquillian.cube.containerobject.CubeDockerFile;
import org.arquillian.cube.containerobject.Environment;
import org.arquillian.cube.containerobject.Image;
import org.arquillian.cube.containerobject.Volume;
import org.arquillian.cube.docker.impl.await.PollingAwaitStrategy;
import org.arquillian.cube.docker.impl.client.config.Await;
import org.arquillian.cube.docker.impl.client.config.BuildImage;
import org.arquillian.cube.docker.impl.client.config.CubeContainer;
import org.arquillian.cube.docker.impl.client.config.Link;
import org.arquillian.cube.docker.impl.client.config.PortBinding;
import org.arquillian.cube.docker.impl.docker.DockerClientExecutor;
import org.arquillian.cube.docker.impl.model.DockerCube;
import org.arquillian.cube.docker.impl.util.ContainerObjectUtil;
import org.arquillian.cube.docker.impl.util.DockerFileUtil;
import org.arquillian.cube.impl.client.enricher.CubeIpTestEnricher;
import org.arquillian.cube.impl.client.enricher.HostPortTestEnricher;
import org.arquillian.cube.impl.util.ReflectionUtil;
import org.arquillian.cube.spi.CubeRegistry;
import org.arquillian.cube.spi.metadata.HasPortBindings;
import org.arquillian.cube.spi.metadata.IsContainerObject;
import org.jboss.arquillian.test.spi.TestEnricher;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter;
/**
* Instantiate container objects. This class is not thread safe.
*
* @author <a href="mailto:rivasdiaz@gmail.com">Ramon Rivas</a>
* @see DockerContainerObjectFactory
*/
public class DockerContainerObjectBuilder<T> {
public static final String TEMPORARY_FOLDER_PREFIX = "arquilliancube_";
public static final String TEMPORARY_FOLDER_SUFFIX = ".build";
private static final Logger logger = Logger.getLogger(DockerContainerObjectBuilder.class.getName());
// parameters received
private final DockerClientExecutor dockerClientExecutor;
private final CubeController cubeController;
private final CubeRegistry cubeRegistry;
private Class<T> containerObjectClass;
private Object containerObjectContainer;
private CubeContainer providedConfiguration;
private Collection<TestEnricher> enrichers = Collections.emptyList();
private Consumer<DockerCube> cubeCreatedCallback;
// temporary variables in the process of building the container and associated cube
private boolean classHasMethodWithCubeDockerFile;
private boolean classDefinesCubeDockerFile;
private boolean classDefinesImage;
private Method methodWithCubeDockerFile;
private CubeDockerFile cubeDockerFileAnnotation;
private Image cubeImageAnnotation;
private String containerName;
private File dockerfileLocation;
private CubeContainer generatedConfigutation, mergedConfiguration;
// results of the building
private T containerObjectInstance;
private DockerCube dockerCube;
public DockerContainerObjectBuilder(DockerClientExecutor dockerClientExecutor, CubeController cubeController,
CubeRegistry cubeRegistry) {
this.dockerClientExecutor = dockerClientExecutor;
this.cubeController = cubeController;
this.cubeRegistry = cubeRegistry;
}
private static String findEnrichedValueForFieldWithCubeIpAnnotation(CubeIp cubeIp, HasPortBindings portBindings) {
final boolean cubeIpInternal = cubeIp.internal();
final String cubeIpValue = (cubeIpInternal) ? portBindings.getInternalIP() : portBindings.getContainerIP();
return cubeIpValue;
}
private static int findEnrichedValueForFieldWithHostPortAnnotation(HostPort hostPort, HasPortBindings portBindings) {
int hostPortValue = hostPort.value();
if (hostPortValue == 0) {
throw new IllegalArgumentException(
String.format("%s annotation does not specify any exposed port", HostPort.class.getSimpleName()));
}
final HasPortBindings.PortAddress bindingForExposedPort = portBindings.getMappedAddress(hostPortValue);
if (bindingForExposedPort == null) {
throw new IllegalArgumentException(
String.format("exposed port %s is not exposed on container object.", hostPortValue));
}
return bindingForExposedPort.getPort();
}
private static File createTemporalDirectoryForCopyingDockerfile(String containerName) throws IOException {
File dir = File.createTempFile(TEMPORARY_FOLDER_PREFIX + containerName, TEMPORARY_FOLDER_SUFFIX);
dir.delete();
if (!dir.mkdirs()) {
throw new IllegalArgumentException("Temp Dir for storing Dockerfile contents could not be created.");
}
dir.deleteOnExit();
return dir;
}
private static Link linkFromCubeAnnotatedField(Field cubeField) {
final String linkName = linkNameFromCubeAnnotatedField(cubeField);
return Link.valueOf(linkName);
}
private static String linkNameFromCubeAnnotatedField(Field cubeField) {
if (cubeField.isAnnotationPresent(org.arquillian.cube.containerobject.Link.class)) {
return cubeField.getAnnotation(org.arquillian.cube.containerobject.Link.class).value();
}
final String cubeName = cubeNameFromCubeAnnotatedField(cubeField);
return cubeName + ":" + cubeName;
}
private static String cubeNameFromCubeAnnotatedField(Field cubeField) {
final org.arquillian.cube.containerobject.Cube cubeAnnotationOnField =
cubeField.getAnnotation(org.arquillian.cube.containerobject.Cube.class);
final Class<?> containerObjectClassFromField = cubeField.getType();
String cubeName = null;
final String cubeAnnotationOnFieldValue = cubeAnnotationOnField.value();
if (cubeAnnotationOnFieldValue != null && !Cube.DEFAULT_VALUE.equals(cubeAnnotationOnFieldValue)) {
// We have found a valid cube name
return cubeAnnotationOnFieldValue;
}
// Needs to check if container object or one of his parents contains a Cube
final String cubeAnnotationOnClassValue =
ContainerObjectUtil.getTopCubeAttribute(containerObjectClassFromField, "value", Cube.class,
Cube.DEFAULT_VALUE);
if (cubeAnnotationOnClassValue != null && !Cube.DEFAULT_VALUE.equals(cubeAnnotationOnClassValue)) {
//We got the cubeName in containerobject
return cubeAnnotationOnClassValue;
}
//No override so we need to use the default logic
return containerObjectClassFromField.getSimpleName();
}
/**
* Specifies an optional object that has a strong reference to the object being created. If set, a reference to it
* is stored as part of the metadata of the container object. This object is expected to control the lifecycle of
* the container object
*
* @param containerObjectContainer
* the container object's container
*
* @return the current builder instance
*
* @see IsContainerObject
*/
public DockerContainerObjectBuilder<T> withContainerObjectContainer(Object containerObjectContainer) {
this.containerObjectContainer = containerObjectContainer;
return this;
}
/**
* Specifies the container object class to be instantiated
*
* @param containerObjectClass
* container object class to be instantiated
*
* @return the current builder instance
*/
public DockerContainerObjectBuilder<T> withContainerObjectClass(Class<T> containerObjectClass) {
if (containerObjectClass == null) {
throw new IllegalArgumentException("container object class cannot be null");
}
this.containerObjectClass = containerObjectClass;
//First we check if this ContainerObject is defining a @CubeDockerFile in static method
final List<Method> methodsWithCubeDockerFile =
ReflectionUtil.getMethodsWithAnnotation(containerObjectClass, CubeDockerFile.class);
if (methodsWithCubeDockerFile.size() > 1) {
throw new IllegalArgumentException(
String.format(
"More than one %s annotation found and only one was expected. Methods where annotation was found are: %s",
CubeDockerFile.class.getSimpleName(), methodsWithCubeDockerFile));
}
classHasMethodWithCubeDockerFile = !methodsWithCubeDockerFile.isEmpty();
classDefinesCubeDockerFile = containerObjectClass.isAnnotationPresent(CubeDockerFile.class);
classDefinesImage = containerObjectClass.isAnnotationPresent(Image.class);
if (classHasMethodWithCubeDockerFile) {
methodWithCubeDockerFile = methodsWithCubeDockerFile.get(0);
boolean isMethodStatic = Modifier.isStatic(methodWithCubeDockerFile.getModifiers());
boolean methodHasNoArguments = methodWithCubeDockerFile.getParameterCount() == 0;
boolean methodReturnsAnArchive = Archive.class.isAssignableFrom(methodWithCubeDockerFile.getReturnType());
if (!isMethodStatic || !methodHasNoArguments || !methodReturnsAnArchive) {
throw new IllegalArgumentException(
String.format("Method %s annotated with %s is expected to be static, no args and return %s.",
methodWithCubeDockerFile, CubeDockerFile.class.getSimpleName(), Archive.class.getSimpleName()));
}
}
// User has defined @CubeDockerfile on the class and a method
if (classHasMethodWithCubeDockerFile && classDefinesCubeDockerFile) {
throw new IllegalArgumentException(
String.format(
"More than one %s annotation found and only one was expected. Both class and method %s has the annotation.",
CubeDockerFile.class.getSimpleName(), methodWithCubeDockerFile));
}
// User has defined @CubeDockerfile and @Image
if ((classHasMethodWithCubeDockerFile || classDefinesCubeDockerFile) && classDefinesImage) {
throw new IllegalArgumentException(
String.format("Container Object %s has defined %s annotation and %s annotation together.",
containerObjectClass.getSimpleName(), Image.class.getSimpleName(),
CubeDockerFile.class.getSimpleName()));
}
// User has not defined either @CubeDockerfile or @Image
if (!classDefinesCubeDockerFile && !classDefinesImage && !classHasMethodWithCubeDockerFile) {
throw new IllegalArgumentException(
String.format("Container Object %s is not annotated with either %s or %s annotations.",
containerObjectClass.getName(), CubeDockerFile.class.getSimpleName(), Image.class.getSimpleName()));
}
return this;
}
/**
* Specifies a configuration (can be partial) to be used to override the default configuration set using annotations
* on the container object. The received configuration will be merged with the configuration extracted from the
* container object, and the resulting configuration will be used to build the docker container.
* <p>
* Currently only supports instances of {@link CubeContainerObjectConfiguration}
*
* @param configuration
* partial configuration to override default container object cube configuration
*
* @return the current builder instance
*
* @see CubeContainerObjectConfiguration
* @see CubeContainer
*/
public DockerContainerObjectBuilder<T> withContainerObjectConfiguration(ContainerObjectConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException("configuration cannot be null");
}
if (configuration != null && !(configuration instanceof CubeContainerObjectConfiguration)) {
throw new IllegalArgumentException(
String.format("container object configuration received of type %s, but only %s is supported",
configuration.getClass().getSimpleName(), CubeContainerObjectConfiguration.class.getSimpleName()));
}
this.providedConfiguration =
configuration != null ? ((CubeContainerObjectConfiguration) configuration).getCubeContainerConfiguration()
: null;
return this;
}
/**
* Specifies the list of enrichers that will be used to enrich the container object.
*
* @param enrichers
* list of enrichers that will be used to enrich the container object
*
* @return the current builder instance
*/
public DockerContainerObjectBuilder<T> withEnrichers(Collection<TestEnricher> enrichers) {
if (enrichers == null) {
throw new IllegalArgumentException("enrichers cannot be null");
}
this.enrichers = enrichers;
return this;
}
/**
* Specifies a consumer that will be executed after the cube object is created and after cube is created or started
* by the cube controller. Callers must use this callback to register anything necesary for the controller to work
* and also if they want to keep an instance of the created cube.
*
* @param cubeCreatedCallback
* consumer that will be called when the cube instance is created
*
* @return the current builder instance
*/
public DockerContainerObjectBuilder<T> onCubeCreated(Consumer<DockerCube> cubeCreatedCallback) {
this.cubeCreatedCallback = cubeCreatedCallback;
return this;
}
/**
* Triggers the building process, builds, creates and starts the docker container associated with the requested
* container object, creates the container object and returns it
*
* @return the created container object
*
* @throws IllegalAccessException
* if there is an error accessing the container object fields
* @throws IOException
* if there is an I/O error while preparing the docker build
* @throws InvocationTargetException
* if there is an error while calling the DockerFile archive creation
*/
public T build() throws IllegalAccessException, IOException, InvocationTargetException {
generatedConfigutation = new CubeContainer();
findContainerName();
// if needed, prepare prepare resources required to build a docker image
prepareImageBuild();
// instantiate container object
instantiateContainerObject();
// enrich container object (without cube instance)
enrichContainerObjectBeforeCube();
// extract configuration from container object class
extractConfigurationFromContainerObject();
// merge received configuration with extracted configuration
mergeContainerObjectConfiguration();
// create/start/register associated cube
initializeCube();
// enrich container object (with cube instance)
enrichContainerObjectWithCube();
// return created container object
return containerObjectInstance;
}
private void findContainerName() {
// container name
if (providedConfiguration != null) {
final String providedContainerName = providedConfiguration.getContainerName();
if (providedContainerName != null && !providedContainerName.isEmpty()) {
containerName = providedConfiguration.getContainerName();
}
}
if (containerName == null) {
final String cubeValue =
ContainerObjectUtil.getTopCubeAttribute(containerObjectClass, "value", Cube.class, Cube.DEFAULT_VALUE);
if (cubeValue != null && !Cube.DEFAULT_VALUE.equals(cubeValue)) {
containerName = cubeValue;
}
}
if (containerName == null) {
containerName = containerObjectClass.getSimpleName();
}
generatedConfigutation.setContainerName(containerName);
}
private void prepareImageBuild() throws InvocationTargetException, IllegalAccessException, IOException {
// @CubeDockerfile is defined as static method
if (classHasMethodWithCubeDockerFile) {
cubeDockerFileAnnotation = methodWithCubeDockerFile.getAnnotation(CubeDockerFile.class);
final Archive<?> archive = (Archive<?>) methodWithCubeDockerFile.invoke(null, new Object[0]);
File output = createTemporalDirectoryForCopyingDockerfile(containerName);
logger.finer(String.format("Created %s directory for storing contents of %s cube.", output, containerName));
archive.as(ExplodedExporter.class).exportExplodedInto(output);
dockerfileLocation = output;
} else if (classDefinesCubeDockerFile) {
cubeDockerFileAnnotation = containerObjectClass.getAnnotation(CubeDockerFile.class);
//Copy Dockerfile and all contains of the same directory in a known directory.
File output = createTemporalDirectoryForCopyingDockerfile(containerName);
logger.finer(String.format("Created %s directory for storing contents of %s cube.", output, containerName));
DockerFileUtil.copyDockerfileDirectory(containerObjectClass, cubeDockerFileAnnotation, output);
dockerfileLocation = output;
} else if (classDefinesImage) {
cubeImageAnnotation = containerObjectClass.getAnnotation(Image.class);
}
}
private void instantiateContainerObject() {
containerObjectInstance =
ReflectionUtil.newInstance(containerObjectClass.getName(), new Class[0], new Class[0], containerObjectClass);
}
private void enrichContainerObjectBeforeCube() {
for (TestEnricher enricher : enrichers) {
boolean requiresDockerInstanceCreated =
enricher instanceof HostPortTestEnricher || enricher instanceof CubeIpTestEnricher;
if (!requiresDockerInstanceCreated) {
enricher.enrich(containerObjectInstance);
}
}
}
private void extractConfigurationFromContainerObject() {
// this method will focus on extracting the configuration from the container object class
// most probably, the caller will also try to pass some configuration from the current instantiation point
// (for example, annotations on a field)
// received configuration overrides container object configuration, so in some cases the extraction is skipped
// port bindings
if (providedConfiguration == null || providedConfiguration.getPortBindings() == null) {
final String[] portBindingsFromAnnotation =
ContainerObjectUtil.getTopCubeAttribute(containerObjectClass, "portBinding", Cube.class,
Cube.DEFAULT_PORT_BINDING);
if (portBindingsFromAnnotation != null && !Arrays.equals(portBindingsFromAnnotation,
Cube.DEFAULT_PORT_BINDING)) {
List<PortBinding> portBindings = Arrays.stream(portBindingsFromAnnotation)
.map(PortBinding::valueOf)
.collect(Collectors.toList());
generatedConfigutation.setPortBindings(portBindings);
}
}
// await
if (providedConfiguration == null || providedConfiguration.getAwait() == null) {
final int[] awaitPortsFromAnnotation =
ContainerObjectUtil.getTopCubeAttribute(containerObjectClass, "awaitPorts", Cube.class,
Cube.DEFAULT_AWAIT_PORT_BINDING);
if (awaitPortsFromAnnotation != null && !Arrays.equals(awaitPortsFromAnnotation,
Cube.DEFAULT_AWAIT_PORT_BINDING)) {
final Await await = new Await();
await.setStrategy(PollingAwaitStrategy.TAG);
await.setPorts(Arrays.asList(ArrayUtils.toObject(awaitPortsFromAnnotation)));
generatedConfigutation.setAwait(await);
}
}
// environment variables
// merged instead of overridden
if (true) {
List<String> environmentVariables =
ContainerObjectUtil.getAllAnnotations(containerObjectClass, Environment.class)
.stream()
.map(environment -> environment.key() + "=" + environment.value())
.collect(Collectors.toList());
generatedConfigutation.setEnv(environmentVariables);
}
// volumes
// merged instead of overridden
if (true) {
List<String> volumeBindings = ContainerObjectUtil.getAllAnnotations(containerObjectClass, Volume.class)
.stream()
.map(volume -> volume.hostPath() + ":" + volume.containerPath() + ":rw")
.collect(Collectors.toList());
generatedConfigutation.setBinds(volumeBindings);
}
// links
if (providedConfiguration == null || providedConfiguration.getLinks() == null) {
List<Link> links = ReflectionUtil.getFieldsWithAnnotation(containerObjectClass, Cube.class)
.stream()
.map(DockerContainerObjectBuilder::linkFromCubeAnnotatedField)
.collect(Collectors.toList());
generatedConfigutation.setLinks(links);
}
// image
if (classDefinesCubeDockerFile || classHasMethodWithCubeDockerFile) {
BuildImage dockerfileConfiguration = new BuildImage(
dockerfileLocation.getAbsolutePath(),
null,
cubeDockerFileAnnotation.nocache(),
cubeDockerFileAnnotation.remove());
generatedConfigutation.setBuildImage(dockerfileConfiguration);
} else {
generatedConfigutation.setImage(
org.arquillian.cube.docker.impl.client.config.Image.valueOf(cubeImageAnnotation.value()));
}
}
private void mergeContainerObjectConfiguration() {
mergedConfiguration = new CubeContainer();
if (providedConfiguration != null) {
mergedConfiguration.merge(providedConfiguration);
}
mergedConfiguration.merge(generatedConfigutation);
// TODO if both provided and generated configurations have environment variables or volumes, they must be merged instead.
// TODO should this be handled in CubeContainer::merge() instead?
if (providedConfiguration != null) {
// environment variables
if (providedConfiguration.getEnv() != null && generatedConfigutation.getEnv() != null) {
Collection<String> env = new ArrayList<>();
env.addAll(mergedConfiguration.getEnv());
env.addAll(generatedConfigutation.getEnv());
mergedConfiguration.setEnv(env);
}
// volumes
if (providedConfiguration.getBinds() != null && generatedConfigutation.getBinds() != null) {
Collection<String> binds = new ArrayList<>();
binds.addAll(mergedConfiguration.getBinds());
binds.addAll(generatedConfigutation.getBinds());
mergedConfiguration.setBinds(binds);
}
}
}
private void initializeCube() {
if (isNotInitialized()) {
dockerCube = new DockerCube(containerName, mergedConfiguration, dockerClientExecutor);
Class<?> containerObjectContainerClass =
containerObjectContainer != null ? containerObjectContainer.getClass() : null;
dockerCube.addMetadata(IsContainerObject.class, new IsContainerObject(containerObjectContainerClass));
logger.finer(String.format("Created Cube with name %s and configuration %s", containerName,
dockerCube.configuration()));
if (cubeCreatedCallback != null) {
cubeCreatedCallback.accept(dockerCube);
}
cubeController.create(containerName);
cubeController.start(containerName);
}
}
private boolean isNotInitialized() {
return cubeRegistry.getCube(containerName) == null;
}
private void enrichContainerObjectWithCube() throws IllegalAccessException {
enrichAnnotatedPortBuildingFields(CubeIp.class,
DockerContainerObjectBuilder::findEnrichedValueForFieldWithCubeIpAnnotation);
enrichAnnotatedPortBuildingFields(HostPort.class,
DockerContainerObjectBuilder::findEnrichedValueForFieldWithHostPortAnnotation);
}
private <T extends Annotation> void enrichAnnotatedPortBuildingFields(Class<T> annotationType,
BiFunction<T, HasPortBindings, ?> fieldEnricher) throws IllegalAccessException {
final List<Field> annotatedFields = ReflectionUtil.getFieldsWithAnnotation(containerObjectClass, annotationType);
if (annotatedFields.isEmpty()) return;
final HasPortBindings portBindings = dockerCube.getMetadata(HasPortBindings.class);
if (portBindings == null) {
throw new IllegalArgumentException(String.format(
"Container Object %s contains fields annotated with %s but no ports are exposed by the container",
containerObjectClass.getSimpleName(), annotationType.getSimpleName()));
}
for (Field annotatedField : annotatedFields) {
final T annotation = annotatedField.getAnnotation(annotationType);
try {
annotatedField.set(containerObjectInstance, fieldEnricher.apply(annotation, portBindings));
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException(
String.format("Container Object %s contains field %s annotated with %s, with error: %s",
containerObjectClass.getSimpleName(), annotatedField.getName(), annotationType.getSimpleName(),
ex.getLocalizedMessage()),
ex);
}
}
}
}