
client.DockerSshClient Maven / Gradle / Ivy
/**
* Copyright 2015 Groupon.com
*
* 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 client;
import client.docker.PortMapping;
import client.docker.inspectionbeans.ImageInspection;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.base.Charsets;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.connection.ConnectionException;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.transport.TransportException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import javax.inject.Named;
/**
* Ssh client for deploying docker images to a host.
*
* @author Matthew Hayter (mhayter at groupon dot com)
*/
public class DockerSshClient implements DockerDeploymentClient, DockerDeploymentClient.RunCommandRunner {
/**
* Public constructor.
*
* @param client an ssh client
* @param dockerCmd the command for "docker"
*/
@AssistedInject
public DockerSshClient(@Assisted final SSHClient client, @Named("DockerCmd") final String dockerCmd) {
_sshClient = client;
_dockerCmd = dockerCmd;
}
@Override
public List getRunningContainers() throws DockerDeploymentClientException {
final String inspectionJson;
/*
'docker ps -q | docker inspect' : -q tells docker to only show container ids (quiet mode), and then
'docker inspect' takes a list of ids and produces a json array.
*/
inspectionJson = sshExecAndGetOutput(_dockerCmd + " ps -q | xargs " + _dockerCmd + " inspect");
final List containers = new LinkedList<>();
if (inspectionJson.isEmpty()) {
return containers;
}
final ObjectMapper mapper = new ObjectMapper();
try {
final ArrayNode rootArray = mapper.readValue(inspectionJson, ArrayNode.class);
for (final JsonNode containerJson : rootArray) {
final ContainerDescription c = new ContainerDescription(
containerJson.get("Id").asText(),
containerJson.get("Name").asText(),
containerJson.get("Image").asText(),
containerJson.get("Created").asText()
);
containers.add(c);
}
} catch (final IOException e) {
throw new JsonFormatError("Error parsing JSON from docker command", e);
}
return containers;
}
@Override
public void shutdown() {
try {
_sshClient.disconnect();
_sshClient.close();
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void pull(final String imageReference) throws DockerDeploymentClientException {
sshExecExpectSuccess(_dockerCmd + " pull " + imageReference);
// All is well!
}
/**
* Runs the image using -d for detached-mode. This allows the run command to finish while the docker container
* continues to run.
*/
@Override
public void run(final DefaultDockerRunCommandBuilder runParameters) throws DockerDeploymentClientException {
final StringBuilder sb = new StringBuilder();
sb
.append(_dockerCmd)
.append(" run -d ")
.append(String.format("--name=%s ", runParameters.getContainerName()));
for (final PortMapping mapping : runParameters.getPortMappings()) {
sb.append(String.format("-p %d:%d ", mapping.getExternalPort(), mapping.getInternalPort()));
}
sb.append(runParameters.getImageReference());
sshExecExpectSuccess(sb.toString());
}
@Override
public DockerDeploymentClient.RunCommandBuilder createRunCommandBuilder(final String imageReference) {
return new DefaultDockerRunCommandBuilder(imageReference, this);
}
@Override
public void stopAndRemoveContainer(final String containerReference) throws DockerDeploymentClientException {
this.stop(containerReference);
this.rm(containerReference);
}
/**
* If the image reference does not exist, this function will throw a JsonFormatError as the
* output will not contain the expected objects.
*
* @param imageReferences the image references
*/
@Override
public List inspectImages(final List imageReferences) throws DockerDeploymentClientException {
if (imageReferences.size() == 0) {
return Collections.emptyList();
}
final ObjectMapper inspectionMapper = new ObjectMapper();
inspectionMapper.setPropertyNamingStrategy(new PropertyNamingStrategy.PropertyNamingStrategyBase() {
static final long serialVersionUID = 1L;
@Override
public String translate(final String s) {
// Json produced by docker uses capitalized words for property names.
return s.substring(0, 1).toUpperCase(Locale.ENGLISH) + s.substring(1);
}
});
final StringBuilder sb = new StringBuilder();
sb.append(_dockerCmd).append(" inspect ");
for (final String imageReference : imageReferences) {
sb.append(imageReference).append(" ");
}
final String inspectionJson = sshExecAndGetOutput(sb.toString());
final List inspections;
try {
inspections = inspectionMapper.readValue(
inspectionJson,
inspectionMapper.getTypeFactory().constructCollectionType(List.class, ImageInspection.class));
} catch (final IOException e) {
throw new DockerDeploymentClient.JsonFormatError("docker inspect output in unexpected format", e);
}
return inspections;
}
/**
* Stops a container.
*
* @param containerReference Either a container ID or a container name.
* @throws DockerDeploymentClientException on deployment error
*/
public void stop(final String containerReference) throws DockerDeploymentClientException {
sshExecExpectSuccess(_dockerCmd + " stop " + containerReference);
}
/**
* Removes a container.
*
* @param containerReference Either a container ID or a container name.
* @throws DockerDeploymentClientException on deployment error
*/
public void rm(final String containerReference) throws DockerDeploymentClientException {
sshExecExpectSuccess(_dockerCmd + " rm " + containerReference);
}
/**
* Handy for commands that should never fail, and you just want the output.
*/
private String sshExecAndGetOutput(final String cmd) throws DockerDeploymentClientException {
return withSession(session -> {
final Session.Command commandResult;
commandResult = session.exec(cmd);
//commandResult.join();
return IOUtils.readFully(commandResult.getInputStream()).toString(Charsets.UTF_8.name());
});
}
private void sshExecExpectSuccess(final String cmd) throws DockerDeploymentClientException {
final int exitStatus;
final Session.Command cmdResult;
cmdResult = withSession(session -> {
final Session.Command exec = session.exec(cmd);
exec.join();
return exec;
});
final Integer boxedExitStatus = cmdResult.getExitStatus();
if (boxedExitStatus == null) {
throw new DockerDeploymentClientException("Received null but expected exit status of SSH command");
}
exitStatus = boxedExitStatus;
if (exitStatus != 0) {
final String stdErr;
try {
stdErr = IOUtils.readFully(cmdResult.getErrorStream()).toString(Charsets.UTF_8.name());
} catch (final IOException e) {
throw new NonZeroExitStatusException(
String.format("Docker command exited with status %d but was unable to get stderr", exitStatus)
);
}
throw new NonZeroExitStatusException(
String.format("Docker command exited with status %d and stderr: %s", exitStatus, stdErr)
);
}
}
private R withSession(final FunctionWithException f) throws DockerDeploymentClientException {
Session session = null;
try {
session = _sshClient.startSession();
// CHECKSTYLE.OFF: IllegalCatch - we need to catch everything, we'll rethrow it later
} catch (final Exception e) {
// CHECKSTYLE.ON: IllegalCatch
throw new DockerDeploymentClientException("SSH Error", e);
}
final R ret;
try {
ret = f.accept(session);
// CHECKSTYLE.OFF: IllegalCatch - we need to catch everything, we'll rethrow it later
} catch (final Exception e) {
// CHECKSTYLE.ON: IllegalCatch
try {
session.close();
} catch (final TransportException | ConnectionException ignored) {
// Don't care
e.addSuppressed(ignored);
}
throw new DockerDeploymentClientException("SSH Error", e);
}
return ret;
}
private final SSHClient _sshClient;
private final String _dockerCmd;
@FunctionalInterface
private interface FunctionWithException {
R accept(T param) throws Exception;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy