@Before public void baseSetup() throws Exception { System.setProperty("user.name", TEST_USER); masterPort = temporaryPorts.localPort("helios master"); masterAdminPort = temporaryPorts.localPort("helios master admin"); masterEndpoint = "http://localhost:" + masterPort(); masterAdminEndpoint = "http://localhost:" + masterAdminPort(); zk = zooKeeperTestManager(); listThreads(); zk.ensure("/config"); zk.ensure("/status"); agentStateDirs = temporaryFolder.newFolder("helios-agents").toPath(); masterStateDirs = temporaryFolder.newFolder("helios-masters").toPath(); // TODO (mbrown): not 100% sure what a minimal client is but it sounds good httpClient = HttpClients.createMinimal(); }
@Before public void dockerSetup() throws Exception { final String portRange = System.getenv("DOCKER_PORT_RANGE"); final AllocatedPort allocatedPort; final int probePort; if (portRange != null) { final String[] parts = portRange.split(":", 2); dockerPortRange = Range.closedOpen(Integer.valueOf(parts[0]), Integer.valueOf(parts[1])); allocatedPort = Polling.await( LONG_WAIT_SECONDS, SECONDS, new Callable<AllocatedPort>() { @Override public AllocatedPort call() throws Exception { final int port = ThreadLocalRandom.current() .nextInt( dockerPortRange.lowerEndpoint(), dockerPortRange.upperEndpoint()); return temporaryPorts.tryAcquire("docker-probe", port); } }); probePort = allocatedPort.port(); } else { dockerPortRange = temporaryPorts.localPortRange("docker", 10); probePort = dockerPortRange().lowerEndpoint(); allocatedPort = null; } try { assertDockerReachable(probePort); } finally { if (allocatedPort != null) { allocatedPort.release(); } } }
public abstract class SystemTestBase { private static final Logger log = LoggerFactory.getLogger(SystemTestBase.class); public static final int WAIT_TIMEOUT_SECONDS = 40; public static final int LONG_WAIT_SECONDS = 400; public static final String BUSYBOX = "busybox:latest"; public static final String BUSYBOX_WITH_DIGEST = "busybox@sha256:16a2a52884c2a9481ed267c2d46483eac7693b813a63132368ab098a71303f8a"; public static final String NGINX = "rohan/nginx-alpine:latest"; public static final String UHTTPD = "fnichol/docker-uhttpd:latest"; public static final String ALPINE = "onescience/alpine:latest"; public static final String MEMCACHED = "rohan/memcached-mini:latest"; public static final List<String> IDLE_COMMAND = asList("sh", "-c", "trap 'exit 0' SIGINT SIGTERM; while :; do sleep 1; done"); public final String testTag = "test_" + randomHexString(); public final String testJobName = "job_" + testTag; public final String testJobVersion = "v" + randomHexString(); public final String testJobNameAndVersion = testJobName + ":" + testJobVersion; public static final DockerHost DOCKER_HOST = DockerHost.fromEnv(); public static final String TEST_USER = "******"; public static final String TEST_HOST = "test-host"; public static final String TEST_MASTER = "test-master"; public static final String MASTER_USER = "******"; public static final String MASTER_PASSWORD = "******"; public static final String AGENT_USER = "******"; public static final String AGENT_PASSWORD = "******"; public static final String MASTER_DIGEST = ZooKeeperAclProviders.digest(MASTER_USER, MASTER_PASSWORD); public static final String AGENT_DIGEST = ZooKeeperAclProviders.digest(AGENT_USER, AGENT_PASSWORD); @Rule public final TemporaryPorts temporaryPorts = TemporaryPorts.create(); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Rule public final ExpectedException exception = ExpectedException.none(); @Rule public final TestRule watcher = new LoggingTestWatcher(); private int masterPort; private int masterAdminPort; private String masterEndpoint; private String masterAdminEndpoint; private Range<Integer> dockerPortRange; private final List<Service> services = newArrayList(); private final List<HeliosClient> clients = Lists.newArrayList(); private Path agentStateDirs; private Path masterStateDirs; private ZooKeeperTestManager zk; protected final String zkClusterId = String.valueOf(ThreadLocalRandom.current().nextInt(10000)); /** An HttpClient that can be used for sending arbitrary HTTP requests */ protected CloseableHttpClient httpClient; @BeforeClass public static void staticSetup() { SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); } @Before public void baseSetup() throws Exception { System.setProperty("user.name", TEST_USER); masterPort = temporaryPorts.localPort("helios master"); masterAdminPort = temporaryPorts.localPort("helios master admin"); masterEndpoint = "http://localhost:" + masterPort(); masterAdminEndpoint = "http://localhost:" + masterAdminPort(); zk = zooKeeperTestManager(); listThreads(); zk.ensure("/config"); zk.ensure("/status"); agentStateDirs = temporaryFolder.newFolder("helios-agents").toPath(); masterStateDirs = temporaryFolder.newFolder("helios-masters").toPath(); // TODO (mbrown): not 100% sure what a minimal client is but it sounds good httpClient = HttpClients.createMinimal(); } @Before public void dockerSetup() throws Exception { final String portRange = System.getenv("DOCKER_PORT_RANGE"); final AllocatedPort allocatedPort; final int probePort; if (portRange != null) { final String[] parts = portRange.split(":", 2); dockerPortRange = Range.closedOpen(Integer.valueOf(parts[0]), Integer.valueOf(parts[1])); allocatedPort = Polling.await( LONG_WAIT_SECONDS, SECONDS, new Callable<AllocatedPort>() { @Override public AllocatedPort call() throws Exception { final int port = ThreadLocalRandom.current() .nextInt( dockerPortRange.lowerEndpoint(), dockerPortRange.upperEndpoint()); return temporaryPorts.tryAcquire("docker-probe", port); } }); probePort = allocatedPort.port(); } else { dockerPortRange = temporaryPorts.localPortRange("docker", 10); probePort = dockerPortRange().lowerEndpoint(); allocatedPort = null; } try { assertDockerReachable(probePort); } finally { if (allocatedPort != null) { allocatedPort.release(); } } } protected DockerClient getNewDockerClient() throws Exception { if (isNullOrEmpty(DOCKER_HOST.dockerCertPath())) { return new DefaultDockerClient(DOCKER_HOST.uri()); } else { final Path dockerCertPath = java.nio.file.Paths.get(DOCKER_HOST.dockerCertPath()); return new DefaultDockerClient(DOCKER_HOST.uri(), new DockerCertificates(dockerCertPath)); } } private void assertDockerReachable(final int probePort) throws Exception { try (final DockerClient docker = getNewDockerClient()) { // Pull our base images try { docker.inspectImage(BUSYBOX); } catch (ImageNotFoundException e) { docker.pull(BUSYBOX); } try { docker.inspectImage(ALPINE); } catch (ImageNotFoundException e) { docker.pull(ALPINE); } // Start a container with an exposed port final HostConfig hostConfig = HostConfig.builder() .portBindings( ImmutableMap.of("4711/tcp", singletonList(PortBinding.of("0.0.0.0", probePort)))) .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX) .cmd("nc", "-p", "4711", "-lle", "cat") .exposedPorts(ImmutableSet.of("4711/tcp")) .hostConfig(hostConfig) .build(); final ContainerCreation creation = docker.createContainer(config, testTag + "-probe"); final String containerId = creation.id(); docker.startContainer(containerId); // Wait for container to come up Polling.await( 5, SECONDS, new Callable<Object>() { @Override public Object call() throws Exception { final ContainerInfo info = docker.inspectContainer(containerId); return info.state().running() ? true : null; } }); log.info("Verifying that docker containers are reachable"); try { Polling.awaitUnchecked( 5, SECONDS, new Callable<Object>() { @Override public Object call() throws Exception { log.info("Probing: {}:{}", DOCKER_HOST.address(), probePort); try (final Socket ignored = new Socket(DOCKER_HOST.address(), probePort)) { return true; } catch (IOException e) { return false; } } }); } catch (TimeoutException e) { fail( "Please ensure that DOCKER_HOST is set to an address that where containers can " + "be reached. If docker is running in a local VM, DOCKER_HOST must be set to the " + "address of that VM. If docker can only be reached on a limited port range, " + "set the environment variable DOCKER_PORT_RANGE=start:end"); } docker.killContainer(containerId); } } protected ZooKeeperTestManager zooKeeperTestManager() { return new ZooKeeperTestingServerManager(); } @After public void baseTeardown() throws Exception { for (final HeliosClient client : clients) { client.close(); } clients.clear(); for (Service service : services) { try { service.stopAsync(); } catch (Exception e) { log.error("Uncaught exception", e); } } for (Service service : services) { try { service.awaitTerminated(); } catch (Exception e) { log.error("Service failed", e); } } services.clear(); // Clean up docker try (final DockerClient dockerClient = getNewDockerClient()) { final List<Container> containers = dockerClient.listContainers(); for (final Container container : containers) { for (final String name : container.names()) { if (name.contains(testTag)) { try { dockerClient.killContainer(container.id()); } catch (DockerException e) { e.printStackTrace(); } break; } } } } catch (Exception e) { log.error("Docker client exception", e); } if (zk != null) { zk.close(); } listThreads(); } private void listThreads() { final Set<Thread> threads = Thread.getAllStackTraces().keySet(); final Map<String, Thread> sorted = Maps.newTreeMap(); for (final Thread t : threads) { final ThreadGroup tg = t.getThreadGroup(); if (t.isAlive() && (tg == null || !tg.getName().equals("system"))) { sorted.put(t.getName(), t); } } log.info("= THREADS " + Strings.repeat("=", 70)); for (final Thread t : sorted.values()) { final ThreadGroup tg = t.getThreadGroup(); log.info( "{}: \"{}\" ({}{})", t.getId(), t.getName(), (tg == null ? "" : tg.getName() + " "), (t.isDaemon() ? "daemon" : "")); } log.info(Strings.repeat("=", 80)); } protected TemporaryPorts temporaryPorts() { return temporaryPorts; } protected ZooKeeperTestManager zk() { return zk; } protected String masterEndpoint() { return masterEndpoint; } protected String masterAdminEndpoint() { return masterAdminEndpoint; } protected String masterName() throws InterruptedException, ExecutionException { return TEST_MASTER; } protected HeliosClient defaultClient() { return client(TEST_USER, masterEndpoint()); } protected HeliosClient client(final String user, final String endpoint) { final HeliosClient client = HeliosClient.newBuilder() .setUser(user) .setEndpoints(singletonList(URI.create(endpoint))) .build(); clients.add(client); return client; } protected int masterPort() { return masterPort; } protected int masterAdminPort() { return masterAdminPort; } public Range<Integer> dockerPortRange() { return dockerPortRange; } protected String testHost() throws InterruptedException, ExecutionException { return TEST_HOST; } protected List<String> setupDefaultMaster(String... args) throws Exception { return setupDefaultMaster(0, args); } protected List<String> setupDefaultMaster(final int offset, String... args) throws Exception { // TODO (dano): Move this bootstrapping to something reusable final CuratorFramework curator = zk.curatorWithSuperAuth(); curator.newNamespaceAwareEnsurePath(Paths.configHosts()).ensure(curator.getZookeeperClient()); curator.newNamespaceAwareEnsurePath(Paths.configJobs()).ensure(curator.getZookeeperClient()); curator.newNamespaceAwareEnsurePath(Paths.configJobRefs()).ensure(curator.getZookeeperClient()); curator.newNamespaceAwareEnsurePath(Paths.statusHosts()).ensure(curator.getZookeeperClient()); curator.newNamespaceAwareEnsurePath(Paths.statusMasters()).ensure(curator.getZookeeperClient()); curator.newNamespaceAwareEnsurePath(Paths.historyJobs()).ensure(curator.getZookeeperClient()); curator .newNamespaceAwareEnsurePath(Paths.configId(zkClusterId)) .ensure(curator.getZookeeperClient()); final List<String> argsList = Lists.newArrayList( "-vvvv", "--no-log-setup", "--http", "http://0.0.0.0:" + (masterPort() + offset), "--admin", "http://0.0.0.0:" + (masterAdminPort() + offset), "--domain", "", "--zk", zk.connectString(), "--zk-enable-acls", "--zk-acl-agent-user", AGENT_USER, "--zk-acl-agent-digest", AGENT_DIGEST, "--zk-acl-master-user", MASTER_USER, "--zk-acl-master-password", MASTER_PASSWORD); final String name; if (asList(args).contains("--name")) { name = args[asList(args).indexOf("--name") + 1]; } else { name = TEST_MASTER + offset; argsList.addAll(asList("--name", TEST_MASTER)); } final String stateDir = masterStateDirs.resolve(name).toString(); argsList.addAll(asList("--state-dir", stateDir)); argsList.addAll(asList(args)); return argsList; } protected MasterMain startDefaultMaster(String... args) throws Exception { return startDefaultMaster(0, args); } protected MasterMain startDefaultMaster(Map<String, String> environmentVariables, String... args) throws Exception { return startDefaultMaster(0, environmentVariables, args); } protected MasterMain startDefaultMaster(final int offset, String... args) throws Exception { return startDefaultMaster(offset, ImmutableMap.<String, String>of(), args); } protected MasterMain startDefaultMaster( final int offset, final Map<String, String> environmentVariables, final String... args) throws Exception { final List<String> argsList = setupDefaultMaster(offset, args); if (argsList == null) { return null; } final MasterMain master = startMaster(environmentVariables, argsList.toArray(new String[argsList.size()])); waitForMasterToBeFullyUp(); return master; } protected Map<String, MasterMain> startDefaultMasters(final int numMasters, String... args) throws Exception { final Map<String, MasterMain> masters = Maps.newHashMap(); for (int i = 0; i < numMasters; i++) { final String name = TEST_MASTER + i; final List<String> argsList = Lists.newArrayList(args); argsList.addAll(asList("--name", name)); masters.put(name, startDefaultMaster(i, argsList.toArray(new String[argsList.size()]))); } return masters; } protected void waitForMasterToBeFullyUp() throws Exception { log.debug("waitForMasterToBeFullyUp: beginning wait loop"); Polling.await( WAIT_TIMEOUT_SECONDS, SECONDS, new Callable<Object>() { @Override public Object call() { try { // While MasterService will start listening for http requests on the main and admin // ports // as soon as it is started (without waiting for ZK to be available), the Healthcheck // registered for Zookeeper connectivity will cause the HealthcheckServlet to not // return // 200 OK until ZK is connected to (and even better, until *everything* is healthy). final HttpGet request = new HttpGet(masterAdminEndpoint + "/healthcheck"); try (CloseableHttpResponse response = httpClient.execute(request)) { final int status = response.getStatusLine().getStatusCode(); log.debug("waitForMasterToBeFullyUp: healthcheck endpoint returned {}", status); return status == HttpStatus.SC_OK; } } catch (Exception e) { return null; } } }); } protected void startDefaultMasterDontWaitForZK( final CuratorClientFactory curatorClientFactory, String... args) throws Exception { List<String> argsList = setupDefaultMaster(args); if (argsList == null) { return; } startMaster(curatorClientFactory, argsList.toArray(new String[argsList.size()])); } protected AgentMain startDefaultAgent(final String host, final String... args) throws Exception { final String stateDir = agentStateDirs.resolve(host).toString(); final List<String> argsList = Lists.newArrayList( "-vvvv", "--no-log-setup", "--no-http", "--name", host, "--docker=" + DOCKER_HOST, "--zk", zk.connectString(), "--zk-session-timeout", "100", "--zk-connection-timeout", "100", "--zk-enable-acls", "--zk-acl-master-user", MASTER_USER, "--zk-acl-master-digest", MASTER_DIGEST, "--zk-acl-agent-user", AGENT_USER, "--zk-acl-agent-password", AGENT_PASSWORD, "--state-dir", stateDir, "--domain", "", "--port-range=" + dockerPortRange.lowerEndpoint() + ":" + dockerPortRange.upperEndpoint()); argsList.addAll(asList(args)); return startAgent(argsList.toArray(new String[argsList.size()])); } protected MasterMain startMaster( final Map<String, String> environmentVariables, final String... args) throws Exception { final MasterMain main = new MasterMain(environmentVariables, args); main.startAsync().awaitRunning(); services.add(main); return main; } MasterMain startMaster(final CuratorClientFactory curatorClientFactory, final String... args) throws Exception { final MasterMain main = new MasterMain(curatorClientFactory, args); main.startAsync().awaitRunning(); services.add(main); return main; } protected AgentMain startAgent(final String... args) throws Exception { final AgentMain main = new AgentMain(args); main.startAsync().awaitRunning(); services.add(main); return main; } protected JobId createJob( final String name, final String version, final String image, final List<String> command) throws Exception { return createJob(name, version, image, command, EMPTY_ENV, EMPTY_PORTS, EMPTY_REGISTRATION); } protected JobId createJob( final String name, final String version, final String image, final List<String> command, final Date expires) throws Exception { return createJob( name, version, image, EMPTY_HOSTNAME, command, EMPTY_ENV, EMPTY_PORTS, EMPTY_REGISTRATION, EMPTY_GRACE_PERIOD, EMPTY_VOLUMES, expires); } protected JobId createJob( final String name, final String version, final String image, final List<String> command, final ImmutableMap<String, String> env) throws Exception { return createJob(name, version, image, command, env, EMPTY_PORTS, EMPTY_REGISTRATION); } protected JobId createJob( final String name, final String version, final String image, final List<String> command, final Map<String, String> env, final Map<String, PortMapping> ports) throws Exception { return createJob(name, version, image, command, env, ports, EMPTY_REGISTRATION); } protected JobId createJob( final String name, final String version, final String image, final List<String> command, final Map<String, String> env, final Map<String, PortMapping> ports, final Map<ServiceEndpoint, ServicePorts> registration) throws Exception { return createJob( name, version, image, command, env, ports, registration, EMPTY_GRACE_PERIOD, EMPTY_VOLUMES); } protected JobId createJob( final String name, final String version, final String image, final List<String> command, final Map<String, String> env, final Map<String, PortMapping> ports, final Map<ServiceEndpoint, ServicePorts> registration, final Integer gracePeriod, final Map<String, String> volumes) throws Exception { return createJob( name, version, image, EMPTY_HOSTNAME, command, env, ports, registration, gracePeriod, volumes, EMPTY_EXPIRES); } protected JobId createJob( final String name, final String version, final String image, final String hostname, final List<String> command, final Map<String, String> env, final Map<String, PortMapping> ports, final Map<ServiceEndpoint, ServicePorts> registration, final Integer gracePeriod, final Map<String, String> volumes, final Date expires) throws Exception { return createJob( Job.newBuilder() .setName(name) .setVersion(version) .setImage(image) .setHostname(hostname) .setCommand(command) .setEnv(env) .setPorts(ports) .setRegistration(registration) .setGracePeriod(gracePeriod) .setVolumes(volumes) .setExpires(expires) .build()); } protected JobId createJob(final Job job) throws Exception { final String createOutput = createJobRawOutput(job); final String jobId = WHITESPACE.trimFrom(createOutput); return JobId.fromString(jobId); } protected String createJobRawOutput(final Job job) throws Exception { final String name = job.getId().getName(); checkArgument(name.contains(testTag), "Job name must contain testTag to enable cleanup"); final String serializedConfig = Json.asNormalizedString(job); final File configFile = temporaryFolder.newFile(); Files.write(serializedConfig, configFile, Charsets.UTF_8); final List<String> args = ImmutableList.of("-q", "-f", configFile.getAbsolutePath()); return cli("create", args); } protected void deployJob(final JobId jobId, final String host) throws Exception { deployJob(jobId, host, null); } protected void deployJob(final JobId jobId, final String host, final String token) throws Exception { final List<String> deployArgs = Lists.newArrayList(jobId.toString(), host); if (token != null) { deployArgs.addAll(ImmutableList.of("--token", token)); } final String deployOutput = cli("deploy", deployArgs); assertThat(deployOutput, containsString(host + ": done")); final String output = cli("status", "--host", host, "--json"); final Map<JobId, JobStatus> statuses = Json.readUnchecked(output, new TypeReference<Map<JobId, JobStatus>>() {}); assertTrue(statuses.keySet().contains(jobId)); } protected void undeployJob(final JobId jobId, final String host) throws Exception { final String undeployOutput = cli("undeploy", jobId.toString(), host); assertThat(undeployOutput, containsString(host + ": done")); final String output = cli("status", "--host", host, "--json"); final Map<JobId, JobStatus> statuses = Json.readUnchecked(output, new TypeReference<Map<JobId, JobStatus>>() {}); final JobStatus status = statuses.get(jobId); assertTrue(status == null || status.getDeployments().get(host) == null); } protected String startJob(final JobId jobId, final String host) throws Exception { return cli("start", jobId.toString(), host); } protected String stopJob(final JobId jobId, final String host) throws Exception { return cli("stop", jobId.toString(), host); } protected String deregisterHost(final String host) throws Exception { return cli("deregister", host, "--yes"); } protected String cli(final String command, final Object... args) throws Exception { return cli(command, flatten(args)); } protected String cli(final String command, final String... args) throws Exception { return cli(command, asList(args)); } protected String cli(final String command, final List<String> args) throws Exception { final List<String> commands = asList(command, "-z", masterEndpoint(), "--no-log-setup"); final List<String> allArgs = newArrayList(concat(commands, args)); return main(allArgs).toString(); } protected <T> T cliJson(final Class<T> klass, final String command, final String... args) throws Exception { return cliJson(klass, command, asList(args)); } protected <T> T cliJson(final Class<T> klass, final String command, final List<String> args) throws Exception { final List<String> args0 = newArrayList("--json"); args0.addAll(args); return Json.read(cli(command, args0), klass); } protected ByteArrayOutputStream main(final String... args) throws Exception { final ByteArrayOutputStream out = new ByteArrayOutputStream(); final ByteArrayOutputStream err = new ByteArrayOutputStream(); final CliMain main = new CliMain(new PrintStream(out), new PrintStream(err), args); main.run(); return out; } protected ByteArrayOutputStream main(final Collection<String> args) throws Exception { return main(args.toArray(new String[args.size()])); } protected void awaitHostRegistered(final String name, final long timeout, final TimeUnit timeUnit) throws Exception { Polling.await( timeout, timeUnit, new Callable<Object>() { @Override public Object call() throws Exception { final String output = cli("hosts", "-q"); return output.contains(name) ? true : null; } }); } protected HostStatus awaitHostStatus( final String name, final HostStatus.Status status, final int timeout, final TimeUnit timeUnit) throws Exception { return Polling.await( timeout, timeUnit, new Callable<HostStatus>() { @Override public HostStatus call() throws Exception { final String output = cli("hosts", name, "--json"); final Map<String, HostStatus> statuses; try { statuses = Json.read(output, new TypeReference<Map<String, HostStatus>>() {}); } catch (IOException e) { return null; } final HostStatus hostStatus = statuses.get(name); if (hostStatus == null) { return null; } return (hostStatus.getStatus() == status) ? hostStatus : null; } }); } protected TaskStatus awaitJobState( final HeliosClient client, final String host, final JobId jobId, final TaskStatus.State state, final int timeout, final TimeUnit timeunit) throws Exception { return Polling.await( timeout, timeunit, new Callable<TaskStatus>() { @Override public TaskStatus call() throws Exception { final HostStatus hostStatus = getOrNull(client.hostStatus(host)); if (hostStatus == null) { return null; } final TaskStatus taskStatus = hostStatus.getStatuses().get(jobId); return (taskStatus != null && taskStatus.getState() == state) ? taskStatus : null; } }); } protected TaskStatus awaitJobThrottle( final HeliosClient client, final String host, final JobId jobId, final ThrottleState throttled, final int timeout, final TimeUnit timeunit) throws Exception { return Polling.await( timeout, timeunit, new Callable<TaskStatus>() { @Override public TaskStatus call() throws Exception { final HostStatus hostStatus = getOrNull(client.hostStatus(host)); if (hostStatus == null) { return null; } final TaskStatus taskStatus = hostStatus.getStatuses().get(jobId); return (taskStatus != null && taskStatus.getThrottled() == throttled) ? taskStatus : null; } }); } protected void awaitHostRegistered( final HeliosClient client, final String host, final int timeout, final TimeUnit timeUnit) throws Exception { Polling.await( timeout, timeUnit, new Callable<HostStatus>() { @Override public HostStatus call() throws Exception { return getOrNull(client.hostStatus(host)); } }); } protected HostStatus awaitHostStatus( final HeliosClient client, final String host, final HostStatus.Status status, final int timeout, final TimeUnit timeUnit) throws Exception { return Polling.await( timeout, timeUnit, new Callable<HostStatus>() { @Override public HostStatus call() throws Exception { final HostStatus hostStatus = getOrNull(client.hostStatus(host)); if (hostStatus == null) { return null; } return (hostStatus.getStatus() == status) ? hostStatus : null; } }); } protected HostStatus awaitHostStatusWithLabels( final HeliosClient client, final String host, final HostStatus.Status status, final int timeout, final TimeUnit timeUnit) throws Exception { return Polling.await( timeout, timeUnit, new Callable<HostStatus>() { @Override public HostStatus call() throws Exception { final HostStatus hostStatus = getOrNull(client.hostStatus(host)); if (hostStatus == null || hostStatus.getLabels().size() == 0) { return null; } return (hostStatus.getStatus() == status) ? hostStatus : null; } }); } protected HostStatus awaitHostStatusWithHostInfo( final HeliosClient client, final String host, final HostStatus.Status status, final int timeout, final TimeUnit timeUnit) throws Exception { return Polling.await( timeout, timeUnit, new Callable<HostStatus>() { @Override public HostStatus call() throws Exception { final HostStatus hostStatus = getOrNull(client.hostStatus(host)); if (hostStatus == null || hostStatus.getHostInfo() == null) { return null; } return (hostStatus.getStatus() == status) ? hostStatus : null; } }); } protected TaskStatus awaitTaskState( final JobId jobId, final String host, final TaskStatus.State state) throws Exception { return Polling.await( LONG_WAIT_SECONDS, SECONDS, new Callable<TaskStatus>() { @Override public TaskStatus call() throws Exception { final String output = cli("status", "--json", "--job", jobId.toString()); final Map<JobId, JobStatus> statusMap; try { statusMap = Json.read(output, new TypeReference<Map<JobId, JobStatus>>() {}); } catch (IOException e) { return null; } final JobStatus status = statusMap.get(jobId); if (status == null) { return null; } final TaskStatus taskStatus = status.getTaskStatuses().get(host); if (taskStatus == null) { return null; } if (taskStatus.getState() != state) { return null; } return taskStatus; } }); } protected void awaitTaskGone( final HeliosClient client, final String host, final JobId jobId, final long timeout, final TimeUnit timeunit) throws Exception { Polling.await( timeout, timeunit, new Callable<Boolean>() { @Override public Boolean call() throws Exception { final HostStatus hostStatus = getOrNull(client.hostStatus(host)); final TaskStatus taskStatus = hostStatus.getStatuses().get(jobId); final Deployment deployment = hostStatus.getJobs().get(jobId); return taskStatus == null && deployment == null ? true : null; } }); } protected DeploymentGroupStatus awaitDeploymentGroupStatus( final HeliosClient client, final String name, final DeploymentGroupStatus.State state) throws Exception { return Polling.await( LONG_WAIT_SECONDS, SECONDS, new Callable<DeploymentGroupStatus>() { @Override public DeploymentGroupStatus call() throws Exception { final DeploymentGroupStatusResponse response = getOrNull(client.deploymentGroupStatus(name)); if (response != null) { final DeploymentGroupStatus status = response.getDeploymentGroupStatus(); if (status.getState().equals(state)) { return status; } else if (status.getState().equals(DeploymentGroupStatus.State.FAILED)) { assertEquals(state, status.getState()); } } return null; } }); } protected <T> T getOrNull(final ListenableFuture<T> future) throws ExecutionException, InterruptedException { return Futures.withFallback( future, new FutureFallback<T>() { @Override public ListenableFuture<T> create(@NotNull final Throwable t) throws Exception { return Futures.immediateFuture(null); } }) .get(); } protected String readLogFully(final ClientResponse logs) throws IOException { final LogReader logReader = new LogReader(logs.getEntityInputStream()); StringBuilder stringBuilder = new StringBuilder(); LogMessage logMessage; while ((logMessage = logReader.nextMessage()) != null) { stringBuilder.append(UTF_8.decode(logMessage.content())); } logReader.close(); return stringBuilder.toString(); } protected static void removeContainer(final DockerClient dockerClient, final String containerId) throws Exception { // Work around docker sometimes failing to remove a container directly after killing it Polling.await( 1, MINUTES, new Callable<Object>() { @Override public Object call() throws Exception { try { dockerClient.killContainer(containerId); } catch (DockerRequestException e) { if (e.message().contains("is not running")) { // Container already isn't running. So we continue. } else { throw e; } } try { dockerClient.removeContainer(containerId); return true; } catch (ContainerNotFoundException e) { // We're done here return true; } catch (DockerException e) { if ((e instanceof DockerRequestException) && ((DockerRequestException) e) .message() .contains("Driver btrfs failed to remove root filesystem")) { // Workaround btrfs issue where removing containers throws an exception, // but succeeds anyway. return true; } else { return null; } } } }); } protected List<Container> listContainers(final DockerClient dockerClient, final String needle) throws DockerException, InterruptedException { final List<Container> containers = dockerClient.listContainers(); final List<Container> matches = Lists.newArrayList(); for (final Container container : containers) { if (container.names() != null) { for (final String name : container.names()) { if (name.contains(needle)) { matches.add(container); break; } } } } return matches; } protected List<String> flatten(final Object... values) { final Iterable<Object> valuesList = asList(values); return flatten(valuesList); } protected List<String> flatten(final Iterable<?> values) { final List<String> list = new ArrayList<>(); for (Object value : values) { if (value instanceof Iterable) { list.addAll(flatten((Iterable<?>) value)); } else if (value.getClass() == String[].class) { list.addAll(asList((String[]) value)); } else if (value instanceof String) { list.add((String) value); } else { throw new IllegalArgumentException(); } } return list; } protected void assertJobsEqual(final Map<JobId, Job> expected, final Map<JobId, Job> actual) { assertEquals(expected.size(), actual.size()); for (final Map.Entry<JobId, Job> entry : actual.entrySet()) { assertJobEquals(expected.get(entry.getKey()), entry.getValue()); } } protected void assertJobEquals(final Job expected, final Job actual) { final Builder expectedBuilder = expected.toBuilder(); // hack to make sure that any environment variables that were folded into the created job // because of environment variables set at runtime on the test-running-agent are removed // from the actual when we assert the equality below final Builder actualBuilder = actual.toBuilder(); final Map<String, String> metadata = Maps.newHashMap(actual.getMetadata()); for (Map.Entry<String, String> entry : JobCreateCommand.DEFAULT_METADATA_ENVVARS.entrySet()) { final String envVar = entry.getKey(); final String metadataKey = entry.getValue(); final String envValue = System.getenv(envVar); if (envValue != null && actual.getMetadata().containsKey(metadataKey) && actual.getMetadata().get(metadataKey).equals(envValue)) { metadata.remove(metadataKey); } } actualBuilder.setMetadata(metadata); // Remove created timestamp set by master actualBuilder.setCreated(null); // copy the hash expectedBuilder.setHash(actualBuilder.build().getId().getHash()); assertEquals(expectedBuilder.build(), actualBuilder.build()); } protected static String randomHexString() { return toHexString(ThreadLocalRandom.current().nextInt()); } protected void resetAgentStateDir() throws IOException { agentStateDirs = temporaryFolder.newFolder(UUID.randomUUID().toString()).toPath(); } }