/*-
* -\-\-
* Helios Testing Library
* --
* Copyright (C) 2016 Spotify AB
* --
* 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 com.spotify.helios.testing;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.spotify.docker.client.DockerClient;
import com.spotify.docker.client.DockerHost;
import com.spotify.docker.client.LogStream;
import com.spotify.docker.client.exceptions.ImageNotFoundException;
import com.spotify.docker.client.messages.ContainerConfig;
import com.spotify.docker.client.messages.ContainerCreation;
import com.spotify.docker.client.messages.ContainerExit;
import com.spotify.docker.client.messages.ContainerInfo;
import com.spotify.docker.client.messages.ImageInfo;
import com.spotify.docker.client.messages.Info;
import com.spotify.docker.client.messages.NetworkSettings;
import com.spotify.docker.client.messages.PortBinding;
import com.spotify.helios.client.HeliosClient;
import com.spotify.helios.common.descriptors.Deployment;
import com.spotify.helios.common.descriptors.Goal;
import com.spotify.helios.common.descriptors.HostStatus;
import com.spotify.helios.common.descriptors.HostStatus.Status;
import com.spotify.helios.common.descriptors.Job;
import com.spotify.helios.common.descriptors.JobId;
import com.spotify.helios.common.descriptors.TaskStatus;
import com.spotify.helios.common.protocol.JobUndeployResponse;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Matchers;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
public class HeliosSoloDeploymentTest {
private static final String CONTAINER_ID = "abc123";
private static final String HOST1 = "host1";
private static final String HOST2 = "host2";
private static final Job JOB1 = Job.newBuilder()
.setCommand(ImmutableList.of("BOGUS1"))
.setImage("IMAGE")
.setName("NAME")
.setVersion("VERSION")
.build();
private static final Job JOB2 = Job.newBuilder()
.setCommand(ImmutableList.of("BOGUS2"))
.setImage("IMAGE")
.setName("NAME")
.setVersion("VERSION")
.build();
private static final JobId JOB_ID1 = JOB1.getId();
private static final JobId JOB_ID2 = JOB2.getId();
private static final TaskStatus TASK_STATUS1 = TaskStatus.newBuilder()
.setJob(JOB1)
.setGoal(Goal.START)
.setState(TaskStatus.State.RUNNING)
.setContainerId(CONTAINER_ID)
.build();
private static final TaskStatus TASK_STATUS2 = TaskStatus.newBuilder()
.setJob(JOB2)
.setGoal(Goal.START)
.setState(TaskStatus.State.RUNNING)
.build();
private DockerClient dockerClient;
private HeliosClient heliosClient;
private ArgumentCaptor<ContainerConfig> containerConfig;
@Before
public void setup() throws Exception {
this.dockerClient = mock(DockerClient.class);
this.heliosClient = mock(HeliosClient.class);
// the anonymous classes to override a method are to workaround the docker-client "messages"
// having no mutators, fun
final Info info = mock(Info.class);
when(info.operatingSystem()).thenReturn("foo");
when(this.dockerClient.info()).thenReturn(info);
// mock the call to dockerClient.createContainer so we can test the arguments passed to it
this.containerConfig = ArgumentCaptor.forClass(ContainerConfig.class);
final ContainerCreation creation = mock(ContainerCreation.class);
when(creation.id()).thenReturn(CONTAINER_ID);
when(this.dockerClient.createContainer(
this.containerConfig.capture(), anyString())).thenReturn(creation);
// we have to mock out several other calls to get the HeliosSoloDeployment ctor
// to return non-exceptionally. the anonymous classes to override a method are to workaround
// the docker-client "messages" having no mutators, fun
when(this.dockerClient.info()).thenReturn(info);
final PortBinding binding = PortBinding.of("192.168.1.1", 5801);
final ImmutableMap<String, List<PortBinding>> ports =
ImmutableMap.<String, List<PortBinding>>of("5801/tcp", ImmutableList.of(binding));
final ContainerInfo containerInfo = mock(ContainerInfo.class);
final NetworkSettings networkSettings = mock(NetworkSettings.class);
when(networkSettings.gateway()).thenReturn("a-gate-way");
when(networkSettings.ports()).thenReturn(ports);
when(containerInfo.networkSettings()).thenReturn(networkSettings);
when(this.dockerClient.inspectContainer(CONTAINER_ID)).thenReturn(containerInfo);
when(this.dockerClient.waitContainer(CONTAINER_ID)).thenReturn(ContainerExit.create(0));
}
private HeliosSoloDeployment buildHeliosSoloDeployment() {
return buildHeliosSoloDeployment(DockerHost.from("tcp://localhost:2375", ""));
}
private HeliosSoloDeployment buildHeliosSoloDeployment(HeliosSoloDeployment.Builder builder) {
return buildHeliosSoloDeployment(builder, DockerHost.from("tcp://localhost:2375", ""));
}
private HeliosSoloDeployment buildHeliosSoloDeployment(DockerHost dockerHost) {
return buildHeliosSoloDeployment(HeliosSoloDeployment.builder(), dockerHost);
}
private HeliosSoloDeployment buildHeliosSoloDeployment(final HeliosSoloDeployment.Builder builder,
final DockerHost dockerHost) {
return builder.dockerClient(dockerClient)
.dockerHost(dockerHost)
.heliosClient(heliosClient)
.build();
}
@Test
public void testDockerHostContainsLocalhost() throws Exception {
buildHeliosSoloDeployment();
boolean foundSolo = false;
for (final ContainerConfig cc : containerConfig.getAllValues()) {
if (cc.image().contains("helios-solo")) {
assertThat(cc.hostConfig().binds(), hasItem("/var/run/docker.sock:/var/run/docker.sock"));
foundSolo = true;
}
}
assertTrue("Could not find helios-solo container creation", foundSolo);
}
@Test
public void testConfig() throws Exception {
final String image = "helios-test";
final String ns = "namespace";
final String env = "stuff";
final Config config = ConfigFactory.empty()
.withValue("helios.solo.profile", ConfigValueFactory.fromAnyRef("test"))
.withValue("helios.solo.profiles.test.image", ConfigValueFactory.fromAnyRef(image))
.withValue("helios.solo.profiles.test.namespace", ConfigValueFactory.fromAnyRef(ns))
.withValue("helios.solo.profiles.test.env.TEST", ConfigValueFactory.fromAnyRef(env));
final HeliosSoloDeployment deployment = buildHeliosSoloDeployment(
new HeliosSoloDeployment.Builder(null, config));
assertEquals(ns + ".solo.local", deployment.agentName());
boolean foundSolo = false;
for (final ContainerConfig cc : containerConfig.getAllValues()) {
if (cc.image().contains(image)) {
foundSolo = true;
assertThat(cc.env(), hasItem("TEST=" + env));
assertThat(cc.env(), hasItem("HELIOS_NAME=" + ns + ".solo.local"));
}
}
assertTrue("Could not find helios-solo container creation", foundSolo);
}
@Test
public void testDoesNotPullPresentProbeImage() throws Exception {
when(this.dockerClient.inspectImage(HeliosSoloDeployment.PROBE_IMAGE))
.thenReturn(mock(ImageInfo.class));
buildHeliosSoloDeployment();
verify(this.dockerClient, never()).pull(HeliosSoloDeployment.PROBE_IMAGE);
}
@Test
public void testDoesPullAbsentProbeImage() throws Exception {
when(this.dockerClient.inspectImage(HeliosSoloDeployment.PROBE_IMAGE))
.thenThrow(new ImageNotFoundException(HeliosSoloDeployment.PROBE_IMAGE));
buildHeliosSoloDeployment();
verify(this.dockerClient).pull(HeliosSoloDeployment.PROBE_IMAGE);
}
@Test
public void testUndeployLeftoverJobs() throws Exception {
final HeliosSoloDeployment solo = buildHeliosSoloDeployment();
final ListenableFuture<List<String>> hostsFuture = Futures.<List<String>>immediateFuture(
ImmutableList.of(HOST1, HOST2));
when(heliosClient.listHosts()).thenReturn(hostsFuture);
// These futures represent HostStatuses when the job is still deployed
final ListenableFuture<HostStatus> statusFuture11 = Futures.immediateFuture(
HostStatus.newBuilder()
.setStatus(Status.UP)
.setStatuses(ImmutableMap.of(JOB_ID1, TASK_STATUS1))
.setJobs(ImmutableMap.of(JOB_ID1, Deployment.of(JOB_ID1, Goal.START)))
.build());
final ListenableFuture<HostStatus> statusFuture21 = Futures.immediateFuture(
HostStatus.newBuilder()
.setStatus(Status.UP)
.setStatuses(ImmutableMap.of(JOB_ID2, TASK_STATUS2))
.setJobs(ImmutableMap.of(JOB_ID2, Deployment.of(JOB_ID2, Goal.START)))
.build());
// These futures represent HostStatuses when the job is undeployed
final ListenableFuture<HostStatus> statusFuture12 = Futures.immediateFuture(
HostStatus.newBuilder()
.setStatus(Status.UP)
.setStatuses(Collections.<JobId, TaskStatus>emptyMap())
.setJobs(ImmutableMap.of(JOB_ID1, Deployment.of(JOB_ID1, Goal.START)))
.build());
final ListenableFuture<HostStatus> statusFuture22 = Futures.immediateFuture(
HostStatus.newBuilder()
.setStatus(Status.UP)
.setStatuses(Collections.<JobId, TaskStatus>emptyMap())
.setJobs(ImmutableMap.of(JOB_ID2, Deployment.of(JOB_ID2, Goal.START)))
.build());
//noinspection unchecked
when(heliosClient.hostStatus(HOST1)).thenReturn(statusFuture11);
//noinspection unchecked
when(heliosClient.hostStatus(HOST2)).thenReturn(statusFuture21);
final ListenableFuture<JobUndeployResponse> undeployFuture1 = Futures.immediateFuture(
new JobUndeployResponse(JobUndeployResponse.Status.OK, HOST1, JOB_ID1));
final ListenableFuture<JobUndeployResponse> undeployFuture2 = Futures.immediateFuture(
new JobUndeployResponse(JobUndeployResponse.Status.OK, HOST2, JOB_ID2));
// when undeploy is called, respond correctly & patch the mock to return
// the undeployed HostStatus
when(heliosClient.undeploy(JOB_ID1, HOST1)).thenAnswer(
new Answer<ListenableFuture<JobUndeployResponse>>() {
@Override
public ListenableFuture<JobUndeployResponse> answer(final InvocationOnMock invocation)
throws Throwable {
when(heliosClient.hostStatus(HOST1)).thenReturn(statusFuture12);
return undeployFuture1;
}
});
when(heliosClient.undeploy(JOB_ID2, HOST2)).thenAnswer(
new Answer<ListenableFuture<JobUndeployResponse>>() {
@Override
public ListenableFuture<JobUndeployResponse> answer(final InvocationOnMock invocation)
throws Throwable {
when(heliosClient.hostStatus(HOST1)).thenReturn(statusFuture22);
return undeployFuture2;
}
});
solo.undeployLeftoverJobs();
verify(heliosClient).undeploy(JOB_ID1, HOST1);
verify(heliosClient).undeploy(JOB_ID2, HOST2);
}
@Test
public void testUndeployLeftoverJobs_noLeftoverJobs() throws Exception {
final HeliosSoloDeployment solo = buildHeliosSoloDeployment();
final ListenableFuture<Map<JobId, Job>> jobsFuture = Futures.immediateFuture(
Collections.<JobId, Job>emptyMap());
when(heliosClient.jobs()).thenReturn(jobsFuture);
solo.undeployLeftoverJobs();
// There should be no more calls to any HeliosClient methods.
verify(heliosClient, never()).jobStatus(Matchers.any(JobId.class));
}
@Test
public void testLogService() throws Exception {
final InMemoryLogStreamFollower logStreamProvider = InMemoryLogStreamFollower.create();
final HeliosSoloLogService logService = new HeliosSoloLogService(heliosClient,
dockerClient,
logStreamProvider);
final ListenableFuture<List<String>> hostsFuture = Futures.<List<String>>immediateFuture(
ImmutableList.of(HOST1));
when(heliosClient.listHosts()).thenReturn(hostsFuture);
final ListenableFuture<HostStatus> statusFuture = Futures.immediateFuture(
HostStatus.newBuilder()
.setStatus(Status.UP)
.setStatuses(ImmutableMap.of(JOB_ID1, TASK_STATUS1))
.setJobs(ImmutableMap.of(JOB_ID1, Deployment.of(JOB_ID1, Goal.START)))
.build());
when(heliosClient.hostStatus(HOST1)).thenReturn(statusFuture);
when(dockerClient.logs(anyString(), Matchers.<DockerClient.LogsParam>anyVararg()))
.thenReturn(mock(LogStream.class));
logService.runOneIteration();
verify(dockerClient, timeout(5000)).logs(eq(CONTAINER_ID),
Matchers.<DockerClient.LogsParam>anyVararg());
}
}