armada/armada/tests/unit/handlers/test_armada.py

675 lines
25 KiB
Python

# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# 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 mock
import yaml
from armada import const
from armada.handlers import armada
from armada.handlers.test import TESTRUN_STATUS_SUCCESS, TESTRUN_STATUS_FAILURE
from armada.tests.unit import base
from armada.tests.test_utils import AttrDict, makeMockThreadSafe
from armada.utils.release import release_prefixer, get_release_status
from armada.exceptions import ManifestException
from armada.exceptions.override_exceptions import InvalidOverrideValueException
from armada.exceptions.validate_exceptions import InvalidManifestException
from armada.exceptions import tiller_exceptions
from armada.exceptions.armada_exceptions import ChartDeployException
makeMockThreadSafe()
TEST_YAML = """
---
schema: armada/Manifest/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
release_prefix: armada
chart_groups:
- example-group
---
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: example-group
data:
description: this is a test
sequenced: True
chart_group:
- example-chart-1
- example-chart-2
- example-chart-3
- example-chart-4
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart-4
data:
chart_name: test_chart_4
release: test_chart_4
namespace: test
values: {}
source:
type: local
location: /tmp/dummy/armada
subpath: chart_4
dependencies: []
test: true
wait:
timeout: 10
upgrade:
no_hooks: false
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart-3
data:
chart_name: test_chart_3
release: test_chart_3
namespace: test
values: {}
source:
type: local
location: /tmp/dummy/armada
subpath: chart_3
dependencies: []
protected:
continue_processing: false
wait:
timeout: 10
upgrade:
no_hooks: false
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart-2
data:
chart_name: test_chart_2
release: test_chart_2
namespace: test
values: {}
source:
type: local
location: /tmp/dummy/armada
subpath: chart_2
dependencies: []
protected:
continue_processing: true
wait:
timeout: 10
upgrade:
no_hooks: false
options:
force: true
recreate_pods: true
test:
enabled: true
options:
cleanup: true
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart-1
data:
chart_name: test_chart_1
release: test_chart_1
namespace: test
values: {}
source:
type: git
location: git://github.com/dummy/armada
subpath: chart_1
reference: master
dependencies: []
wait:
timeout: 10
native:
enabled: false
test:
enabled: true
"""
CHART_SOURCES = [('git://github.com/dummy/armada', 'chart_1'),
('/tmp/dummy/armada', 'chart_2'),
('/tmp/dummy/armada', 'chart_3'),
('/tmp/dummy/armada', 'chart_4')]
# TODO(seaneagan): Add unit tests with dependencies, including transitive.
class ArmadaHandlerTestCase(base.ArmadaTestCase):
def _test_pre_flight_ops(self, armada_obj):
armada_obj.pre_flight_ops()
expected_config = {
'armada': {
'release_prefix':
'armada',
'chart_groups': [{
'chart_group': [{
'chart': {
'dependencies': [],
'chart_name': 'test_chart_1',
'namespace': 'test',
'release': 'test_chart_1',
'source': {
'location': ('git://github.com/dummy/armada'),
'reference': 'master',
'subpath': 'chart_1',
'type': 'git'
},
'source_dir': CHART_SOURCES[0],
'values': {},
'wait': {
'timeout': 10,
'native': {
'enabled': False
}
},
'test': {
'enabled': True
}
}
}, {
'chart': {
'dependencies': [],
'chart_name': 'test_chart_2',
'namespace': 'test',
'protected': {
'continue_processing': True
},
'release': 'test_chart_2',
'source': {
'location': '/tmp/dummy/armada',
'subpath': 'chart_2',
'type': 'local'
},
'source_dir': CHART_SOURCES[1],
'values': {},
'wait': {
'timeout': 10
},
'upgrade': {
'no_hooks': False,
'options': {
'force': True,
'recreate_pods': True
}
},
'test': {
'enabled': True,
'options': {
'cleanup': True
}
}
}
}, {
'chart': {
'dependencies': [],
'chart_name': 'test_chart_3',
'namespace': 'test',
'protected': {
'continue_processing': False
},
'release': 'test_chart_3',
'source': {
'location': '/tmp/dummy/armada',
'subpath': 'chart_3',
'type': 'local'
},
'source_dir': CHART_SOURCES[2],
'values': {},
'wait': {
'timeout': 10
},
'upgrade': {
'no_hooks': False
}
}
}, {
'chart': {
'dependencies': [],
'chart_name': 'test_chart_4',
'namespace': 'test',
'release': 'test_chart_4',
'source': {
'location': '/tmp/dummy/armada',
'subpath': 'chart_4',
'type': 'local'
},
'source_dir': CHART_SOURCES[3],
'values': {},
'wait': {
'timeout': 10
},
'upgrade': {
'no_hooks': False
},
'test': True
}
}],
'description':
'this is a test',
'name':
'example-group',
'sequenced':
True
}]
}
} # yapf: disable
self.assertTrue(hasattr(armada_obj, 'manifest'))
self.assertIsInstance(armada_obj.manifest, dict)
self.assertIn('armada', armada_obj.manifest)
self.assertEqual(expected_config, armada_obj.manifest)
@mock.patch.object(armada, 'source')
def test_pre_flight_ops(self, mock_source):
"""Test pre-flight checks and operations."""
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
m_tiller = mock.Mock()
m_tiller.tiller_status.return_value = True
armada_obj = armada.Armada(yaml_documents, m_tiller)
# Mock methods called by `pre_flight_ops()`.
mock_source.git_clone.return_value = CHART_SOURCES[0][0]
self._test_pre_flight_ops(armada_obj)
mock_source.git_clone.assert_called_once_with(
'git://github.com/dummy/armada',
'master',
auth_method=None,
proxy_server=None)
@mock.patch.object(armada, 'source')
def test_post_flight_ops(self, mock_source):
"""Test post-flight operations."""
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
# Mock methods called by `pre_flight_ops()`.
m_tiller = mock.Mock()
m_tiller.tiller_status.return_value = True
mock_source.git_clone.return_value = CHART_SOURCES[0][0]
armada_obj = armada.Armada(yaml_documents, m_tiller)
self._test_pre_flight_ops(armada_obj)
armada_obj.post_flight_ops()
for group in armada_obj.manifest['armada']['chart_groups']:
for counter, chart in enumerate(group.get('chart_group')):
if chart.get('chart').get('source').get('type') == 'git':
mock_source.source_cleanup.assert_called_with(
CHART_SOURCES[counter][0])
# TODO(seaneagan): Separate ChartDeploy tests into separate module.
# TODO(seaneagan): Once able to make mock library sufficiently thread safe,
# run sync tests for unsequenced as well by moving them to separate test
# class with two separate subclasses which set chart group `sequenced`
# field, one to true, one to false.
def _test_sync(self,
known_releases,
test_success=True,
test_failure_to_run=False,
expected_last_test_result=None,
diff={'some_key': {'some diff'}}):
"""Test install functionality from the sync() method."""
@mock.patch.object(armada.Armada, 'post_flight_ops')
@mock.patch.object(armada.Armada, 'pre_flight_ops')
@mock.patch('armada.handlers.chart_deploy.ChartBuilder')
@mock.patch('armada.handlers.chart_deploy.Test')
def _do_test(mock_test, mock_chartbuilder, mock_pre_flight,
mock_post_flight):
# Instantiate Armada object.
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
m_tiller = mock.MagicMock()
m_tiller.list_releases.return_value = known_releases
armada_obj = armada.Armada(yaml_documents, m_tiller)
armada_obj.chart_deploy.get_diff = mock.Mock()
chart_group = armada_obj.manifest['armada']['chart_groups'][0]
charts = chart_group['chart_group']
cg_test_all_charts = chart_group.get('test_charts')
mock_test_release = mock_test.return_value.test_release_for_success
if test_failure_to_run:
def fail(tiller, release, timeout=None, cleanup=False):
status = AttrDict(
**{'info': AttrDict(**{'Description': 'Failed'})})
raise tiller_exceptions.ReleaseException(
release, status, 'Test')
mock_test_release.side_effect = fail
else:
mock_test_release.return_value = test_success
# Stub out irrelevant methods called by `armada.sync()`.
mock_chartbuilder.get_source_path.return_value = None
mock_chartbuilder.get_helm_chart.return_value = None
# Simulate chart diff, upgrade should only happen if non-empty.
armada_obj.chart_deploy.get_diff.return_value = diff
armada_obj.sync()
expected_install_release_calls = []
expected_update_release_calls = []
expected_uninstall_release_calls = []
expected_test_constructor_calls = []
for c in charts:
chart = c['chart']
release = chart['release']
prefix = armada_obj.manifest['armada']['release_prefix']
release_name = release_prefixer(prefix, release)
# Simplified check because the actual code uses logical-or's
# multiple conditions, so this is enough.
native_wait_enabled = (chart['wait'].get('native', {}).get(
'enabled', True))
if release_name not in [x.name for x in known_releases]:
expected_install_release_calls.append(
mock.call(
mock_chartbuilder().get_helm_chart(),
"{}-{}".format(
armada_obj.manifest['armada']
['release_prefix'], chart['release']),
chart['namespace'],
values=yaml.safe_dump(chart['values']),
wait=native_wait_enabled,
timeout=mock.ANY))
else:
target_release = None
for known_release in known_releases:
if known_release.name == release_name:
target_release = known_release
break
if target_release:
status = get_release_status(target_release)
if status == const.STATUS_FAILED:
protected = chart.get('protected', {})
if not protected:
expected_uninstall_release_calls.append(
mock.call(release_name))
expected_install_release_calls.append(
mock.call(
mock_chartbuilder().get_helm_chart(),
"{}-{}".format(
armada_obj.manifest['armada']
['release_prefix'],
chart['release']),
chart['namespace'],
values=yaml.safe_dump(chart['values']),
wait=native_wait_enabled,
timeout=mock.ANY))
else:
p_continue = protected.get(
'continue_processing', False)
if p_continue:
continue
else:
if chart_group['sequenced']:
break
if status == const.STATUS_DEPLOYED:
if diff:
upgrade = chart.get('upgrade', {})
disable_hooks = upgrade.get('no_hooks', False)
force = upgrade.get('force', False)
recreate_pods = upgrade.get(
'recreate_pods', False)
expected_update_release_calls.append(
mock.call(
mock_chartbuilder().get_helm_chart(),
"{}-{}".format(
armada_obj.manifest['armada']
['release_prefix'],
chart['release']),
chart['namespace'],
pre_actions={},
post_actions={},
disable_hooks=disable_hooks,
force=force,
recreate_pods=recreate_pods,
values=yaml.safe_dump(chart['values']),
wait=native_wait_enabled,
timeout=mock.ANY))
test_chart_override = chart.get('test')
expected_test_constructor_calls.append(
mock.call(
release_name,
m_tiller,
cg_test_charts=cg_test_all_charts,
test_values=test_chart_override))
any_order = not chart_group['sequenced']
# Verify that at least 1 release is either installed or updated.
self.assertTrue(
len(expected_install_release_calls) >= 1 or
len(expected_update_release_calls) >= 1)
# Verify that the expected number of non-deployed releases are
# installed with expected arguments.
self.assertEqual(
len(expected_install_release_calls),
m_tiller.install_release.call_count)
m_tiller.install_release.assert_has_calls(
expected_install_release_calls, any_order=any_order)
# Verify that the expected number of deployed releases are
# updated with expected arguments.
self.assertEqual(
len(expected_update_release_calls),
m_tiller.update_release.call_count)
m_tiller.update_release.assert_has_calls(
expected_update_release_calls, any_order=any_order)
# Verify that the expected number of deployed releases are
# uninstalled with expected arguments.
self.assertEqual(
len(expected_uninstall_release_calls),
m_tiller.uninstall_release.call_count)
m_tiller.uninstall_release.assert_has_calls(
expected_uninstall_release_calls, any_order=any_order)
# Verify that the expected number of deployed releases are
# tested with expected arguments.
self.assertEqual(
len(expected_test_constructor_calls), mock_test.call_count)
mock_test.assert_has_calls(
expected_test_constructor_calls, any_order=True)
_do_test()
def _get_chart_by_name(self, name):
name = name.split('armada-')[-1]
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
return [
c for c in yaml_documents if c['data'].get('chart_name') == name
][0]
def get_mock_release(self, name, status, last_test_results=None):
status_mock = mock.Mock()
status_mock.return_value = status
chart = self._get_chart_by_name(name)
def get_test_result(success):
status = (TESTRUN_STATUS_SUCCESS
if success else TESTRUN_STATUS_FAILURE)
return mock.Mock(status=status)
last_test_suite_run = None
if last_test_results is not None:
results = [get_test_result(r) for r in last_test_results]
last_test_suite_run = mock.Mock(results=results)
def has_last_test(name):
if name == 'last_test_suite_run':
return last_test_results is not None
self.fail('Called HasField() with unexpected field')
mock_release = mock.Mock(
version=1,
chart=chart,
config=mock.Mock(raw="{}"),
info=mock.Mock(
status=mock.Mock(
Code=mock.MagicMock(Name=status_mock),
HasField=mock.MagicMock(side_effect=has_last_test),
last_test_suite_run=last_test_suite_run)))
mock_release.name = name
return mock_release
def test_armada_sync_with_no_deployed_releases(self):
known_releases = []
self._test_sync(known_releases)
def test_armada_sync_with_one_deployed_release(self):
c1 = 'armada-test_chart_1'
known_releases = [self.get_mock_release(c1, const.STATUS_DEPLOYED)]
self._test_sync(known_releases)
def test_armada_sync_with_one_deployed_release_no_diff(self):
c1 = 'armada-test_chart_1'
known_releases = [self.get_mock_release(c1, const.STATUS_DEPLOYED)]
self._test_sync(known_releases, diff=set())
def test_armada_sync_with_failed_test_result(self):
c1 = 'armada-test_chart_1'
known_releases = [
self.get_mock_release(
c1, const.STATUS_DEPLOYED, last_test_results=[False])
]
self._test_sync(
known_releases, diff=set(), expected_last_test_result=False)
def test_armada_sync_with_success_test_result(self):
c1 = 'armada-test_chart_1'
known_releases = [
self.get_mock_release(
c1, const.STATUS_DEPLOYED, last_test_results=[True])
]
self._test_sync(
known_releases, diff=set(), expected_last_test_result=True)
def test_armada_sync_with_success_test_result_no_tests(self):
c1 = 'armada-test_chart_1'
known_releases = [
self.get_mock_release(
c1, const.STATUS_DEPLOYED, last_test_results=[])
]
self._test_sync(
known_releases, diff=set(), expected_last_test_result=True)
def test_armada_sync_with_both_deployed_releases(self):
c1 = 'armada-test_chart_1'
c2 = 'armada-test_chart_2'
known_releases = [
self.get_mock_release(c1, const.STATUS_DEPLOYED),
self.get_mock_release(c2, const.STATUS_DEPLOYED)
]
self._test_sync(known_releases)
def test_armada_sync_with_unprotected_releases(self):
c1 = 'armada-test_chart_1'
known_releases = [self.get_mock_release(c1, const.STATUS_FAILED)]
self._test_sync(known_releases)
def test_armada_sync_with_protected_releases_continue(self):
c1 = 'armada-test_chart_1'
c2 = 'armada-test_chart_2'
known_releases = [
self.get_mock_release(c2, const.STATUS_FAILED),
self.get_mock_release(c1, const.STATUS_FAILED)
]
self._test_sync(known_releases)
def test_armada_sync_with_protected_releases_halt(self):
c3 = 'armada-test_chart_3'
known_releases = [self.get_mock_release(c3, const.STATUS_FAILED)]
def _test_method():
self._test_sync(known_releases)
self.assertRaises(ChartDeployException, _test_method)
def test_armada_sync_test_failure(self):
def _test_method():
self._test_sync([], test_success=False)
self.assertRaises(ChartDeployException, _test_method)
def test_armada_sync_test_failure_to_run(self):
def _test_method():
self._test_sync([], test_failure_to_run=True)
self.assertRaises(ChartDeployException, _test_method)
class ArmadaNegativeHandlerTestCase(base.ArmadaTestCase):
@mock.patch.object(armada, 'source')
def test_armada_get_manifest_exception(self, mock_source):
"""Test armada handling with invalid manifest."""
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
error_re = ('Documents must be a list of documents with at least one '
'of each of the following schemas: .*')
self.assertRaisesRegexp(ManifestException, error_re, armada.Armada,
yaml_documents[:1], mock.MagicMock())
@mock.patch.object(armada, 'source')
def test_armada_override_exception(self, mock_source):
"""Test Armada checks with invalid chart override."""
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
override = ('chart:example-chart-2:name=' 'overridden', )
error_re = ('is not a valid override statement')
with self.assertRaisesRegexp(InvalidOverrideValueException, error_re):
armada.Armada(yaml_documents, mock.MagicMock(), set_ovr=override)
@mock.patch.object(armada, 'source')
def test_armada_manifest_exception_override_none(self, mock_source):
"""Test Armada checks with invalid manifest."""
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
example_document = [
d for d in yaml_documents
if d['metadata']['name'] == 'example-chart-4'
][0]
del example_document['data']['release']
error_re = ('Invalid document .*')
with self.assertRaisesRegexp(InvalidManifestException, error_re):
armada.Armada(yaml_documents, mock.MagicMock(), set_ovr=None)