io.bitgrillr.gocddockerexecplugin.docker.DockerUtils.java Source code

Java tutorial

Introduction

Here is the source code for io.bitgrillr.gocddockerexecplugin.docker.DockerUtils.java

Source

/**
 * Copyright 2018 Christopher Arnold <cma.arnold@gmail.com> and contributors.
 *
 * 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 io.bitgrillr.gocddockerexecplugin.docker;

import com.spotify.docker.client.DefaultDockerClient;
import com.spotify.docker.client.DockerClient;
import com.spotify.docker.client.DockerClient.ExecCreateParam;
import com.spotify.docker.client.DockerClient.RemoveContainerParam;
import com.spotify.docker.client.LogStream;
import com.spotify.docker.client.exceptions.DockerException;
import com.spotify.docker.client.exceptions.ImageNotFoundException;
import com.spotify.docker.client.exceptions.ImagePullFailedException;
import com.spotify.docker.client.messages.ContainerConfig;
import com.spotify.docker.client.messages.ExecCreation;
import com.spotify.docker.client.messages.HostConfig;
import com.thoughtworks.go.plugin.api.task.JobConsoleLogger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;

/**
 * Contains various utility methods for interacting with the Docker daemon.
 */
public class DockerUtils {

    static DockerClient dockerClient = null;

    private DockerUtils() {
    }

    static DockerClient getDockerClient() {
        if (dockerClient == null) {
            dockerClient = new DefaultDockerClient(
                    System.getProperty("gocddockerexecplugin.dockerhost", "unix:///var/run/docker.sock"));
        }
        return dockerClient;
    }

    /**
     * Pulls the specified image.
     *
     * @param image Image to pull.
     * @throws DockerException If an error occurs.
     * @throws InterruptedException If the process is interrupted.
     */
    public static void pullImage(String image) throws DockerException, InterruptedException {
        JobConsoleLogger.getConsoleLogger()
                .printLine((new StringBuilder()).append("Pulling image '").append(image).append("'").toString());
        // basic logic for ProgressHandler pulled from LoggingPullHandler in docker-client
        getDockerClient().pull(image, pm -> {
            if (pm.error() != null) {
                if (pm.error().contains("404") || pm.error().toLowerCase().contains("not found")) {
                    throw new ImageNotFoundException(image, pm.toString());
                } else {
                    throw new ImagePullFailedException(image, pm.toString());
                }
            } else {
                StringBuilder message = new StringBuilder().append(pm.status());
                if ("Downloading".equals(pm.status()) || "Extracting".equals(pm.status())) {
                    message.append(" ");
                    message.append(pm.progress());
                }
                JobConsoleLogger.getConsoleLogger().printLine(message.toString());
            }
        });
    }

    /**
     * Creates a container for the specified image and starts it with the command 'tail -f /dev/null'.
     *
     * @param image Image to create the container from.
     * @param pwd Working directory to be bind mounted into the container.
     * @return ID of the created container.
     * @throws DockerException If an error occurs creating the container.
     * @throws InterruptedException If the process is interrupted.
     */
    public static String createContainer(String image, String pwd, Map<String, String> envVars)
            throws DockerException, InterruptedException {
        JobConsoleLogger.getConsoleLogger().printLine((new StringBuilder())
                .append("Creating container from image '").append(image).append("'").toString());
        final String id = getDockerClient()
                .createContainer(ContainerConfig.builder().image(image).cmd("tail", "-f", "/dev/null")
                        .hostConfig(HostConfig.builder()
                                .appendBinds((new StringBuilder()).append(pwd).append(":/app").toString()).build())
                        .workingDir("/app")
                        .env(envVars.entrySet().stream().<List<String>>reduce(new ArrayList<>(), (memo, value) -> {
                            memo.add((new StringBuilder().append(value.getKey()).append("=")
                                    .append(value.getValue())).toString());
                            return memo;
                        }, (memo1, memo2) -> {
                            memo1.addAll(memo2);
                            return memo1;
                        })).build())
                .id();
        JobConsoleLogger.getConsoleLogger()
                .printLine((new StringBuilder()).append("Created container '").append(id).append("'").toString());
        getDockerClient().startContainer(id);
        JobConsoleLogger.getConsoleLogger()
                .printLine((new StringBuilder()).append("Started container '").append(id).append("'").toString());
        return id;
    }

    /**
     * Executes the specified command in the given container as the specified user.
     *
     * @param containerId Id of the container to execute the command in.
     * @param user User to execute the command as. Pass 'null' to run as default user of the container.
     * @param cmd Command to execute.
     * @param args An Array of arguments.
     * @return Exit code of the command.
     * @throws DockerException If an error occurs executing the command.
     * @throws InterruptedException If teh process is interrupted.
     */
    public static int execCommand(String containerId, String user, String cmd, String... args)
            throws DockerException, InterruptedException {
        return execCommand(containerId, line -> JobConsoleLogger.getConsoleLogger().printLine(line), user, cmd,
                args);
    }

    /**
     * Returns the UID of the default user of the container.
     *
     * @param containerId Id of the container.
     * @return The UID of the default user.
     * @throws DockerException In an error occurs executing the command.
     * @throws InterruptedException If the process is interrupted.
     */
    public static String getContainerUid(String containerId) throws DockerException, InterruptedException {
        final List<String> uid = new ArrayList<>();
        final int exitCode = execCommand(containerId, uid::add, null, "sh", "-c", "echo \"$(id -u):$(id -g)\"");
        if (exitCode != 0) {
            throw new IllegalStateException("echo ${UID} command failed");
        }
        return uid.get(0);
    }

    /**
     * Stops and removes the specified container and it's volumes ('docker rm -v containerId'). This will
     * wait one minute before issuing SIGKILL to the container.
     *
     * @param containerId ID of container to remove.
     * @throws DockerException If an occurs removing the container.
     * @throws InterruptedException If the process is interrupted.
     */
    public static void removeContainer(String containerId) throws DockerException, InterruptedException {
        JobConsoleLogger.getConsoleLogger().printLine(
                (new StringBuilder()).append("Stopping container '").append(containerId).append("'").toString());
        getDockerClient().stopContainer(containerId, 60);

        JobConsoleLogger.getConsoleLogger().printLine(
                (new StringBuilder()).append("Removing container '").append(containerId).append("'").toString());
        getDockerClient().removeContainer(containerId, RemoveContainerParam.removeVolumes());
    }

    /**
     * Return a String representation of the given command and arguments, e.g. "'echo 'hello' 'world'".
     *
     * @param command Command.
     * @param arguments Arguments.
     * @return Printable String.
     */
    public static String getCommandString(String command, String... arguments) {
        return (new StringBuilder()).append("'").append(command)
                .append(Arrays.stream(arguments)
                        .reduce(new StringBuilder(),
                                (accumulator, value) -> accumulator.append(" '").append(value).append("'"),
                                (partial1, partial2) -> partial1.append(partial2.toString()))
                        .toString())
                .append("'").toString();
    }

    private static int execCommand(String containerId, Printer printer, String user, String cmd, String... args)
            throws DockerException, InterruptedException {
        JobConsoleLogger.getConsoleLogger().printLine((new StringBuilder())
                .append("Creating exec instance for command ").append(getCommandString(cmd, args)).toString());
        String[] cmdArray = new String[args.length + 1];
        cmdArray[0] = cmd;
        System.arraycopy(args, 0, cmdArray, 1, args.length);
        // for whatever reason, unless all streams are attached, the exec barfs and no-one knows why
        // see https://github.com/spotify/docker-client/issues/513
        List<ExecCreateParam> execCreateParams = new ArrayList<>();
        execCreateParams.add(ExecCreateParam.attachStdout());
        execCreateParams.add(ExecCreateParam.attachStderr());
        execCreateParams.add(ExecCreateParam.attachStdin());
        if (user != null) {
            execCreateParams.add(ExecCreateParam.user(user));
        }
        final ExecCreation execCreation = getDockerClient().execCreate(containerId, cmdArray,
                execCreateParams.toArray(new ExecCreateParam[execCreateParams.size()]));
        if (execCreation.warnings() != null && !execCreation.warnings().isEmpty()) {
            for (final String warning : execCreation.warnings()) {
                JobConsoleLogger.getConsoleLogger()
                        .printLine((new StringBuilder()).append("WARNING: ").append(warning).toString());
            }
        }
        JobConsoleLogger.getConsoleLogger().printLine((new StringBuilder()).append("Created exec instance '")
                .append(execCreation.id()).append("'").toString());

        JobConsoleLogger.getConsoleLogger().printLine((new StringBuilder()).append("Starting exec instance '")
                .append(execCreation.id()).append("'").toString());
        try (final LogStream logStream = getDockerClient().execStart(execCreation.id())) {
            while (logStream.hasNext()) {
                final String logMessage = StringUtils
                        .chomp(StandardCharsets.UTF_8.decode(logStream.next().content()).toString());
                for (String logLine : logMessage.split("\n")) {
                    printer.print(logLine);
                }
            }
        }

        final Integer exitStatus = getDockerClient().execInspect(execCreation.id()).exitCode();
        if (exitStatus == null) {
            throw new IllegalStateException("Exit code of exec comand null");
        }
        JobConsoleLogger.getConsoleLogger().printLine((new StringBuilder()).append("Exec instance '")
                .append(execCreation.id()).append("' exited with status ").append(exitStatus).toString());
        return exitStatus;
    }

    private interface Printer {
        void print(String line);
    }

}