Merge "Move to semantic diffing of charts"

This commit is contained in:
Zuul 2018-08-21 18:47:08 +00:00 committed by Gerrit Code Review
commit e4a270b06d
4 changed files with 141 additions and 84 deletions

View File

@ -40,3 +40,29 @@ class ProtectedReleaseException(ArmadaException):
'Armada encountered protected release %s in FAILED status' % 'Armada encountered protected release %s in FAILED status' %
reason) reason)
super(ProtectedReleaseException, self).__init__(self._message) super(ProtectedReleaseException, self).__init__(self._message)
class InvalidValuesYamlException(ArmadaException):
'''
Exception that occurs when Armada encounters invalid values.yaml content in
a helm chart.
'''
def __init__(self, chart_description):
self._message = (
'Armada encountered invalid values.yaml in helm chart: %s' %
chart_description)
super(InvalidValuesYamlException, self).__init__(self._message)
class InvalidOverrideValuesYamlException(ArmadaException):
'''
Exception that occurs when Armada encounters invalid override yaml in
helm chart.
'''
def __init__(self, chart_description):
self._message = (
'Armada encountered invalid values.yaml in helm chart: %s' %
chart_description)
super(InvalidValuesYamlException, self).__init__(self._message)

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import difflib from deepdiff import DeepDiff
import functools import functools
import time import time
import yaml import yaml
@ -370,7 +370,7 @@ class Armada(object):
'cleanup', False) 'cleanup', False)
chartbuilder = ChartBuilder(chart) chartbuilder = ChartBuilder(chart)
protoc_chart = chartbuilder.get_helm_chart() new_chart = chartbuilder.get_helm_chart()
# Begin Chart timeout deadline # Begin Chart timeout deadline
deadline = time.time() + wait_timeout deadline = time.time() + wait_timeout
@ -385,7 +385,7 @@ class Armada(object):
release_name, namespace) release_name, namespace)
# extract the installed chart and installed values from the # extract the installed chart and installed values from the
# latest release so we can compare to the intended state # latest release so we can compare to the intended state
apply_chart, apply_values = self.find_release_chart( old_chart, old_values_string = self.find_release_chart(
deployed_releases, release_name) deployed_releases, release_name)
upgrade = chart.get('upgrade', {}) upgrade = chart.get('upgrade', {})
@ -404,20 +404,26 @@ class Armada(object):
if not self.disable_update_post and upgrade_post: if not self.disable_update_post and upgrade_post:
post_actions = upgrade_post post_actions = upgrade_post
# Show delta for both the chart templates and the chart try:
# values old_values = yaml.safe_load(old_values_string)
# TODO(alanmeadows) account for .files differences except yaml.YAMLError:
# once we support those chart_desc = '{} (previously deployed)'.format(
LOG.info('Checking upgrade chart diffs.') old_chart.metadata.name)
upgrade_diff = self.show_diff(chart, apply_chart, raise armada_exceptions.\
apply_values, InvalidOverrideValuesYamlException(chart_desc)
chartbuilder.dump(), values,
msg)
if not upgrade_diff: LOG.info('Checking for updates to chart release inputs.')
LOG.info("There are no updates found in this chart") diff = self.get_diff(old_chart, old_values, new_chart,
values)
if not diff:
LOG.info("Found no updates to chart release inputs")
continue continue
LOG.info("Found updates to chart release inputs")
LOG.debug("%s", diff)
msg['diff'].append({chart['release']: str(diff)})
# TODO(MarshM): Add tiller dry-run before upgrade and # TODO(MarshM): Add tiller dry-run before upgrade and
# consider deadline impacts # consider deadline impacts
@ -426,7 +432,7 @@ class Armada(object):
LOG.info('Beginning Upgrade, wait=%s, timeout=%ss', LOG.info('Beginning Upgrade, wait=%s, timeout=%ss',
this_chart_should_wait, timer) this_chart_should_wait, timer)
tiller_result = self.tiller.update_release( tiller_result = self.tiller.update_release(
protoc_chart, new_chart,
release_name, release_name,
namespace, namespace,
pre_actions=pre_actions, pre_actions=pre_actions,
@ -458,7 +464,7 @@ class Armada(object):
LOG.info('Beginning Install, wait=%s, timeout=%ss', LOG.info('Beginning Install, wait=%s, timeout=%ss',
this_chart_should_wait, timer) this_chart_should_wait, timer)
tiller_result = self.tiller.install_release( tiller_result = self.tiller.install_release(
protoc_chart, new_chart,
release_name, release_name,
namespace, namespace,
values=yaml.safe_dump(values), values=yaml.safe_dump(values),
@ -584,49 +590,65 @@ class Armada(object):
LOG.info("Test failed for release: %s", release_name) LOG.info("Test failed for release: %s", release_name)
raise tiller_exceptions.TestFailedException(release_name) raise tiller_exceptions.TestFailedException(release_name)
def show_diff(self, chart, installed_chart, installed_values, target_chart, def get_diff(self, old_chart, old_values, new_chart, new_values):
target_values, msg): '''
'''Produce a unified diff of the installed chart vs our intention''' Get the diff between old and new chart release inputs to determine
whether an upgrade is needed.
# TODO(MarshM) This gives decent output comparing values. Would be Release inputs which are relevant are the override values given, and
# nice to clean it up further. Are \\n or \n\n ever valid diffs? the chart content including:
# Can these be cleanly converted to dicts, for easier compare?
def _sanitize_diff_str(str):
return str.replace('\\n', '\n').replace('\n\n', '\n').split('\n')
source = _sanitize_diff_str(str(installed_chart.SerializeToString())) * default values (values.yaml),
target = _sanitize_diff_str(str(target_chart)) * templates and their content
chart_diff = list(difflib.unified_diff(source, target, n=0)) * files and their content
* the above for each chart on which the chart depends transitively.
chart_release = chart.get('release', None) This excludes Chart.yaml content as that is rarely used by the chart
via ``{{ .Chart }}``, and even when it is does not usually necessitate
an upgrade.
if len(chart_diff) > 0: :param old_chart: The deployed chart.
LOG.info("Found diff in Chart (%s)", chart_release) :type old_chart: Chart
diff_msg = [] :param old_values: The deployed chart override values.
for line in chart_diff: :type old_values: dict
diff_msg.append(line) :param new_chart: The chart to deploy.
msg['diff'].append({'chart': diff_msg}) :type new_chart: Chart
:param new_values: The chart override values to deploy.
:type new_values: dict
:return: Mapping of difference types to sets of those differences.
:rtype: dict
'''
pretty_diff = '\n'.join(diff_msg) def make_release_input(chart, values, desc):
LOG.debug(pretty_diff) # TODO(seaneagan): Should we include `chart.metadata` (Chart.yaml)?
try:
default_values = yaml.safe_load(chart.values.raw)
except yaml.YAMLError:
chart_desc = '{} ({})'.format(chart.metadata.name, desc)
raise armada_exceptions.InvalidValuesYamlException(chart_desc)
files = {f.type_url: f.value for f in chart.files}
templates = {t.name: t.data for t in chart.templates}
dependencies = {
d.metadata.name: make_release_input(d)
for d in chart.dependencies
}
source = _sanitize_diff_str(installed_values) return {
target = _sanitize_diff_str(yaml.safe_dump(target_values)) 'chart': {
values_diff = list(difflib.unified_diff(source, target, n=0)) 'values': default_values,
'files': files,
'templates': templates,
'dependencies': dependencies
},
'values': values
}
if len(values_diff) > 0: old_input = make_release_input(old_chart, old_values,
LOG.info("Found diff in values (%s)", chart_release) 'previously deployed')
diff_msg = [] new_input = make_release_input(new_chart, new_values,
for line in values_diff: 'currently being deployed')
diff_msg.append(line)
msg['diff'].append({'values': diff_msg})
pretty_diff = '\n'.join(diff_msg) return DeepDiff(old_input, new_input, view='tree')
LOG.debug(pretty_diff)
result = (len(chart_diff) > 0) or (len(values_diff) > 0)
return result
def _chart_cleanup(self, prefix, charts, msg): def _chart_cleanup(self, prefix, charts, msg):
LOG.info('Processing chart cleanup to remove unspecified releases.') LOG.info('Processing chart cleanup to remove unspecified releases.')

View File

@ -320,7 +320,8 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
def _test_sync(self, def _test_sync(self,
known_releases, known_releases,
test_success=True, test_success=True,
test_failure_to_run=False): test_failure_to_run=False,
diff={'some_key': {'some diff'}}):
"""Test install functionality from the sync() method.""" """Test install functionality from the sync() method."""
@mock.patch.object(armada.Armada, 'post_flight_ops') @mock.patch.object(armada.Armada, 'post_flight_ops')
@ -333,7 +334,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
# Instantiate Armada object. # Instantiate Armada object.
yaml_documents = list(yaml.safe_load_all(TEST_YAML)) yaml_documents = list(yaml.safe_load_all(TEST_YAML))
armada_obj = armada.Armada(yaml_documents) armada_obj = armada.Armada(yaml_documents)
armada_obj.show_diff = mock.Mock() armada_obj.get_diff = mock.Mock()
chart_group = armada_obj.manifest['armada']['chart_groups'][0] chart_group = armada_obj.manifest['armada']['chart_groups'][0]
charts = chart_group['chart_group'] charts = chart_group['chart_group']
@ -358,6 +359,9 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
mock_chartbuilder.get_source_path.return_value = None mock_chartbuilder.get_source_path.return_value = None
mock_chartbuilder.get_helm_chart.return_value = None mock_chartbuilder.get_helm_chart.return_value = None
# Simulate chart diff, upgrade should only happen if non-empty.
armada_obj.get_diff.return_value = diff
armada_obj.sync() armada_obj.sync()
expected_install_release_calls = [] expected_install_release_calls = []
@ -374,6 +378,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
# multiple conditions, so this is enough. # multiple conditions, so this is enough.
this_chart_should_wait = chart['wait']['timeout'] > 0 this_chart_should_wait = chart['wait']['timeout'] > 0
expected_apply = True
if release_name not in [x[0] for x in known_releases]: if release_name not in [x[0] for x in known_releases]:
expected_install_release_calls.append( expected_install_release_calls.append(
mock.call( mock.call(
@ -418,26 +423,31 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
break break
if status == const.STATUS_DEPLOYED: if status == const.STATUS_DEPLOYED:
upgrade = chart.get('upgrade', {}) if not diff:
disable_hooks = upgrade.get('no_hooks', False) expected_apply = False
force = upgrade.get('force', False) else:
recreate_pods = upgrade.get('recreate_pods', False) 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( expected_update_release_calls.append(
mock.call( mock.call(
mock_chartbuilder().get_helm_chart(), mock_chartbuilder().get_helm_chart(),
"{}-{}".format( "{}-{}".format(
armada_obj.manifest['armada'] armada_obj.manifest['armada'][
['release_prefix'], chart['release']), 'release_prefix'],
chart['namespace'], chart['release']),
pre_actions={}, chart['namespace'],
post_actions={}, pre_actions={},
disable_hooks=disable_hooks, post_actions={},
force=force, disable_hooks=disable_hooks,
recreate_pods=recreate_pods, force=force,
values=yaml.safe_dump(chart['values']), recreate_pods=recreate_pods,
wait=this_chart_should_wait, values=yaml.safe_dump(chart['values']),
timeout=chart['wait']['timeout'])) wait=this_chart_should_wait,
timeout=chart['wait']['timeout']))
test_chart_override = chart.get('test') test_chart_override = chart.get('test')
# Use old default value when not using newer `test` key # Use old default value when not using newer `test` key
@ -451,7 +461,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
test_cleanup = test_chart_override.get('options', {}).get( test_cleanup = test_chart_override.get('options', {}).get(
'cleanup', False) 'cleanup', False)
if test_this_chart: if test_this_chart and expected_apply:
expected_test_release_for_success_calls.append( expected_test_release_for_success_calls.append(
mock.call( mock.call(
m_tiller, m_tiller,
@ -508,23 +518,21 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
def test_armada_sync_with_one_deployed_release(self): def test_armada_sync_with_one_deployed_release(self):
c1 = 'armada-test_chart_1' c1 = 'armada-test_chart_1'
known_releases = [[ known_releases = [[c1, None, None, "{}", const.STATUS_DEPLOYED]]
c1, None,
self._get_chart_by_name(c1), None, const.STATUS_DEPLOYED
]]
self._test_sync(known_releases) self._test_sync(known_releases)
def test_armada_sync_with_one_deployed_release_no_diff(self):
c1 = 'armada-test_chart_1'
known_releases = [[c1, None, None, "{}", const.STATUS_DEPLOYED]]
self._test_sync(known_releases, diff=set())
def test_armada_sync_with_both_deployed_releases(self): def test_armada_sync_with_both_deployed_releases(self):
c1 = 'armada-test_chart_1' c1 = 'armada-test_chart_1'
c2 = 'armada-test_chart_2' c2 = 'armada-test_chart_2'
known_releases = [[ known_releases = [[c1, None, None, "{}", const.STATUS_DEPLOYED],
c1, None, [c2, None, None, "{}", const.STATUS_DEPLOYED]]
self._get_chart_by_name(c1), None, const.STATUS_DEPLOYED
], [
c2, None,
self._get_chart_by_name(c2), None, const.STATUS_DEPLOYED
]]
self._test_sync(known_releases) self._test_sync(known_releases)
def test_armada_sync_with_unprotected_releases(self): def test_armada_sync_with_unprotected_releases(self):

View File

@ -1,3 +1,4 @@
deepdiff==3.3.0
gitpython gitpython
grpcio==1.10.0 grpcio==1.10.0
grpcio-tools==1.10.0 grpcio-tools==1.10.0