package org.arquillian.cube.docker.impl.model; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.exception.NotModifiedException; import java.net.InetAddress; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import org.arquillian.cube.docker.impl.await.AwaitStrategyFactory; import org.arquillian.cube.docker.impl.client.config.CubeContainer; import org.arquillian.cube.docker.impl.client.metadata.ChangesOnFilesystem; import org.arquillian.cube.docker.impl.client.metadata.CopyFromContainer; import org.arquillian.cube.docker.impl.client.metadata.CopyToContainer; import org.arquillian.cube.docker.impl.client.metadata.ExecuteProcessInContainer; import org.arquillian.cube.docker.impl.client.metadata.GetTop; import org.arquillian.cube.docker.impl.client.metadata.ReportMetrics; import org.arquillian.cube.docker.impl.docker.DockerClientExecutor; import org.arquillian.cube.docker.impl.util.BindingUtil; import org.arquillian.cube.spi.BaseCube; import org.arquillian.cube.spi.Binding; import org.arquillian.cube.spi.Binding.PortBinding; import org.arquillian.cube.spi.CubeControlException; import org.arquillian.cube.spi.event.lifecycle.AfterCreate; import org.arquillian.cube.spi.event.lifecycle.AfterDestroy; import org.arquillian.cube.spi.event.lifecycle.AfterStart; import org.arquillian.cube.spi.event.lifecycle.AfterStop; import org.arquillian.cube.spi.event.lifecycle.BeforeCreate; import org.arquillian.cube.spi.event.lifecycle.BeforeDestroy; import org.arquillian.cube.spi.event.lifecycle.BeforeStart; import org.arquillian.cube.spi.event.lifecycle.BeforeStop; import org.arquillian.cube.spi.event.lifecycle.CubeLifecyleEvent; import org.arquillian.cube.spi.metadata.CanCopyFromContainer; import org.arquillian.cube.spi.metadata.CanCopyToContainer; import org.arquillian.cube.spi.metadata.CanExecuteProcessInContainer; import org.arquillian.cube.spi.metadata.CanReportMetrics; import org.arquillian.cube.spi.metadata.CanSeeChangesOnFilesystem; import org.arquillian.cube.spi.metadata.CanSeeTop; import org.arquillian.cube.spi.metadata.HasPortBindings; import org.arquillian.cube.spi.metadata.IsBuildable; import org.jboss.arquillian.core.api.Event; import org.jboss.arquillian.core.api.annotation.Inject; public class DockerCube extends BaseCube<CubeContainer> { private static final Logger log = Logger.getLogger(DockerCube.class.getName()); private final PortBindings portBindings; private State state = State.DESTROYED; private String id; private Binding binding = null; private CubeContainer configuration; private long startingTimeInMillis = 0; private long stoppingTimeInMillis = 0; @Inject private Event<CubeLifecyleEvent> lifecycle; private DockerClientExecutor executor; public DockerCube(String id, CubeContainer configuration, DockerClientExecutor executor) { this.id = id; this.configuration = configuration; this.executor = executor; this.portBindings = new PortBindings(); addDefaultMetadata(); } private void addDefaultMetadata() { addMetadata(CanCopyToContainer.class, new CopyToContainer(getId(), executor)); addMetadata(CanExecuteProcessInContainer.class, new ExecuteProcessInContainer(getId(), executor)); addMetadata(CanCopyFromContainer.class, new CopyFromContainer(getId(), executor)); addMetadata(CanSeeChangesOnFilesystem.class, new ChangesOnFilesystem(getId(), executor)); addMetadata(CanSeeTop.class, new GetTop(getId(), executor)); addMetadata(HasPortBindings.class, portBindings); addMetadata(CanReportMetrics.class, new ReportMetrics(this)); if (configuration.getBuildImage() != null) { String path = configuration.getBuildImage().getDockerfileLocation(); if (path != null) { addMetadata(IsBuildable.class, new IsBuildable(path)); } } } @Override public State state() { return state; } @Override public String getId() { return id; } @Override public void create() throws CubeControlException { if (state != State.DESTROYED) { return; } try { lifecycle.fire(new BeforeCreate(id)); log.fine(String.format("Creating container with name %s and configuration %s.", id, configuration)); long currentTime = System.currentTimeMillis(); executor.createContainer(id, configuration); this.startingTimeInMillis = System.currentTimeMillis() - currentTime; log.fine(String.format("Created container with id %s.", id)); state = State.CREATED; lifecycle.fire(new AfterCreate(id)); } catch (Exception e) { state = State.CREATE_FAILED; throw CubeControlException.failedCreate(id, e); } } @Override public void start() throws CubeControlException { if (state == State.STARTED || state == State.PRE_RUNNING) { return; } try { lifecycle.fire(new BeforeStart(id)); long currentTime = System.currentTimeMillis(); executor.startContainer(id, configuration); long partialDuration = System.currentTimeMillis() - currentTime; this.startingTimeInMillis = this.startingTimeInMillis + partialDuration; state = State.STARTED; portBindings.containerStarted(); if (!AwaitStrategyFactory.create(executor, this, configuration).await()) { throw new IllegalArgumentException(String.format("Cannot connect to %s container", id)); } lifecycle.fire(new AfterStart(id)); } catch (Exception e) { state = State.START_FAILED; throw CubeControlException.failedStart(id, e); } } @Override public void stop() throws CubeControlException { if (state == State.STOPPED || state == State.PRE_RUNNING) { return; } try { lifecycle.fire(new BeforeStop(id)); long currentTime = System.currentTimeMillis(); try { if (configuration.isKillContainer()) { executor.killContainer(id); } else { executor.stopContainer(id); } } catch (NotFoundException e) { } catch (NotModifiedException e) { } this.stoppingTimeInMillis = System.currentTimeMillis() - currentTime; state = State.STOPPED; lifecycle.fire(new AfterStop(id)); } catch (Exception e) { state = State.STOP_FAILED; throw CubeControlException.failedStop(id, e); } } @Override public void destroy() throws CubeControlException { if (state != State.STOPPED) { return; } try { lifecycle.fire(new BeforeDestroy(id)); long currentTime = System.currentTimeMillis(); try { executor.removeContainer(id, configuration.getRemoveVolumes()); } catch (NotFoundException e) { } catch (NotModifiedException e) { } long partialDuration = System.currentTimeMillis() - currentTime; this.stoppingTimeInMillis = this.stoppingTimeInMillis + partialDuration; state = State.DESTROYED; lifecycle.fire(new AfterDestroy(id)); } catch (Exception e) { state = State.DESTORY_FAILED; throw CubeControlException.failedDestroy(id, e); } } @Override public Binding bindings() { if (binding != null) { return binding; } if (state != State.STARTED && state != State.PRE_RUNNING) { throw new IllegalStateException("Can't get binding for cube " + id + " when status not " + State.STARTED + " or " + State.PRE_RUNNING + ". Status is " + state); } binding = BindingUtil.binding(executor, id); return binding; } @Override public Binding configuredBindings() { return BindingUtil.binding(configuration, executor); } @Override public boolean isRunningOnRemote() { // TODO should we create an adapter class so we don't expose client classes in this part? List<com.github.dockerjava.api.model.Container> runningContainers = executor.listRunningContainers(); for (com.github.dockerjava.api.model.Container container : runningContainers) { for (String name : container.getNames()) { if (name.startsWith("/")) { name = name.substring(1); // Names array adds an slash to the docker name container. } if (name.equals(getId())) { // cube id is the container name in docker0 Id in docker is the hash // that identifies it. return true; } } } return false; } @Override public CubeContainer configuration() { return configuration; } @Override public void changeToPreRunning() { if (state != State.DESTROYED && state != State.STARTED) { return; } log.fine(String.format("Reusing prerunning container with name %s and configuration %s.", id, configuration)); state = State.PRE_RUNNING; } public long getStartingTimeInMillis() { return startingTimeInMillis; } public long getStoppingTimeInMillis() { return stoppingTimeInMillis; } private class PortBindings implements HasPortBindings { private final Map<Integer, PortAddress> mappedPorts; private final Set<Integer> containerPorts; private final Set<Integer> boundPorts; private String containerIP; private String internalIP; private PortBindings() { this.mappedPorts = new HashMap<Integer, PortAddress>(); this.containerPorts = new LinkedHashSet<Integer>(); final Binding configuredBindings = configuredBindings(); containerIP = configuredBindings.getIP(); for (PortBinding portBinding : configuredBindings.getPortBindings()) { final int exposedPort = portBinding.getExposedPort(); final Integer boundPort = portBinding.getBindingPort(); containerPorts.add(exposedPort); if (boundPort != null && containerIP != null) { mappedPorts.put(exposedPort, new PortAddressImpl(containerIP, boundPort)); } } this.boundPorts = new LinkedHashSet<Integer>(containerPorts.size()); } @Override public boolean isBound() { return EnumSet.of(State.PRE_RUNNING, State.STARTED).contains(state); } @Override public synchronized String getContainerIP() { return containerIP; } @Override public String getInternalIP() { return internalIP; } @Override public Set<Integer> getContainerPorts() { return Collections.unmodifiableSet(containerPorts); } @Override public synchronized Set<Integer> getBoundPorts() { return isBound() ? Collections.unmodifiableSet(boundPorts) : getContainerPorts(); } @Override public synchronized PortAddress getMappedAddress(int targetPort) { if (mappedPorts.containsKey(targetPort)) { return mappedPorts.get(targetPort); } return null; } @Override public InetAddress getPortForwardBindAddress() { return null; } /* * Initialize bound ports and regenerate port mappings */ private synchronized void containerStarted() { final Binding bindings = bindings(); containerIP = bindings.getIP(); internalIP = bindings.getInternalIP(); for (PortBinding portBinding : bindings.getPortBindings()) { final int exposedPort = portBinding.getExposedPort(); final Integer boundPort = portBinding.getBindingPort(); boundPorts.add(exposedPort); if (boundPort != null && containerIP != null) { // just overwrite existing entries mappedPorts.put(exposedPort, new PortAddressImpl(containerIP, boundPort)); } } } } }