org.jboss.pnc.environment.docker.DockerEnvironmentDriver.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.pnc.environment.docker.DockerEnvironmentDriver.java

Source

/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.environment.docker;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Module;
import org.jboss.pnc.common.Configuration;
import org.jboss.pnc.common.json.ConfigurationParseException;
import org.jboss.pnc.common.json.moduleconfig.DockerEnvironmentDriverModuleConfig;
import org.jboss.pnc.common.json.moduleprovider.PncConfigProvider;
import org.jboss.pnc.common.monitor.PullingMonitor;
import org.jboss.pnc.common.util.HttpUtils;
import org.jboss.pnc.model.BuildType;
import org.jboss.pnc.spi.environment.EnvironmentDriver;
import org.jboss.pnc.spi.environment.StartedEnvironment;
import org.jboss.pnc.spi.environment.exception.EnvironmentDriverException;
import org.jboss.pnc.spi.repositorymanager.model.RepositorySession;
import org.jclouds.ContextBuilder;
import org.jclouds.compute.ComputeServiceContext;
import org.jclouds.docker.DockerApi;
import org.jclouds.docker.domain.Config;
import org.jclouds.docker.domain.Container;
import org.jclouds.docker.domain.HostConfig;
import org.jclouds.docker.features.RemoteApi;
import org.jclouds.docker.options.RemoveContainerOptions;
import org.jclouds.logging.slf4j.config.SLF4JLoggingModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.lang.invoke.MethodHandles;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Implementation of environment driver, which uses Docker to run environments
 * 
 * @author Jakub Bartecek <jbartece@redhat.com>
 *
 */
@ApplicationScoped
public class DockerEnvironmentDriver implements EnvironmentDriver {

    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass().getName());

    // A path in the remote container
    private static final Path workingDirectory = Paths.get("/tmp");
    private boolean disabled;

    @Inject
    private Generator generator;

    private ComputeServiceContext dockerContext;

    private RemoteApi dockerClient;

    /** User in running environment to which we can connect with SSH */
    private String containerUser;

    /** Password of user to which we can connect with SSH */
    private String containerUserPsswd;

    /** Connection URL to Docker control port */
    private String dockerEndpoint;

    /** ID of docker image */
    private String dockerImageId;

    private String dockerIp;

    private String containerFirewallAllowedDestinations;

    /** proxy server settings passes as environment variables to docker builder */
    private String proxyServer;

    private String proxyPort;

    private String nonProxyHosts;

    private PullingMonitor pullingMonitor;

    /**
     * States of creating Docker container
     * 
     * @author Jakub Bartecek <jbartece@redhat.com>
     *
     */
    private enum BuildContainerState {
        NOT_BUILT, BUILT, STARTED,
    }

    /**
     * Only workaround for CDI constructor parameter injection
     */
    @Deprecated
    public DockerEnvironmentDriver() {
    }

    /**
     * Loads configuration, prepares connection to Docker daemon
     * 
     * @throws ConfigurationParseException Thrown if configuration cannot be obtained
     */
    @Inject
    public DockerEnvironmentDriver(Configuration configuration, PullingMonitor pullingMonitor)
            throws ConfigurationParseException {
        this.pullingMonitor = pullingMonitor;
        DockerEnvironmentDriverModuleConfig config = configuration
                .getModuleConfig(new PncConfigProvider<DockerEnvironmentDriverModuleConfig>(
                        DockerEnvironmentDriverModuleConfig.class));

        dockerIp = config.getIp();
        dockerEndpoint = "http://" + dockerIp + ":2375";
        containerUser = config.getInContainerUser();
        containerUserPsswd = config.getInContainerUserPassword();
        dockerImageId = config.getImageId();
        containerFirewallAllowedDestinations = config.getFirewallAllowedDestinations();
        proxyServer = config.getProxyServer();
        proxyPort = config.getProxyPort();
        nonProxyHosts = config.getNonProxyHosts();
        disabled = config.isDisabled();
        logger.info("Is Docker environment driver disabled: {}", disabled);

        dockerContext = ContextBuilder.newBuilder("docker").endpoint(dockerEndpoint)
                .credentials(containerUser, containerUserPsswd)
                .modules(ImmutableSet.<Module>of(new SLF4JLoggingModule())).buildView(ComputeServiceContext.class);
        dockerClient = dockerContext.unwrapApi(DockerApi.class).getRemoteApi();
    }

    @Override
    public StartedEnvironment buildEnvironment(BuildType buildType, RepositorySession repositorySession)
            throws EnvironmentDriverException {
        if (!canBuildEnvironment(buildType))
            throw new UnsupportedOperationException(
                    "DockerEnvironmentDriver currently provides support only for Linux environments on Docker.");

        String containerId = generator.generateContainerId();
        BuildContainerState buildContainerState = BuildContainerState.NOT_BUILT;

        logger.info("Trying to start Docker container...");
        int sshPort, jenkinsPort;
        try {
            Config config = Config.builder().imageId(dockerImageId)
                    .env(prepareEnvVariables(repositorySession.getConnectionInfo().getDependencyUrl(),
                            repositorySession.getConnectionInfo().getDeployUrl(), proxyServer, proxyPort,
                            repositorySession.getBuildRepositoryId(), nonProxyHosts))
                    .build();
            logger.debug("Creating docker container with config: " + config);
            Container createdContainer = dockerClient.createContainer(containerId, config);
            buildContainerState = BuildContainerState.BUILT;

            dockerClient.startContainer(containerId,
                    HostConfig.builder().publishAllPorts(true).privileged(true).build());
            buildContainerState = BuildContainerState.STARTED;

            Map<String, HostPortMapping> containerPortMappings = getContainerPortMappings(createdContainer.getId());
            // Find out, which ports are opened for SSH and Jenkins
            sshPort = getSshPort(containerPortMappings);
            jenkinsPort = getJenkinsPort(containerPortMappings);
        } catch (Exception e) {
            // Creating container failed => clean up
            logger.warn("Docker container failed to start. " + e);

            if (buildContainerState != BuildContainerState.NOT_BUILT) {
                if (buildContainerState == BuildContainerState.BUILT)
                    destroyContainer(containerId, false);
                else
                    destroyContainer(containerId, true);
            }
            throw new EnvironmentDriverException("Docker container couldn't be created.", e);
        }

        logger.info("Created and started Docker container. ID: " + containerId + ", SSH port: " + sshPort
                + ", Jenkins Port: " + jenkinsPort + ", Working directory: " + workingDirectory);

        return new DockerStartedEnvironment(this, pullingMonitor, repositorySession, containerId, jenkinsPort,
                sshPort, "http://" + dockerIp, workingDirectory);
    }

    @Override
    public boolean canBuildEnvironment(BuildType buildType) {
        if (disabled) {
            logger.info("Skipping driver as it is disabled by config.");
            return false;
        }

        if (buildType == BuildType.JAVA)
            return true;
        else
            return false;
    }

    /**
     * Destroys running container
     * 
     * @param containerId ID of container
     * @throws EnvironmentDriverException Thrown if any error occurs during destroying running environment
     */
    public void destroyEnvironment(String containerId) throws EnvironmentDriverException {
        destroyContainer(containerId, true);
    }

    /**
     * Destroys container
     * 
     * @param containerId ID of container
     * @param isRunning True if the container is running
     * @throws EnvironmentDriverException Thrown if any error occurs during destroying running environment
     */
    private void destroyContainer(String containerId, boolean isRunning) throws EnvironmentDriverException {
        logger.info("Trying to destroy Docker container with ID: " + containerId);
        try {
            if (isRunning)
                dockerClient.stopContainer(containerId);
            dockerClient.removeContainer(containerId, new RemoveContainerOptionsExtended().force(true));
        } catch (RuntimeException e) {
            logger.warn("Docker container (ID:" + containerId + " )couldn't be removed: " + e);
            throw new EnvironmentDriverException("Cannot destroy environment.", e);
        }
        logger.info("Docker container with ID: " + containerId + " was destroyed.");
    }

    /**
     * Gets public host port of Jenkins
     * 
     * @param ports Port mappings of container
     * @return Public host port of Jenkins
     */
    private int getJenkinsPort(Map<String, HostPortMapping> ports) {
        return Integer.parseInt(ports.get("8080").getHostPort());
    }

    /**
     * Gets public host port of SSH
     * 
     * @param ports Port mappings of container
     * @return Public host port of SSH
     */
    private int getSshPort(Map<String, HostPortMapping> ports) {
        return Integer.parseInt(ports.get("22").getHostPort());
    }

    /**
     * Prepares configuration of environment variables
     * for creating Docker container
     * 
     * @param dependencyUrl AProx dependencyUrl
     * @param deployUrl AProx deployUrl
     * @param proxyServer Proxy server IP address or DNS resolvable name
     * @param proxyPort number of proxy server port where is it listening
     * @param proxyUsername the getBuildRepositoryId for tracking
     * @param nonProxyHosts the list of '|' delimited addresses/names ('*' wildcards allowed) which should NOT be proxied
     * @return Environment variables configuration
     */
    private List<String> prepareEnvVariables(String dependencyUrl, String deployUrl, String proxyServer,
            String proxyPort, String proxyUsername, String nonProxyHosts) {
        String proxyActive = "false";

        if ((proxyServer != null && proxyPort != null) && (!proxyServer.isEmpty() && !proxyPort.isEmpty())) {
            proxyActive = "true";
        }

        List<String> envVariables = new ArrayList<>();
        envVariables.add("firewallAllowedDestinations=" + containerFirewallAllowedDestinations);
        envVariables.add("AProxDependencyUrl=" + dependencyUrl);
        envVariables.add("AProxDeployUrl=" + deployUrl);
        envVariables.add("isHttpActive=" + proxyActive);
        envVariables.add("proxyServer=" + proxyServer);
        envVariables.add("proxyPort=" + proxyPort);
        envVariables.add("proxyUsername=" + proxyUsername);
        envVariables.add("nonProxyHosts=" + nonProxyHosts);

        if (logger.isDebugEnabled()) {
            logger.debug("Setting environment variables for docker container " + envVariables.toString());
        }
        return envVariables;
    }

    /**
     * Get container port mapping from Docker daemon REST interface.
     * 
     * @param containerId ID of running container
     * @return Map with pairs containerPort:publicPort
     * @throws Exception Thrown if data could not be obtained from Docker daemon or are corrupted
     */
    private Map<String, HostPortMapping> getContainerPortMappings(String containerId) throws Exception {
        Map<String, HostPortMapping> resultMap = new HashMap<>();
        String response = HttpUtils.processGetRequest(String.class,
                dockerEndpoint + "/containers/" + containerId + "/json");

        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode rootNode = objectMapper.readTree(response);
        JsonNode networkSettingsNode = rootNode.path("NetworkSettings");
        JsonNode portsNode = networkSettingsNode.path("Ports");

        Map<String, List<HostPortMapping>> portsMap = objectMapper.readValue(portsNode.traverse(),
                new TypeReference<Map<String, List<HostPortMapping>>>() {
                });

        for (Map.Entry<String, List<HostPortMapping>> entry : portsMap.entrySet()) {
            resultMap.put(entry.getKey().substring(0, entry.getKey().indexOf("/")), entry.getValue().get(0));
        }

        return resultMap;
    }

    /**
     * Extended options for removing container, which forces Docker to remove
     * volume attached to the container 
     * 
     * @author Jakub Bartecek &lt;jbartece@redhat.com&gt;
     *
     */
    private class RemoveContainerOptionsExtended extends RemoveContainerOptions {

        public RemoveContainerOptionsExtended() {
            this.queryParameters.put("v", "true");
        }
    }

}