blob: 5dfa38fec95fdc6693b766a6a4290fa9a67b8095 [file] [log] [blame]
#
# 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.
#
import json
import time
import mock
import pytest
from mock import ANY, Mock, call, create_autospec, patch
from pystachio import Empty
from apache.aurora.client.cli import (
EXIT_API_ERROR,
EXIT_COMMAND_FAILURE,
EXIT_INVALID_PARAMETER,
EXIT_OK,
EXIT_UNKNOWN_ERROR,
Context
)
from apache.aurora.client.cli.diff_formatter import DiffFormatter
from apache.aurora.client.cli.options import TaskInstanceKey
from apache.aurora.client.cli.update import (
CLIENT_UPDATE_ID,
AbortUpdate,
ListUpdates,
PauseUpdate,
ResumeUpdate,
RollbackUpdate,
StartUpdate,
UpdateFilter,
UpdateInfo,
UpdateWait
)
from apache.aurora.common.aurora_job_key import AuroraJobKey
from apache.aurora.config import AuroraConfig
from apache.aurora.config.schema.base import Job
from .util import AuroraClientCommandTest, FakeAuroraCommandContext, mock_verb_options
from gen.apache.aurora.api.constants import ACTIVE_JOB_UPDATE_STATES
from gen.apache.aurora.api.ttypes import (
GetJobUpdateDetailsResult,
GetJobUpdateSummariesResult,
JobInstanceUpdateEvent,
JobKey,
JobUpdate,
JobUpdateAction,
JobUpdateDetails,
JobUpdateEvent,
JobUpdateKey,
JobUpdateState,
JobUpdateStatus,
JobUpdateSummary,
Metadata,
Response,
ResponseCode,
ResponseDetail,
Result,
StartJobUpdateResult
)
UPDATE_KEY = JobUpdateKey(job=AuroraClientCommandTest.TEST_JOBKEY.to_thrift(), id="update_id")
def get_status_query_response(count=1, status=JobUpdateStatus.ROLLED_FORWARD):
return Response(
responseCode=ResponseCode.OK,
result=Result(
getJobUpdateSummariesResult=GetJobUpdateSummariesResult(
updateSummaries=[
JobUpdateSummary(
key=UPDATE_KEY,
user="me",
state=JobUpdateState(
status=status,
createdTimestampMs=1411404927,
lastModifiedTimestampMs=14114056030)) for i in range(count)
]
)
)
)
class TestStartUpdate(AuroraClientCommandTest):
def setUp(self):
self._command = StartUpdate()
self._job_key = AuroraJobKey.from_thrift("cluster", UPDATE_KEY.job)
self._mock_options = mock_verb_options(self._command)
self._mock_options.instance_spec = TaskInstanceKey(self._job_key, None)
self._mock_options.wait = False
self._fake_context = FakeAuroraCommandContext()
self._fake_context.set_options(self._mock_options)
self._mock_api = self._fake_context.get_api('UNUSED')
self._formatter = Mock(spec=DiffFormatter)
super(TestStartUpdate, self).setUp()
@classmethod
def create_mock_config(cls, is_cron=False):
# TODO(wfarner): Consider using a real AuroraConfig object for this.
mock_config = create_autospec(spec=AuroraConfig, spec_set=True, instance=True)
raw_config = Job(cron_schedule='something' if is_cron else Empty)
mock_config.raw = Mock(return_value=raw_config)
mock_config.cluster = Mock(return_value=cls.TEST_CLUSTER)
return mock_config
def test_start_update_with_lock(self):
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
self._mock_api.start_job_update.return_value = AuroraClientCommandTest.create_blank_response(
ResponseCode.JOB_UPDATING_ERROR,
"Error.")
with patch('apache.aurora.client.cli.update.DiffFormatter'):
with pytest.raises(Context.CommandError):
self._command.execute(self._fake_context)
assert self._mock_api.start_job_update.mock_calls == [
call(mock_config, None, self._mock_options.instance_spec.instance, ANY)
]
self.assert_lock_message(self._fake_context)
def test_start_update_invalid_request(self):
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
err_msg = "Error."
self._mock_api.start_job_update.return_value = AuroraClientCommandTest.create_blank_response(
ResponseCode.INVALID_REQUEST,
err_msg)
with patch('apache.aurora.client.cli.update.DiffFormatter'):
with pytest.raises(Context.CommandError):
self._command.execute(self._fake_context)
assert self._mock_api.start_job_update.mock_calls == [
call(mock_config, None, self._mock_options.instance_spec.instance, ANY)
]
assert self._fake_context.get_err() == [
StartUpdate.FAILED_TO_START_UPDATE_ERROR_MSG, "\t%s" % err_msg
]
def test_start_update_already_inprogress(self):
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
update_id = 'some-mocked-uuid'
err_msg = "Active updates exist for this job."
return_value = AuroraClientCommandTest.create_start_job_update_result(
ResponseCode.INVALID_REQUEST, err_msg, UPDATE_KEY, {Metadata(CLIENT_UPDATE_ID, update_id)})
self._mock_api.start_job_update.return_value = return_value
with patch('apache.aurora.client.cli.update.DiffFormatter'):
with patch('apache.aurora.client.cli.update.uuid') as mock_uuid:
mock_uuid.uuid4.return_value = update_id
self._command.execute(self._fake_context)
assert self._mock_api.start_job_update.mock_calls == [
call(mock_config, None, self._mock_options.instance_spec.instance,
{CLIENT_UPDATE_ID: update_id})
]
assert self._fake_context.get_out() == [
StartUpdate.UPDATE_MSG_TEMPLATE %
('http://something_or_other/scheduler/bozo/test/hello/update/update_id'),
]
assert self._fake_context.get_err() == []
def test_start_update_different_update_inprogress(self):
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
update_id = 'some-mocked-uuid'
update_id_2 = 'some-other-mocked-uuid'
err_msg = "Active updates exist for this job."
return_value = AuroraClientCommandTest.create_start_job_update_result(
ResponseCode.INVALID_REQUEST, err_msg, UPDATE_KEY, {Metadata(CLIENT_UPDATE_ID, update_id)})
self._mock_api.start_job_update.return_value = return_value
with patch('apache.aurora.client.cli.update.DiffFormatter'):
with pytest.raises(Context.CommandError):
with patch('apache.aurora.client.cli.update.uuid') as mock_uuid:
mock_uuid.uuid4.return_value = update_id_2
self._command.execute(self._fake_context)
assert self._mock_api.start_job_update.mock_calls == [
call(mock_config, None, self._mock_options.instance_spec.instance,
{CLIENT_UPDATE_ID: update_id_2})
]
assert self._fake_context.get_out() == []
assert self._fake_context.get_err() == [
StartUpdate.FAILED_TO_START_UPDATE_ERROR_MSG, "\t%s" % err_msg
]
def test_update_cron_job_fails(self):
mock_config = self.create_mock_config(is_cron=True)
self._fake_context.get_job_config = Mock(return_value=mock_config)
with pytest.raises(Context.CommandError):
self._command.execute(self._fake_context)
def test_update_no_active_instance_check(self):
self._mock_options.instance_spec = TaskInstanceKey(self.TEST_JOBKEY, [1])
self._mock_options.strict = True
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
self._mock_api.start_job_update.return_value = self.create_simple_success_response()
with patch('apache.aurora.client.cli.update.DiffFormatter') as formatter:
formatter.return_value = self._formatter
self._command.execute(self._fake_context)
assert self._formatter.show_job_update_diff.mock_calls == [
call(self._mock_options.instance_spec.instance)
]
assert self._mock_api.start_job_update.mock_calls == [
call(mock_config, None, self._mock_options.instance_spec.instance, ANY)
]
def test_start_update_command_line_succeeds(self):
resp = self.create_simple_success_response()
resp.result = Result(startJobUpdateResult=StartJobUpdateResult(
key=JobUpdateKey(job=JobKey(role="role", environment="env", name="name"), id="id")))
self._mock_api.start_job_update.return_value = resp
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
self._mock_options.instance_spec = TaskInstanceKey(self._job_key, None)
self._mock_options.message = 'hello'
with patch('apache.aurora.client.cli.update.DiffFormatter') as formatter:
formatter.return_value = self._formatter
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._formatter.show_job_update_diff.mock_calls == [
call(self._mock_options.instance_spec.instance)
]
assert self._mock_api.start_job_update.mock_calls == [
call(ANY, 'hello', None, ANY)
]
assert self._fake_context.get_out() == [
StartUpdate.UPDATE_MSG_TEMPLATE %
('http://something_or_other/scheduler/role/env/name/update/id'),
]
assert self._fake_context.get_err() == []
def test_start_update_and_wait_success(self):
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
self._mock_options.wait = True
resp = self.create_simple_success_response()
resp.result = Result(startJobUpdateResult=StartJobUpdateResult(
key=JobUpdateKey(job=JobKey(role="role", environment="env", name="name"), id="id")))
self._mock_api.start_job_update.return_value = resp
self._mock_api.query_job_updates.side_effect = [
get_status_query_response(status=JobUpdateStatus.ROLLED_FORWARD)
]
with patch('apache.aurora.client.cli.update.DiffFormatter') as formatter:
formatter.return_value = self._formatter
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._formatter.show_job_update_diff.mock_calls == [
call(self._mock_options.instance_spec.instance)
]
assert self._mock_api.start_job_update.mock_calls == [call(ANY, None, None, ANY)]
assert self._mock_api.query_job_updates.mock_calls == [
call(update_key=resp.result.startJobUpdateResult.key)
]
assert self._fake_context.get_out() == [
StartUpdate.UPDATE_MSG_TEMPLATE %
('http://something_or_other/scheduler/role/env/name/update/id'),
'Current state ROLLED_FORWARD'
]
assert self._fake_context.get_err() == []
def test_start_update_and_wait_rollback(self):
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
self._mock_options.wait = True
resp = self.create_simple_success_response()
resp.result = Result(startJobUpdateResult=StartJobUpdateResult(
key=JobUpdateKey(job=JobKey(role="role", environment="env", name="name"), id="id")))
self._mock_api.start_job_update.return_value = resp
self._mock_api.query_job_updates.side_effect = [
get_status_query_response(status=JobUpdateStatus.ROLLED_BACK)
]
with patch('apache.aurora.client.cli.update.DiffFormatter'):
assert self._command.execute(self._fake_context) == EXIT_COMMAND_FAILURE
def test_start_update_and_wait_error(self):
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
self._mock_options.wait = True
resp = self.create_simple_success_response()
resp.result = Result(startJobUpdateResult=StartJobUpdateResult(
key=JobUpdateKey(job=JobKey(role="role", environment="env", name="name"), id="id")))
self._mock_api.start_job_update.return_value = resp
self._mock_api.query_job_updates.side_effect = [
get_status_query_response(status=JobUpdateStatus.ERROR)
]
with patch('apache.aurora.client.cli.update.DiffFormatter'):
assert self._command.execute(self._fake_context) == EXIT_UNKNOWN_ERROR
def test_start_update_command_line_succeeds_noop_update(self):
resp = self.create_simple_success_response()
resp.details = [ResponseDetail(message="Noop update.")]
self._mock_api.start_job_update.return_value = resp
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
with patch('apache.aurora.client.cli.update.DiffFormatter') as formatter:
formatter.return_value = self._formatter
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._formatter.show_job_update_diff.mock_calls == [
call(self._mock_options.instance_spec.instance)
]
assert self._mock_api.start_job_update.mock_calls == [call(ANY, None, None, ANY)]
assert self._fake_context.get_out() == [
"Noop update."
]
assert self._fake_context.get_err() == []
def test_update_pulse_interval_too_small(self):
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
error = Context.CommandError(100, 'something failed')
self._mock_api.start_job_update.side_effect = error
with patch('apache.aurora.client.cli.update.DiffFormatter'):
with pytest.raises(Context.CommandError) as e:
self._command.execute(self._fake_context)
assert e.value == error
assert self._mock_api.start_job_update.mock_calls == [call(ANY, None, None, ANY)]
def test_start_update_and_open_page(self):
mock_config = self.create_mock_config()
self._fake_context.get_job_config = Mock(return_value=mock_config)
self._mock_options.open_browser = True
resp = self.create_simple_success_response()
resp.result = Result(startJobUpdateResult=StartJobUpdateResult(
key=JobUpdateKey(job=JobKey(role="role", environment="env", name="name"), id="id")))
self._mock_api.start_job_update.return_value = resp
with patch('apache.aurora.client.cli.update.DiffFormatter') as formatter:
formatter.return_value = self._formatter
assert self._command.execute(self._fake_context) == EXIT_OK
assert self.mock_webbrowser.mock_calls == [
call('http://something_or_other/scheduler/role/env/name/update/id')
]
class TestListUpdates(AuroraClientCommandTest):
def setUp(self):
self._command = ListUpdates()
self._job_key = AuroraJobKey.from_thrift("cluster", UPDATE_KEY.job)
self._mock_options = mock_verb_options(self._command)
self._mock_options.filter = UpdateFilter(
cluster=self.TEST_CLUSTER, role=None, env=None, job=None)
self._fake_context = FakeAuroraCommandContext()
self._fake_context.set_options(self._mock_options)
self._mock_api = self._fake_context.get_api('UNUSED')
def test_list_updates_command(self):
self._mock_api.query_job_updates.return_value = get_status_query_response(count=3)
self._mock_options.user = 'me'
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._mock_api.query_job_updates.mock_calls == [
call(role=None, user="me", job_key=None, update_statuses=None)
]
# Ideally we would use a resource file for this, but i was unable to find a way to make that
# work in both pants and pycharm.
assert self._fake_context.get_out_str() == "\
JOB UPDATE ID STATUS \
CREATED BY STARTED LAST MODIFIED " + """
west/bozo/test/hello update_id ROLLED_FORWARD\
me 1970-01-17T08:03:24 1970-06-13T08:34:16
west/bozo/test/hello update_id ROLLED_FORWARD\
me 1970-01-17T08:03:24 1970-06-13T08:34:16
west/bozo/test/hello update_id ROLLED_FORWARD\
me 1970-01-17T08:03:24 1970-06-13T08:34:16"""
def test_list_updates_by_status(self):
self._mock_options.status = ListUpdates.STATUS_GROUPS.keys()
self._mock_api.query_job_updates.return_value = get_status_query_response(count=3)
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._mock_api.query_job_updates.mock_calls == [
call(
role=None,
user=None,
job_key=None,
update_statuses=set(JobUpdateStatus._VALUES_TO_NAMES.keys()))
]
def test_list_updates_by_env(self):
self._mock_options.filter = UpdateFilter(
cluster=self.TEST_CLUSTER, role='role', env='noenv', job=None)
self._mock_api.query_job_updates.return_value = get_status_query_response(count=3)
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._mock_api.query_job_updates.mock_calls == [
call(role="role", user=None, job_key=None, update_statuses=None)
]
# None of the returned values matched the env filter, so there is no output.
assert self._fake_context.get_out_str() == ''
def test_list_updates_command_json(self):
self._mock_options.user = 'me'
self._mock_options.write_json = True
self._mock_api.query_job_updates.return_value = get_status_query_response(count=3)
assert self._command.execute(self._fake_context) == EXIT_OK
assert json.loads(self._fake_context.get_out_str()) == [
{
"status": "ROLLED_FORWARD",
"started": "1970-01-17T08:03:24",
"last_modified": "1970-06-13T08:34:16",
"user": "me",
"job": "west/bozo/test/hello",
"id": "update_id"
},
{
"status": "ROLLED_FORWARD",
"started": "1970-01-17T08:03:24",
"last_modified": "1970-06-13T08:34:16",
"user": "me",
"job": "west/bozo/test/hello",
"id": "update_id"
},
{
"status": "ROLLED_FORWARD",
"started": "1970-01-17T08:03:24",
"last_modified": "1970-06-13T08:34:16",
"user": "me",
"job": "west/bozo/test/hello",
"id": "update_id"
}
]
class TestUpdateStatus(AuroraClientCommandTest):
def setUp(self):
self._command = UpdateInfo()
self._mock_options = mock_verb_options(self._command)
self._mock_options.jobspec = self.TEST_JOBKEY
self._mock_options.id = None
self._fake_context = FakeAuroraCommandContext()
self._fake_context.set_options(self._mock_options)
self._mock_api = self._fake_context.get_api('UNUSED')
def test_status_fails_no_updates(self):
response = self.create_simple_success_response()
response.result = Result(
getJobUpdateSummariesResult=GetJobUpdateSummariesResult(updateSummaries=[]))
self._mock_api.query_job_updates.return_value = response
assert EXIT_INVALID_PARAMETER == self._command.execute(self._fake_context)
assert self._fake_context.get_err() == ["There is no active update for this job."]
class TestPauseUpdate(AuroraClientCommandTest):
def setUp(self):
self._command = PauseUpdate()
self._mock_options = mock_verb_options(self._command)
self._mock_options.jobspec = self.TEST_JOBKEY
self._fake_context = FakeAuroraCommandContext()
self._fake_context.set_options(self._mock_options)
self._mock_api = self._fake_context.get_api('UNUSED')
def test_pause_update_command_line_succeeds(self):
self._mock_api.query_job_updates.return_value = get_status_query_response()
self._mock_api.pause_job_update.return_value = self.create_simple_success_response()
self._mock_options.message = 'hello'
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.pause_job_update.mock_calls == [call(UPDATE_KEY, 'hello')]
assert self._fake_context.get_out() == ["Update has been paused."]
assert self._fake_context.get_err() == []
def test_pause_update_command_line_error(self):
self._mock_api.query_job_updates.return_value = get_status_query_response()
self._mock_api.pause_job_update.return_value = self.create_error_response()
with pytest.raises(Context.CommandError):
self._command.execute(self._fake_context)
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.pause_job_update.mock_calls == [call(UPDATE_KEY, None)]
assert self._fake_context.get_out() == []
assert self._fake_context.get_err() == ["Failed to pause update due to error:", "\tWhoops"]
class TestAbortUpdate(AuroraClientCommandTest):
def setUp(self):
self._command = AbortUpdate()
self._mock_options = mock_verb_options(self._command)
self._mock_options.jobspec = self.TEST_JOBKEY
self._fake_context = FakeAuroraCommandContext()
self._fake_context.set_options(self._mock_options)
self._mock_api = self._fake_context.get_api('UNUSED')
def test_abort_update_command_line_succeeds(self):
self._mock_api.query_job_updates.return_value = get_status_query_response()
self._mock_api.abort_job_update.return_value = self.create_simple_success_response()
self._mock_options.message = 'hello'
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.abort_job_update.mock_calls == [call(UPDATE_KEY, 'hello')]
assert self._fake_context.get_out() == ["Update has been aborted."]
assert self._fake_context.get_err() == []
def test_abort_update_command_line_error(self):
self._mock_api.query_job_updates.return_value = get_status_query_response()
self._mock_api.abort_job_update.return_value = self.create_error_response()
with pytest.raises(Context.CommandError):
self._command.execute(self._fake_context)
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.abort_job_update.mock_calls == [call(UPDATE_KEY, None)]
assert self._fake_context.get_out() == []
assert self._fake_context.get_err() == ["Failed to abort update due to error:", "\tWhoops"]
def test_abort_invalid_api_response(self):
# Mimic the API returning two active updates for one job, which should be impossible.
self._mock_api.query_job_updates.return_value = get_status_query_response(count=2)
self._mock_api.abort_job_update.return_value = self.create_error_response()
with pytest.raises(Context.CommandError) as error:
self._command.execute(self._fake_context)
assert error.message == (
'scheduler returned multiple active updates for this job.')
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.abort_job_update.mock_calls == []
assert self._fake_context.get_out() == []
assert self._fake_context.get_err() == []
class TestResumeUpdate(AuroraClientCommandTest):
def setUp(self):
self._command = ResumeUpdate()
self._mock_options = mock_verb_options(self._command)
self._mock_options.jobspec = self.TEST_JOBKEY
self._fake_context = FakeAuroraCommandContext()
self._fake_context.set_options(self._mock_options)
self._mock_api = self._fake_context.get_api('UNUSED')
def test_resume_update_command_line_succeeds(self):
self._mock_api.query_job_updates.return_value = get_status_query_response()
self._mock_api.resume_job_update.return_value = self.create_simple_success_response()
self._mock_options.message = 'hello'
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.resume_job_update.mock_calls == [call(UPDATE_KEY, 'hello')]
assert self._fake_context.get_out() == ["Update has been resumed."]
assert self._fake_context.get_err() == []
def test_resume_update_command_line_error(self):
self._mock_api.query_job_updates.return_value = get_status_query_response()
self._mock_api.resume_job_update.return_value = self.create_error_response()
with pytest.raises(Context.CommandError):
self._command.execute(self._fake_context)
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.resume_job_update.mock_calls == [call(UPDATE_KEY, None)]
assert self._fake_context.get_out() == []
assert self._fake_context.get_err() == ["Failed to resume update due to error:", "\tWhoops"]
class TestUpdateInfo(AuroraClientCommandTest):
def setUp(self):
self._command = UpdateInfo()
self._mock_options = mock_verb_options(self._command)
self._mock_options.jobspec = self.TEST_JOBKEY
self._mock_options.id = None
self._fake_context = FakeAuroraCommandContext()
self._fake_context.set_options(self._mock_options)
self._mock_api = self._fake_context.get_api('UNUSED')
@classmethod
def get_update_details_response(cls):
query_response = Response()
query_response.responseCode = ResponseCode.OK
query_response.result = Result()
details = JobUpdateDetails(
update=JobUpdate(
summary=JobUpdateSummary(
key=UPDATE_KEY,
user="me",
state=JobUpdateState(
status=JobUpdateStatus.ROLLING_FORWARD,
createdTimestampMs=1000,
lastModifiedTimestampMs=2000),
metadata={
Metadata("issue", "test"),
Metadata("country", "America"),
Metadata("country", "Canada")})),
updateEvents=[
JobUpdateEvent(
status=JobUpdateStatus.ROLLING_FORWARD,
timestampMs=3000),
JobUpdateEvent(
status=JobUpdateStatus.ROLL_FORWARD_PAUSED,
message="Investigating issues",
timestampMs=4000),
JobUpdateEvent(
status=JobUpdateStatus.ROLLING_FORWARD,
timestampMs=5000)],
instanceEvents=[
JobInstanceUpdateEvent(
instanceId=1,
timestampMs=6000,
action=JobUpdateAction.INSTANCE_UPDATING),
JobInstanceUpdateEvent(
instanceId=2,
timestampMs=7000,
action=JobUpdateAction.INSTANCE_UPDATING),
JobInstanceUpdateEvent(
instanceId=1,
timestampMs=8000,
action=JobUpdateAction.INSTANCE_UPDATED),
JobInstanceUpdateEvent(
instanceId=2,
timestampMs=9000,
action=JobUpdateAction.INSTANCE_UPDATED)])
query_response.result.getJobUpdateDetailsResult = GetJobUpdateDetailsResult(
detailsList=[details])
return query_response
def test_active_update_info(self):
self._mock_api.query_job_updates.return_value = get_status_query_response(count=1)
self._mock_api.get_job_update_details.return_value = self.get_update_details_response()
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._fake_context.get_api(self.TEST_CLUSTER).query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)
]
assert '\n'.join(self._fake_context.get_out()) == """\
Job: west/bozo/test/hello, UpdateID: update_id
Started 1970-01-01T00:00:01, last activity: 1970-01-01T00:00:02
Current status: ROLLING_FORWARD
Update events:
Status: ROLLING_FORWARD at 1970-01-01T00:00:03
Status: ROLL_FORWARD_PAUSED at 1970-01-01T00:00:04
message: Investigating issues
Status: ROLLING_FORWARD at 1970-01-01T00:00:05
Instance events:
Instance 1 at 1970-01-01T00:00:06: INSTANCE_UPDATING
Instance 2 at 1970-01-01T00:00:07: INSTANCE_UPDATING
Instance 1 at 1970-01-01T00:00:08: INSTANCE_UPDATED
Instance 2 at 1970-01-01T00:00:09: INSTANCE_UPDATED
Metadata:
country: America
country: Canada
issue: test"""
def test_update_info(self):
self._mock_options.id = 'update_id'
self._mock_api.query_job_updates.return_value = get_status_query_response(count=1)
self._mock_api.get_job_update_details.return_value = self.get_update_details_response()
assert self._command.execute(self._fake_context) == EXIT_OK
def test_update_info_json(self):
self._mock_options.write_json = True
update_status_response = get_status_query_response(count=1)
self._mock_api.query_job_updates.return_value = update_status_response
self._mock_api.get_job_update_details.return_value = self.get_update_details_response()
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)
]
assert self._mock_api.get_job_update_details.mock_calls == [
call(update_status_response.result.getJobUpdateSummariesResult.updateSummaries[0].key)
]
assert json.loads(self._fake_context.get_out_str()) == {
"status": "ROLLING_FORWARD",
"last_modified": "1970-01-01T00:00:02",
"started": 1000,
"update_events": [
{
"status": "ROLLING_FORWARD",
"timestampMs": 3000
},
{
"status": "ROLL_FORWARD_PAUSED",
"message": "Investigating issues",
"timestampMs": 4000
},
{
"status": "ROLLING_FORWARD",
"timestampMs": 5000
}
],
"job": "west/bozo/test/hello",
"updateId": "update_id",
"instance_update_events": [
{
"action": "INSTANCE_UPDATING",
"instance": 1,
"timestamp": 6000
},
{
"action": "INSTANCE_UPDATING",
"instance": 2,
"timestamp": 7000
},
{
"action": "INSTANCE_UPDATED",
"instance": 1,
"timestamp": 8000
},
{
"action": "INSTANCE_UPDATED",
"instance": 2,
"timestamp": 9000
}
],
"metadata": [
{"country": "America"},
{"country": "Canada"},
{"issue": "test"}
]
}
class TestUpdateWait(AuroraClientCommandTest):
def setUp(self):
self._command = UpdateWait(clock=mock.create_autospec(spec=time))
self._mock_options = mock_verb_options(self._command)
self._mock_options.jobspec = self.TEST_JOBKEY
self._mock_options.id = 'update_id'
self._fake_context = FakeAuroraCommandContext()
self._fake_context.set_options(self._mock_options)
self._mock_api = self._fake_context.get_api('UNUSED')
self._fetch_call = call(
update_key=JobUpdateKey(job=self.TEST_JOBKEY.to_thrift(), id=self._mock_options.id))
def test_wait_success(self):
updating_response = get_status_query_response(status=JobUpdateStatus.ROLLING_FORWARD)
updated_response = get_status_query_response(status=JobUpdateStatus.ROLLED_FORWARD)
self._mock_api.query_job_updates.side_effect = [updating_response, updated_response]
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._fake_context.get_out() == ['Current state ROLLING_FORWARD',
'Current state ROLLED_FORWARD']
assert self._mock_api.query_job_updates.mock_calls == [self._fetch_call, self._fetch_call]
def test_wait_rolled_back(self):
response = get_status_query_response(status=JobUpdateStatus.ROLLED_BACK)
self._mock_api.query_job_updates.side_effect = [response]
assert self._command.execute(self._fake_context) == EXIT_COMMAND_FAILURE
def test_wait_non_ok_response(self):
self._mock_api.query_job_updates.return_value = Response(
responseCode=ResponseCode.INVALID_REQUEST)
with pytest.raises(Context.CommandError) as e:
self._command.execute(self._fake_context)
assert e.value.code == EXIT_API_ERROR
assert self._fake_context.get_out() == []
assert self._mock_api.query_job_updates.mock_calls == [self._fetch_call]
def test_update_wait_not_found(self):
self._mock_api.query_job_updates.return_value = Response(
responseCode=ResponseCode.OK,
result=Result(getJobUpdateSummariesResult=GetJobUpdateSummariesResult(updateSummaries=[])))
with pytest.raises(Context.CommandError) as e:
self._command.execute(self._fake_context)
assert e.value.code == EXIT_INVALID_PARAMETER
assert self._fake_context.get_out() == []
assert self._mock_api.query_job_updates.mock_calls == [self._fetch_call]
def test_wait_scheduler_returns_multiple_summaries(self):
self._mock_api.query_job_updates.return_value = get_status_query_response(count=2)
with pytest.raises(Context.CommandError) as e:
self._command.execute(self._fake_context)
assert e.value.code == EXIT_API_ERROR
assert self._fake_context.get_out() == []
assert self._mock_api.query_job_updates.mock_calls == [self._fetch_call]
class TestRollbackUpdate(AuroraClientCommandTest):
def setUp(self):
self._command = RollbackUpdate()
self._mock_options = mock_verb_options(self._command)
self._mock_options.jobspec = self.TEST_JOBKEY
self._mock_options.wait = False
self._fake_context = FakeAuroraCommandContext()
self._fake_context.set_options(self._mock_options)
self._mock_api = self._fake_context.get_api('UNUSED')
def test_rollback_update_command_line_succeeds(self):
self._mock_api.query_job_updates.return_value = get_status_query_response()
self._mock_api.rollback_job_update.return_value = self.create_simple_success_response()
self._mock_options.message = 'hello'
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.rollback_job_update.mock_calls == [call(UPDATE_KEY, 'hello')]
assert self._fake_context.get_out() == ["Update rollback has started."]
assert self._fake_context.get_err() == []
def test_rollback_update_command_line_error(self):
self._mock_api.query_job_updates.return_value = get_status_query_response()
self._mock_api.rollback_job_update.return_value = self.create_error_response()
with pytest.raises(Context.CommandError):
self._command.execute(self._fake_context)
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.rollback_job_update.mock_calls == [call(UPDATE_KEY, None)]
assert self._fake_context.get_out() == []
assert self._fake_context.get_err() == ["Failed to rollback update due to error:", "\tWhoops"]
def test_rollback_invalid_api_response(self):
# Mimic the API returning two active updates for one job, which should be impossible.
self._mock_api.query_job_updates.return_value = get_status_query_response(count=2)
self._mock_api.rollback_job_update.return_value = self.create_error_response()
with pytest.raises(Context.CommandError) as error:
self._command.execute(self._fake_context)
assert error.message == (
'scheduler returned multiple active updates for this job.')
assert self._mock_api.query_job_updates.mock_calls == [
call(update_statuses=ACTIVE_JOB_UPDATE_STATES, job_key=self.TEST_JOBKEY)]
assert self._mock_api.rollback_job_update.mock_calls == []
assert self._fake_context.get_out() == []
assert self._fake_context.get_err() == []
def test_rollback_and_wait_success(self):
self._mock_options.wait = True
updating_response = get_status_query_response(status=JobUpdateStatus.ROLLING_FORWARD)
updated_response = get_status_query_response(status=JobUpdateStatus.ROLLED_BACK)
self._mock_api.query_job_updates.side_effect = [updating_response, updated_response]
self._mock_api.rollback_job_update.return_value = self.create_simple_success_response()
assert self._command.execute(self._fake_context) == EXIT_OK
assert self._mock_api.rollback_job_update.mock_calls == [call(UPDATE_KEY, None)]
assert self._fake_context.get_err() == []
def test_rollback_and_wait_rolled_forward(self):
self._mock_options.wait = True
updating_response = get_status_query_response(status=JobUpdateStatus.ROLLING_FORWARD)
updated_response = get_status_query_response(status=JobUpdateStatus.ROLLED_FORWARD)
self._mock_api.query_job_updates.side_effect = [updating_response, updated_response]
self._mock_api.rollback_job_update.return_value = self.create_simple_success_response()
assert self._command.execute(self._fake_context) == EXIT_COMMAND_FAILURE
def test_rollback_and_wait_error(self):
self._mock_options.wait = True
updating_response = get_status_query_response(status=JobUpdateStatus.ROLLING_FORWARD)
failed_response = get_status_query_response(status=JobUpdateStatus.ERROR)
self._mock_api.query_job_updates.side_effect = [updating_response, failed_response]
self._mock_api.rollback_job_update.return_value = self.create_simple_success_response()
assert self._command.execute(self._fake_context) == EXIT_UNKNOWN_ERROR