package org.arquillian.cube.docker.impl.await;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.arquillian.cube.docker.impl.client.config.Await;
import org.arquillian.cube.docker.impl.docker.DockerClientExecutor;
import org.arquillian.cube.docker.impl.util.Ping;
import org.arquillian.cube.impl.util.IOUtil;
import org.arquillian.cube.spi.Cube;
import org.arquillian.cube.spi.CubeOutput;
import org.arquillian.cube.spi.metadata.HasPortBindings;
import org.arquillian.cube.spi.metadata.HasPortBindings.PortAddress;
public class PollingAwaitStrategy extends SleepingAwaitStrategyBase {
public static final String TAG = "polling";
public static final String CONTAINER_DIRECTORY = "/tmp";
private static final Logger log = Logger.getLogger(PollingAwaitStrategy.class.getName());
private static final String MESSAGE = "Service is Up";
private static final String WAIT_FOR_IT_SCRIPT = "wait-for-it.sh";
private static final int DEFAULT_POLL_ITERATIONS = 80;
private static final String DEFAULT_POLL_TYPE = "sscommand";
private int pollIterations = DEFAULT_POLL_ITERATIONS;
private String type = DEFAULT_POLL_TYPE;
private DockerClientExecutor dockerClientExecutor;
private Cube<?> cube;
private List<Integer> ports = null;
// To avoid having to copy the script for all ports, state is saved.
private boolean alreadyCopiedWaitForIt = false;
public PollingAwaitStrategy(Cube<?> cube, DockerClientExecutor dockerClientExecutor, Await params) {
super(params.getSleepPollingTime());
this.cube = cube;
this.dockerClientExecutor = dockerClientExecutor;
if (params.getIterations() != null) {
this.pollIterations = params.getIterations();
}
if (params.getType() != null) {
this.type = params.getType();
}
if (params.getPorts() != null && params.getPorts().size() > 0) {
this.ports = params.getPorts();
}
}
public int getPollIterations() {
return pollIterations;
}
public String getType() {
return type;
}
public List<Integer> getPorts() {
return ports;
}
@Override
public boolean await() {
HasPortBindings portBindings = cube.getMetadata(HasPortBindings.class);
if (portBindings == null) {
log.fine("Cube does not have any ports to ping.");
return true;
}
Collection<Integer> pingPorts = this.ports;
if (ports == null) {
pingPorts = portBindings.getBoundPorts();
}
for (Integer port : pingPorts) {
switch (this.type) {
case "ping": {
PortAddress mapping = portBindings.getMappedAddress(port);
if (mapping == null) {
throw new IllegalArgumentException(
"Can not use polling of type " + type + " on non externally bound port " + port);
}
log.fine(String.format("Pinging host %s and port %s with type", mapping.getIP(), mapping.getPort(),
this.type));
if (!Ping.ping(mapping.getIP(), mapping.getPort(), this.pollIterations, this.getSleepTime(),
this.getTimeUnit())) {
return false;
}
}
break;
case "sscommand": {
try {
if (!Ping.ping(dockerClientExecutor, cube.getId(), resolveCommand("ss", port),
this.pollIterations, this.getSleepTime(), this.getTimeUnit())) {
return false;
}
} catch (UnsupportedOperationException e) {
// In case of not having ss command installed on container, it automatically fall back to waitforit approach
try {
if (!executeWaitForIt(portBindings.getInternalIP(), port)) {
return false;
}
} catch (UnsupportedOperationException ex) {
PortAddress mapping = portBindings.getMappedAddress(port);
if (mapping == null) {
throw new IllegalArgumentException(
"Can not use polling of type " + type + " on non externally bound port " + port);
}
log.fine(
String.format("Pinging host %s and port %s with type", mapping.getIP(), mapping.getPort(),
this.type));
if (!Ping.ping(mapping.getIP(), mapping.getPort(), this.pollIterations, this.getSleepTime(),
this.getTimeUnit())) {
return false;
}
}
}
}
break;
case "waitforit": {
if (!executeWaitForIt(portBindings.getInternalIP(), port)) {
return false;
}
}
}
}
return true;
}
private boolean executeWaitForIt(String containerIp, int port) {
// We copy our wait-for-it-sh.sh file form classpath
// Our wait-for-it-sh.sh script also works with busybox/alpine which is not true with the official one.
try {
if (!alreadyCopiedWaitForIt) {
final Path waitForItLocation = copyWaitForItScriptToTempDir();
// Then copy this into the container
dockerClientExecutor.copyStreamToContainer(cube.getId(), waitForItLocation.toFile(),
new File(CONTAINER_DIRECTORY));
alreadyCopiedWaitForIt = true;
}
String command = resolveWaitForItCommand(containerIp, port);
final String[] commands = {"sh", "-c", command};
CubeOutput result = dockerClientExecutor.execStart(cube.getId(), commands);
if (result.getError() != null && result.getError().contains("can't execute")) {
throw new UnsupportedOperationException(result.getError());
}
return result.getStandard() != null && result.getStandard().trim().contains(MESSAGE);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
private Path copyWaitForItScriptToTempDir() throws IOException {
final Path arquilliancube = Files.createTempDirectory("arquilliancube");
final Path waitForItLocation = arquilliancube.resolve(Paths.get(WAIT_FOR_IT_SCRIPT));
Files.copy(
PollingAwaitStrategy.class.getResourceAsStream("/org/arquillian/cube/docker/impl/await/wait-for-it.sh"),
waitForItLocation);
Files.setPosixFilePermissions(waitForItLocation, getScriptPermissions());
return waitForItLocation;
}
private Set<PosixFilePermission> getScriptPermissions() {
final PosixFilePermission ownerExecute = PosixFilePermission.OWNER_EXECUTE;
final PosixFilePermission groupExecute = PosixFilePermission.GROUP_EXECUTE;
final PosixFilePermission othersExecute = PosixFilePermission.OTHERS_EXECUTE;
final PosixFilePermission ownerRead = PosixFilePermission.OWNER_READ;
final PosixFilePermission groupRead = PosixFilePermission.GROUP_READ;
final PosixFilePermission othersRead = PosixFilePermission.OTHERS_READ;
final Set<PosixFilePermission> perms = new HashSet<>();
perms.addAll(Arrays.asList(
ownerExecute, ownerRead,
groupExecute, groupRead,
othersExecute, othersRead
)
);
return perms;
}
private String resolveWaitForItCommand(String containerIp, int port) {
return String.format("%s/%s %s:%s -s -- echo %s", CONTAINER_DIRECTORY, WAIT_FOR_IT_SCRIPT, containerIp, port,
MESSAGE);
}
private String resolveCommand(String command, int port) {
Map<String, String> values = new HashMap<String, String>();
values.put("port", Integer.toString(port));
String templateContent =
IOUtil.asStringPreservingNewLines(PollingAwaitStrategy.class.getResourceAsStream(command + ".sh"));
return IOUtil.replacePlaceholders(templateContent, values);
}
}