/*- * -\-\- * 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.rollingupdate; import static com.spotify.helios.common.descriptors.DeploymentGroup.RollingUpdateReason.HOSTS_CHANGED; import static com.spotify.helios.common.descriptors.DeploymentGroup.RollingUpdateReason.MANUAL; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.spotify.helios.common.descriptors.DeploymentGroup; import com.spotify.helios.common.descriptors.DeploymentGroupStatus; import com.spotify.helios.common.descriptors.DeploymentGroupTasks; import com.spotify.helios.common.descriptors.RolloutOptions; import com.spotify.helios.common.descriptors.RolloutTask; import com.spotify.helios.servicescommon.coordination.CreateEmpty; import com.spotify.helios.servicescommon.coordination.Delete; import com.spotify.helios.servicescommon.coordination.SetData; import com.spotify.helios.servicescommon.coordination.ZooKeeperClient; import com.spotify.helios.servicescommon.coordination.ZooKeeperOperation; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Set; import org.apache.zookeeper.data.Stat; import org.hamcrest.CustomTypeSafeMatcher; import org.hamcrest.Matcher; import org.junit.Test; public class RollingUpdateOpFactoryTest { private static final DeploymentGroup MANUAL_DEPLOYMENT_GROUP = DeploymentGroup.newBuilder() .setName("my_group") .setRolloutOptions(RolloutOptions.newBuilder().build()) .setRollingUpdateReason(MANUAL) .build(); private static final DeploymentGroup HOSTS_CHANGED_DEPLOYMENT_GROUP = DeploymentGroup.newBuilder() .setName("my_group") .setRolloutOptions(RolloutOptions.newBuilder().build()) .setRollingUpdateReason(HOSTS_CHANGED) .build(); private final DeploymentGroupEventFactory eventFactory = mock(DeploymentGroupEventFactory.class); @Test public void testStartManualNoHosts() throws Exception { // Create a DeploymentGroupTasks object with no rolloutTasks (defaults to empty list). final DeploymentGroupTasks deploymentGroupTasks = DeploymentGroupTasks.newBuilder() .setDeploymentGroup(MANUAL_DEPLOYMENT_GROUP) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory( deploymentGroupTasks, eventFactory); final ZooKeeperClient client = mock(ZooKeeperClient.class); when(client.exists(anyString())).thenReturn(null); final RollingUpdateOp op = opFactory.start(MANUAL_DEPLOYMENT_GROUP, client); // Three ZK operations should return: // * create tasks node // * delete the tasks // * set the status to DONE assertEquals( ImmutableSet.of( new CreateEmpty("/status/deployment-group-tasks/my_group"), new Delete("/status/deployment-group-tasks/my_group"), new SetData("/status/deployment-groups/my_group", DeploymentGroupStatus.newBuilder() .setState(DeploymentGroupStatus.State.DONE) .setError(null) .build() .toJsonBytes())), ImmutableSet.copyOf(op.operations())); // Two events should return: rollingUpdateStarted and rollingUpdateDone assertEquals(2, op.events().size()); verify(eventFactory).rollingUpdateStarted(MANUAL_DEPLOYMENT_GROUP); verify(eventFactory).rollingUpdateDone(MANUAL_DEPLOYMENT_GROUP); } @Test public void testStartManualNoHostsTasksAlreadyExist() throws Exception { // Create a DeploymentGroupTasks object with no rolloutTasks (defaults to empty list). final DeploymentGroupTasks deploymentGroupTasks = DeploymentGroupTasks.newBuilder() .setDeploymentGroup(MANUAL_DEPLOYMENT_GROUP) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory( deploymentGroupTasks, eventFactory); final ZooKeeperClient client = mock(ZooKeeperClient.class); when(client.exists(anyString())).thenReturn(mock(Stat.class)); final RollingUpdateOp op = opFactory.start(MANUAL_DEPLOYMENT_GROUP, client); // Two ZK operations should return: // * delete the tasks // * set the status to DONE assertEquals( ImmutableSet.of( new Delete("/status/deployment-group-tasks/my_group"), new SetData("/status/deployment-groups/my_group", DeploymentGroupStatus.newBuilder() .setState(DeploymentGroupStatus.State.DONE) .setError(null) .build() .toJsonBytes())), ImmutableSet.copyOf(op.operations())); // Two events should return: rollingUpdateStarted and rollingUpdateDone assertEquals(2, op.events().size()); verify(eventFactory).rollingUpdateStarted(MANUAL_DEPLOYMENT_GROUP); verify(eventFactory).rollingUpdateDone(MANUAL_DEPLOYMENT_GROUP); } @Test public void testStartManualWithHosts() throws Exception { // Create a DeploymentGroupTasks object with some rolloutTasks. final ArrayList<RolloutTask> rolloutTasks = Lists.newArrayList( RolloutTask.of(RolloutTask.Action.UNDEPLOY_OLD_JOBS, "host1"), RolloutTask.of(RolloutTask.Action.DEPLOY_NEW_JOB, "host1"), RolloutTask.of(RolloutTask.Action.AWAIT_RUNNING, "host1") ); final DeploymentGroupTasks deploymentGroupTasks = DeploymentGroupTasks.newBuilder() .setTaskIndex(0) .setRolloutTasks(rolloutTasks) .setDeploymentGroup(MANUAL_DEPLOYMENT_GROUP) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory( deploymentGroupTasks, eventFactory); final ZooKeeperClient client = mock(ZooKeeperClient.class); when(client.exists(anyString())).thenReturn(null); final RollingUpdateOp op = opFactory.start(MANUAL_DEPLOYMENT_GROUP, client); // Three ZK operations should return: // * create tasks node // * set the task index to 0 // * set the status to ROLLING_OUT assertEquals( ImmutableSet.of( new CreateEmpty("/status/deployment-group-tasks/my_group"), new SetData("/status/deployment-group-tasks/my_group", DeploymentGroupTasks.newBuilder() .setRolloutTasks(rolloutTasks) .setTaskIndex(0) .setDeploymentGroup(MANUAL_DEPLOYMENT_GROUP) .build() .toJsonBytes()), new SetData("/status/deployment-groups/my_group", DeploymentGroupStatus.newBuilder() .setState(DeploymentGroupStatus.State.ROLLING_OUT) .build() .toJsonBytes())), ImmutableSet.copyOf(op.operations())); // Two events should return: rollingUpdateStarted and rollingUpdateDone assertEquals(1, op.events().size()); verify(eventFactory).rollingUpdateStarted(MANUAL_DEPLOYMENT_GROUP); } @Test public void testStartHostsChanged() throws Exception { // Create a DeploymentGroupTasks object with some rolloutTasks. final ArrayList<RolloutTask> rolloutTasks = Lists.newArrayList( RolloutTask.of(RolloutTask.Action.UNDEPLOY_OLD_JOBS, "host1"), RolloutTask.of(RolloutTask.Action.DEPLOY_NEW_JOB, "host1"), RolloutTask.of(RolloutTask.Action.AWAIT_RUNNING, "host1") ); final DeploymentGroupTasks deploymentGroupTasks = DeploymentGroupTasks.newBuilder() .setTaskIndex(0) .setRolloutTasks(rolloutTasks) .setDeploymentGroup(HOSTS_CHANGED_DEPLOYMENT_GROUP) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory( deploymentGroupTasks, eventFactory); final ZooKeeperClient client = mock(ZooKeeperClient.class); when(client.exists(anyString())).thenReturn(null); final RollingUpdateOp op = opFactory.start(HOSTS_CHANGED_DEPLOYMENT_GROUP, client); // Three ZK operations should return: // * create tasks node // * set the task index to 0 // * another to set the status to ROLLING_OUT assertEquals( ImmutableSet.of( new CreateEmpty("/status/deployment-group-tasks/my_group"), new SetData("/status/deployment-group-tasks/my_group", DeploymentGroupTasks.newBuilder() .setRolloutTasks(rolloutTasks) .setTaskIndex(0) .setDeploymentGroup(HOSTS_CHANGED_DEPLOYMENT_GROUP) .build() .toJsonBytes()), new SetData("/status/deployment-groups/my_group", DeploymentGroupStatus.newBuilder() .setState(DeploymentGroupStatus.State.ROLLING_OUT) .build() .toJsonBytes())), ImmutableSet.copyOf(op.operations())); // Two events should return: rollingUpdateStarted and rollingUpdateDone assertEquals(1, op.events().size()); verify(eventFactory).rollingUpdateStarted(HOSTS_CHANGED_DEPLOYMENT_GROUP); } @Test public void testNextTaskNoOps() { final DeploymentGroupTasks deploymentGroupTasks = DeploymentGroupTasks.newBuilder() .setTaskIndex(0) .setRolloutTasks(Lists.newArrayList( RolloutTask.of(RolloutTask.Action.UNDEPLOY_OLD_JOBS, "host1"), RolloutTask.of(RolloutTask.Action.AWAIT_RUNNING, "host1"), RolloutTask.of(RolloutTask.Action.DEPLOY_NEW_JOB, "host1"))) .setDeploymentGroup(MANUAL_DEPLOYMENT_GROUP) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory( deploymentGroupTasks, eventFactory); final RollingUpdateOp op = opFactory.nextTask(); // A nextTask op with no ZK operations should result advancing the task index assertEquals(1, op.operations().size()); assertEquals(new SetData("/status/deployment-group-tasks/my_group", deploymentGroupTasks.toBuilder() .setTaskIndex(1) .build() .toJsonBytes()), op.operations().get(0)); // No events should be generated assertEquals(0, op.events().size()); } @Test public void testNextTaskWithOps() { final DeploymentGroupTasks deploymentGroupTasks = DeploymentGroupTasks.newBuilder() .setTaskIndex(0) .setRolloutTasks(Lists.newArrayList( RolloutTask.of(RolloutTask.Action.UNDEPLOY_OLD_JOBS, "host1"), RolloutTask.of(RolloutTask.Action.AWAIT_RUNNING, "host1"), RolloutTask.of(RolloutTask.Action.DEPLOY_NEW_JOB, "host1"))) .setDeploymentGroup(MANUAL_DEPLOYMENT_GROUP) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory( deploymentGroupTasks, eventFactory); final ZooKeeperOperation mockOp = mock(ZooKeeperOperation.class); final RollingUpdateOp op = opFactory.nextTask(Lists.newArrayList(mockOp)); // A nexTask op with ZK operations should result in advancing the task index // and also contain the specified ZK operations assertEquals( ImmutableSet.of( mockOp, new SetData("/status/deployment-group-tasks/my_group", deploymentGroupTasks.toBuilder() .setTaskIndex(1) .build() .toJsonBytes())), ImmutableSet.copyOf(op.operations())); // This is not a no-op -> an event should be emitted assertEquals(1, op.events().size()); verify(eventFactory).rollingUpdateTaskSucceeded( MANUAL_DEPLOYMENT_GROUP, deploymentGroupTasks.getRolloutTasks().get(deploymentGroupTasks.getTaskIndex())); } @Test public void testTransitionToDone() { final DeploymentGroupTasks deploymentGroupTasks = DeploymentGroupTasks.newBuilder() .setTaskIndex(2) .setRolloutTasks(Lists.newArrayList( RolloutTask.of(RolloutTask.Action.UNDEPLOY_OLD_JOBS, "host1"), RolloutTask.of(RolloutTask.Action.AWAIT_RUNNING, "host1"), RolloutTask.of(RolloutTask.Action.DEPLOY_NEW_JOB, "host1"))) .setDeploymentGroup(MANUAL_DEPLOYMENT_GROUP) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory( deploymentGroupTasks, eventFactory); final RollingUpdateOp op = opFactory.nextTask(); // When state -> DONE we expected // * deployment group tasks are deleted // * deployment group status is updated (to DONE) assertEquals( ImmutableSet.of( new SetData("/status/deployment-groups/my_group", DeploymentGroupStatus.newBuilder() .setState(DeploymentGroupStatus.State.DONE) .setError(null) .build() .toJsonBytes()), new Delete("/status/deployment-group-tasks/my_group")), ImmutableSet.copyOf(op.operations())); // ...and that an event is emitted assertEquals(1, op.events().size()); verify(eventFactory).rollingUpdateDone(MANUAL_DEPLOYMENT_GROUP); } @Test public void testTransitionToFailed() { final DeploymentGroupTasks deploymentGroupTasks = DeploymentGroupTasks.newBuilder() .setTaskIndex(0) .setRolloutTasks(Lists.newArrayList( RolloutTask.of(RolloutTask.Action.UNDEPLOY_OLD_JOBS, "host1"), RolloutTask.of(RolloutTask.Action.AWAIT_RUNNING, "host1"), RolloutTask.of(RolloutTask.Action.DEPLOY_NEW_JOB, "host1"))) .setDeploymentGroup(MANUAL_DEPLOYMENT_GROUP) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory( deploymentGroupTasks, eventFactory); final RollingUpdateOp op = opFactory.error("foo", "host1", RollingUpdateError.HOST_NOT_FOUND); final Map<String, Object> failEvent = Maps.newHashMap(); when(eventFactory.rollingUpdateTaskFailed( any(DeploymentGroup.class), any(RolloutTask.class), anyString(), any(RollingUpdateError.class))).thenReturn(failEvent); // When state -> FAILED we expected // * deployment group tasks are deleted // * deployment group status is updated (to FAILED) assertEquals( ImmutableSet.of( new SetData("/status/deployment-groups/my_group", DeploymentGroupStatus.newBuilder() .setState(DeploymentGroupStatus.State.FAILED) .setError("host1: foo") .build() .toJsonBytes()), new Delete("/status/deployment-group-tasks/my_group")), ImmutableSet.copyOf(op.operations())); // ...and that a failed-task event and a rolling-update failed event are emitted assertEquals(2, op.events().size()); verify(eventFactory).rollingUpdateTaskFailed( eq(MANUAL_DEPLOYMENT_GROUP), eq(deploymentGroupTasks.getRolloutTasks().get(deploymentGroupTasks.getTaskIndex())), anyString(), eq(RollingUpdateError.HOST_NOT_FOUND), eq(Collections.<String, Object>emptyMap())); verify(eventFactory).rollingUpdateFailed( eq(MANUAL_DEPLOYMENT_GROUP), eq(failEvent)); } @Test public void testYield() { final DeploymentGroupTasks deploymentGroupTasks = DeploymentGroupTasks.newBuilder() .setTaskIndex(0) .setRolloutTasks(Lists.newArrayList( RolloutTask.of(RolloutTask.Action.UNDEPLOY_OLD_JOBS, "host1"), RolloutTask.of(RolloutTask.Action.AWAIT_RUNNING, "host1"), RolloutTask.of(RolloutTask.Action.DEPLOY_NEW_JOB, "host1"))) .setDeploymentGroup(MANUAL_DEPLOYMENT_GROUP) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory( deploymentGroupTasks, eventFactory); final RollingUpdateOp op = opFactory.yield(); assertEquals(0, op.operations().size()); assertEquals(0, op.events().size()); } @Test public void testErrorWhenIgnoreFailuresIsTrue() { final DeploymentGroup deploymentGroup = DeploymentGroup.newBuilder() .setName("ignore_failure_group") .setRolloutOptions(RolloutOptions.newBuilder() .setIgnoreFailures(true) .build() ) .setRollingUpdateReason(MANUAL) .build(); // the current task is the AWAIT_RUNNING one final DeploymentGroupTasks tasks = DeploymentGroupTasks.newBuilder() .setTaskIndex(2) .setRolloutTasks(ImmutableList.of( RolloutTask.of(RolloutTask.Action.UNDEPLOY_OLD_JOBS, "host1"), RolloutTask.of(RolloutTask.Action.DEPLOY_NEW_JOB, "host1"), RolloutTask.of(RolloutTask.Action.AWAIT_RUNNING, "host1") )) .setDeploymentGroup(deploymentGroup) .build(); final RollingUpdateOpFactory opFactory = new RollingUpdateOpFactory(tasks, eventFactory); final RollingUpdateOp nextOp = opFactory.error("something went wrong", "host1", RollingUpdateError.TIMED_OUT_WAITING_FOR_JOB_TO_REACH_RUNNING); assertThat(nextOp.operations(), containsInAnyOrder( new SetData("/status/deployment-groups/ignore_failure_group", DeploymentGroupStatus.newBuilder() .setState(DeploymentGroupStatus.State.DONE) .setError(null) .build() .toJsonBytes() ), new Delete("/status/deployment-group-tasks/ignore_failure_group") )); } }