/*-
* -\-\-
* Helios Services
* --
* 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;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.common.collect.ImmutableList;
import com.spotify.helios.common.HeliosException;
import com.spotify.helios.common.descriptors.Deployment;
import com.spotify.helios.common.descriptors.DeploymentGroup;
import com.spotify.helios.common.descriptors.Goal;
import com.spotify.helios.common.descriptors.HostSelector;
import com.spotify.helios.common.descriptors.Job;
import com.spotify.helios.common.descriptors.JobId;
import com.spotify.helios.common.descriptors.RolloutOptions;
import com.spotify.helios.master.DeploymentGroupDoesNotExistException;
import com.spotify.helios.master.DeploymentGroupExistsException;
import com.spotify.helios.master.HostNotFoundException;
import com.spotify.helios.master.JobDoesNotExistException;
import com.spotify.helios.master.JobNotDeployedException;
import com.spotify.helios.master.JobStillDeployedException;
import com.spotify.helios.master.ZooKeeperMasterModel;
import com.spotify.helios.servicescommon.EventSender;
import com.spotify.helios.servicescommon.coordination.DefaultZooKeeperClient;
import com.spotify.helios.servicescommon.coordination.Paths;
import com.spotify.helios.servicescommon.coordination.ZooKeeperClient;
import com.spotify.helios.servicescommon.coordination.ZooKeeperClientProvider;
import com.spotify.helios.servicescommon.coordination.ZooKeeperModelReporter;
import java.util.List;
import java.util.Map;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
public class ZooKeeperMasterModelIntegrationTest {
private static final String IMAGE = "IMAGE";
private static final String COMMAND = "COMMAND";
private static final String JOB_NAME = "JOB_NAME";
private static final String HOST = "HOST";
private static final Job JOB = Job.newBuilder()
.setCommand(ImmutableList.of(COMMAND))
.setImage(IMAGE)
.setName(JOB_NAME)
.setVersion("VERSION")
.build();
private static final JobId JOB_ID = JOB.getId();
private static final String DEPLOYMENT_GROUP_NAME = "my_group";
private static final DeploymentGroup DEPLOYMENT_GROUP = DeploymentGroup.newBuilder()
.setName(DEPLOYMENT_GROUP_NAME)
.setHostSelectors(ImmutableList.of(HostSelector.parse("role=foo")))
.setJobId(JOB_ID)
.setRolloutOptions(RolloutOptions.newBuilder().build())
.build();
private ZooKeeperMasterModel model;
private final EventSender eventSender = mock(EventSender.class);
private final String deploymentGroupEventTopic = "deploymentGroupEventTopic";
private final ZooKeeperTestManager zk = new ZooKeeperTestingServerManager();
@Rule
public ExpectedException exception = ExpectedException.none();
@Before
public void setup() throws Exception {
final RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
final CuratorFramework curator = CuratorFrameworkFactory.newClient(zk.connectString(),
retryPolicy);
curator.start();
final ZooKeeperClient client = new DefaultZooKeeperClient(curator);
// TODO (dano): this bootstrapping is essentially duplicated from MasterService,
// should be moved into ZooKeeperMasterModel?
client.ensurePath(Paths.configHosts());
client.ensurePath(Paths.configJobs());
client.ensurePath(Paths.configJobRefs());
client.ensurePath(Paths.statusHosts());
client.ensurePath(Paths.statusMasters());
client.ensurePath(Paths.historyJobs());
final ZooKeeperClientProvider zkProvider =
new ZooKeeperClientProvider(client, ZooKeeperModelReporter.noop());
final List<EventSender> eventSenders = ImmutableList.of(eventSender);
model = new ZooKeeperMasterModel(zkProvider, getClass().getName(), eventSenders,
deploymentGroupEventTopic);
}
@Test
public void testHostListing() throws Exception {
final String secondHost = "SECOND";
assertThat(model.listHosts(), empty());
model.registerHost(HOST, "foo");
assertThat(model.listHosts(), contains(HOST));
model.registerHost(secondHost, "bar");
assertThat(model.listHosts(), contains(HOST, secondHost));
model.deregisterHost(HOST);
assertThat(model.listHosts(), contains(secondHost));
}
@Test
public void testHostListingWithNamePatternFilter() throws Exception {
// sanity check that no hosts exist
assertThat(model.listHosts(), empty());
final String hostname = "host1";
model.registerHost(hostname, "foo");
for (int i = 1; i <= hostname.length(); i++) {
assertThat(model.listHosts(hostname.substring(0, i)), contains(hostname));
}
// negative match
assertThat(model.listHosts("host2"), is(empty()));
final String secondHost = "host2";
model.registerHost(secondHost, "bar");
assertThat(model.listHosts("host"), contains(hostname, secondHost));
model.deregisterHost(hostname);
assertThat(model.listHosts(secondHost), contains(secondHost));
}
@Test
public void testJobCreation() throws Exception {
assertThat(model.getJobs().entrySet(), empty());
model.addJob(JOB);
assertEquals(model.getJobs().get(JOB_ID), JOB);
assertEquals(model.getJob(JOB_ID), JOB);
final Job secondJob = Job.newBuilder()
.setCommand(ImmutableList.of(COMMAND))
.setImage(IMAGE)
.setName(JOB_NAME)
.setVersion("SECOND")
.build();
model.addJob(secondJob);
assertEquals(model.getJob(secondJob.getId()), secondJob);
assertEquals(2, model.getJobs().size());
}
@Test
public void testJobRemove() throws Exception {
model.addJob(JOB);
model.registerHost(HOST, "foo");
model.deployJob(HOST,
Deployment.newBuilder().setGoal(Goal.START).setJobId(JOB_ID).build());
try {
model.removeJob(JOB_ID);
fail("should have thrown an exception");
} catch (JobStillDeployedException e) {
assertTrue(true);
}
model.undeployJob(HOST, JOB_ID);
assertNotNull(model.getJobs().get(JOB_ID));
model.removeJob(JOB_ID); // should succeed
assertNull(model.getJobs().get(JOB_ID));
}
@Test
public void testDeploy() throws Exception {
try {
model.deployJob(HOST,
Deployment.newBuilder()
.setGoal(Goal.START)
.setJobId(JOB_ID)
.build());
fail("should throw");
} catch (JobDoesNotExistException | HostNotFoundException e) {
assertTrue(true);
}
model.addJob(JOB);
try {
model.deployJob(HOST,
Deployment.newBuilder().setGoal(Goal.START).setJobId(JOB_ID).build());
fail("should throw");
} catch (HostNotFoundException e) {
assertTrue(true);
}
model.registerHost(HOST, "foo");
model.deployJob(HOST,
Deployment.newBuilder().setGoal(Goal.START).setJobId(JOB_ID).build());
model.undeployJob(HOST, JOB_ID);
model.removeJob(JOB_ID);
try {
model.deployJob(HOST,
Deployment.newBuilder().setGoal(Goal.START).setJobId(JOB_ID).build());
fail("should throw");
} catch (JobDoesNotExistException e) {
assertTrue(true);
}
}
@Test
public void testHostRegistration() throws Exception {
model.registerHost(HOST, "foo");
final List<String> hosts1 = model.listHosts();
assertThat(hosts1, hasItem(HOST));
model.deregisterHost(HOST);
final List<String> hosts2 = model.listHosts();
assertEquals(0, hosts2.size());
}
@Test
public void testUpdateDeploy() throws Exception {
try {
stopJob(model, JOB);
fail("should have thrown JobNotDeployedException");
} catch (JobNotDeployedException e) {
assertTrue(true);
} catch (Exception e) {
fail("Should have thrown an JobNotDeployedException, got " + e.getClass());
}
model.addJob(JOB);
try {
stopJob(model, JOB);
fail("should have thrown exception");
} catch (HostNotFoundException e) {
assertTrue(true);
} catch (Exception e) {
fail("Should have thrown an HostNotFoundException");
}
model.registerHost(HOST, "foo");
final List<String> hosts = model.listHosts();
assertThat(hosts, hasItem(HOST));
try {
stopJob(model, JOB);
fail("should have thrown exception");
} catch (JobNotDeployedException e) {
assertTrue(true);
} catch (Exception e) {
fail("Should have thrown an JobNotDeployedException");
}
model.deployJob(HOST, Deployment.newBuilder()
.setGoal(Goal.START)
.setJobId(JOB.getId())
.build());
final Map<JobId, Job> jobsOnHost = model.getJobs();
assertEquals(1, jobsOnHost.size());
final Job descriptor = jobsOnHost.get(JOB.getId());
assertEquals(JOB, descriptor);
stopJob(model, JOB); // should succeed this time!
final Deployment jobCfg = model.getDeployment(HOST, JOB.getId());
assertEquals(Goal.STOP, jobCfg.getGoal());
}
private void stopJob(ZooKeeperMasterModel model, Job job) throws HeliosException {
model.updateDeployment(HOST, Deployment.newBuilder()
.setGoal(Goal.STOP)
.setJobId(job.getId())
.build());
}
@Test
public void testAddDeploymentGroup() throws Exception {
model.addDeploymentGroup(DEPLOYMENT_GROUP);
assertEquals(DEPLOYMENT_GROUP, model.getDeploymentGroup(DEPLOYMENT_GROUP_NAME));
}
@Test
public void testAddExistingDeploymentGroup() throws Exception {
model.addDeploymentGroup(DEPLOYMENT_GROUP);
exception.expect(DeploymentGroupExistsException.class);
model.addDeploymentGroup(DEPLOYMENT_GROUP);
}
@Test
public void testRemoveDeploymentGroup() throws Exception {
model.addDeploymentGroup(DEPLOYMENT_GROUP);
model.removeDeploymentGroup(DEPLOYMENT_GROUP_NAME);
exception.expect(DeploymentGroupDoesNotExistException.class);
model.getDeploymentGroup(DEPLOYMENT_GROUP_NAME);
}
@Test
public void testRemoveNonExistingDeploymentGroup() throws Exception {
model.removeDeploymentGroup(DEPLOYMENT_GROUP_NAME);
}
@Test
public void testUpdateDeploymentGroupHostsSendsEvent() throws Exception {
model.addDeploymentGroup(DEPLOYMENT_GROUP);
model.updateDeploymentGroupHosts(DEPLOYMENT_GROUP_NAME, ImmutableList.of(HOST));
verify(eventSender, times(2)).send(eq(deploymentGroupEventTopic), any(byte[].class));
verifyNoMoreInteractions(eventSender);
}
@Test
public void testRollingUpdateSendsEvent() throws Exception {
model.addDeploymentGroup(DEPLOYMENT_GROUP);
model.addJob(JOB);
model.rollingUpdate(DEPLOYMENT_GROUP, JOB_ID, RolloutOptions.newBuilder().build());
verify(eventSender, times(2)).send(eq(deploymentGroupEventTopic), any(byte[].class));
verifyNoMoreInteractions(eventSender);
}
}