Add caching and cleanup of chart tarballs

Caching and cleanup of git repository chart sources was previously
implemented. This adds these features for tarball sources as well.

This also implements transitive chart dependency sourcing. Previously
only a single level of dependencies were being downloaded, which
would lead to an error when multiple dependency levels exist.

Change-Id: I988e473a6ea29331e036d26c3ec7269374e0188f
This commit is contained in:
Sean Eagan 2018-10-25 15:08:21 -05:00
parent b170daeea7
commit cbb8ed33e1
4 changed files with 41 additions and 65 deletions

View File

@ -103,7 +103,7 @@ class Armada(object):
raise raise
self.manifest = Manifest( self.manifest = Manifest(
self.documents, target_manifest=target_manifest).get_manifest() self.documents, target_manifest=target_manifest).get_manifest()
self.cloned_dirs = set() self.chart_cache = {}
self.chart_deploy = ChartDeploy( self.chart_deploy = ChartDeploy(
disable_update_pre, disable_update_post, self.dry_run, disable_update_pre, disable_update_post, self.dry_run,
k8s_wait_attempts, k8s_wait_attempt_sleep, timeout, self.tiller) k8s_wait_attempts, k8s_wait_attempt_sleep, timeout, self.tiller)
@ -119,16 +119,12 @@ class Armada(object):
raise tiller_exceptions.TillerServicesUnavailableException() raise tiller_exceptions.TillerServicesUnavailableException()
# Clone the chart sources # Clone the chart sources
repos = {}
manifest_data = self.manifest.get(const.KEYWORD_ARMADA, {}) manifest_data = self.manifest.get(const.KEYWORD_ARMADA, {})
for group in manifest_data.get(const.KEYWORD_GROUPS, []): for group in manifest_data.get(const.KEYWORD_GROUPS, []):
for ch in group.get(const.KEYWORD_CHARTS, []): for ch in group.get(const.KEYWORD_CHARTS, []):
self.tag_cloned_repo(ch, repos) self.get_chart(ch)
for dep in ch.get('chart', {}).get('dependencies', []): def get_chart(self, ch):
self.tag_cloned_repo(dep, repos)
def tag_cloned_repo(self, ch, repos):
chart = ch.get('chart', {}) chart = ch.get('chart', {})
chart_source = chart.get('source', {}) chart_source = chart.get('source', {})
location = chart_source.get('location') location = chart_source.get('location')
@ -138,25 +134,30 @@ class Armada(object):
if ct_type == 'local': if ct_type == 'local':
chart['source_dir'] = (location, subpath) chart['source_dir'] = (location, subpath)
elif ct_type == 'tar': elif ct_type == 'tar':
LOG.info('Downloading tarball from: %s', location) source_key = (ct_type, location)
if not CONF.certs: if source_key not in self.chart_cache:
LOG.warn('Disabling server validation certs to extract charts') LOG.info('Downloading tarball from: %s', location)
tarball_dir = source.get_tarball(location, verify=False)
else:
tarball_dir = source.get_tarball(location, verify=CONF.cert)
chart['source_dir'] = (tarball_dir, subpath) if not CONF.certs:
LOG.warn(
'Disabling server validation certs to extract charts')
tarball_dir = source.get_tarball(location, verify=False)
else:
tarball_dir = source.get_tarball(
location, verify=CONF.cert)
self.chart_cache[source_key] = tarball_dir
chart['source_dir'] = (self.chart_cache.get(source_key), subpath)
elif ct_type == 'git': elif ct_type == 'git':
reference = chart_source.get('reference', 'master') reference = chart_source.get('reference', 'master')
repo_branch = (location, reference) source_key = (ct_type, location, reference)
if repo_branch not in repos: if source_key not in self.chart_cache:
auth_method = chart_source.get('auth_method') auth_method = chart_source.get('auth_method')
proxy_server = chart_source.get('proxy_server') proxy_server = chart_source.get('proxy_server')
logstr = 'Cloning repo: {} from branch: {}'.format( logstr = 'Cloning repo: {} from branch: {}'.format(
*repo_branch) location, reference)
if proxy_server: if proxy_server:
logstr += ' proxy: {}'.format(proxy_server) logstr += ' proxy: {}'.format(proxy_server)
if auth_method: if auth_method:
@ -164,19 +165,20 @@ class Armada(object):
LOG.info(logstr) LOG.info(logstr)
repo_dir = source.git_clone( repo_dir = source.git_clone(
*repo_branch, location,
reference,
proxy_server=proxy_server, proxy_server=proxy_server,
auth_method=auth_method) auth_method=auth_method)
self.cloned_dirs.add(repo_dir)
repos[repo_branch] = repo_dir self.chart_cache[source_key] = repo_dir
chart['source_dir'] = (repo_dir, subpath) chart['source_dir'] = (self.chart_cache.get(source_key), subpath)
else:
chart['source_dir'] = (repos.get(repo_branch), subpath)
else: else:
chart_name = chart.get('chart_name') chart_name = chart.get('chart_name')
raise source_exceptions.ChartSourceException(ct_type, chart_name) raise source_exceptions.ChartSourceException(ct_type, chart_name)
for dep in ch.get('chart', {}).get('dependencies', []):
self.get_chart(dep)
def sync(self): def sync(self):
''' '''
Synchronize Helm with the Armada Config(s) Synchronize Helm with the Armada Config(s)
@ -293,9 +295,9 @@ class Armada(object):
LOG.info("Performing post-flight operations.") LOG.info("Performing post-flight operations.")
# Delete temp dirs used for deployment # Delete temp dirs used for deployment
for cloned_dir in self.cloned_dirs: for chart_dir in self.chart_cache.values():
LOG.debug('Removing cloned temp directory: %s', cloned_dir) LOG.debug('Removing temp chart directory: %s', chart_dir)
source.source_cleanup(cloned_dir) source.source_cleanup(chart_dir)
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

@ -151,6 +151,7 @@ CHART_SOURCES = [('git://github.com/dummy/armada', 'chart_1'),
('/tmp/dummy/armada', 'chart_4')] ('/tmp/dummy/armada', 'chart_4')]
# TODO(seaneagan): Add unit tests with dependencies, including transitive.
class ArmadaHandlerTestCase(base.ArmadaTestCase): class ArmadaHandlerTestCase(base.ArmadaTestCase):
def _test_pre_flight_ops(self, armada_obj): def _test_pre_flight_ops(self, armada_obj):

View File

@ -15,7 +15,6 @@
import os import os
import shutil import shutil
import fixtures
import mock import mock
import testtools import testtools
@ -156,23 +155,6 @@ class GitTestCase(base.ArmadaTestCase):
source.source_cleanup(git_path) source.source_cleanup(git_path)
mock_log.warning.assert_not_called() mock_log.warning.assert_not_called()
@test_utils.attr(type=['negative'])
@mock.patch.object(source, 'LOG')
@mock.patch('armada.utils.source.shutil')
def test_source_cleanup_fake_git_path(self, mock_shutil, mock_log):
# Verify that passing in a fake path does nothing but log a warning.
# Don't want anyone using the function to delete random directories.
fake_path = self.useFixture(fixtures.TempDir()).path
self.addCleanup(shutil.rmtree, fake_path)
source.source_cleanup(fake_path)
mock_shutil.rmtree.assert_not_called()
self.assertTrue(mock_log.warning.called)
actual_call = mock_log.warning.mock_calls[0][1][:2]
self.assertEqual(
('%s is not a valid git repository. Details: %s', fake_path),
actual_call)
@test_utils.attr(type=['negative']) @test_utils.attr(type=['negative'])
@mock.patch.object(source, 'LOG') @mock.patch.object(source, 'LOG')
@mock.patch('armada.utils.source.shutil') @mock.patch('armada.utils.source.shutil')
@ -187,9 +169,8 @@ class GitTestCase(base.ArmadaTestCase):
mock_shutil.rmtree.assert_not_called() mock_shutil.rmtree.assert_not_called()
self.assertTrue(mock_log.warning.called) self.assertTrue(mock_log.warning.called)
actual_call = mock_log.warning.mock_calls[0][1] actual_call = mock_log.warning.mock_calls[0][1]
self.assertEqual( self.assertEqual(('Could not find the chart path %s to delete.', path),
('Could not delete the path %s. Is it a git repository?', path), actual_call)
actual_call)
@testtools.skipUnless(base.is_connected(), @testtools.skipUnless(base.is_connected(),
'git clone requires network connectivity.') 'git clone requires network connectivity.')

View File

@ -153,27 +153,19 @@ def extract_tarball(tarball_path):
return temp_dir return temp_dir
def source_cleanup(git_path): def source_cleanup(chart_path):
'''Clean up the git repository that was created by ``git_clone`` above. '''Clean up the chart path that was created above.
Removes the ``git_path`` repository and all associated files if they Removes the ``chart_path`` and all associated files if they
exist. exist.
:param str git_path: The git repository to delete. :param str chart_path: The chart path to delete.
''' '''
if os.path.exists(git_path): if os.path.exists(chart_path):
try: try:
# Internally validates whether the path points to an actual repo. shutil.rmtree(chart_path)
Repo(git_path) except OSError as e:
except git_exc.InvalidGitRepositoryError as e: LOG.warning('Could not delete the path %s. Details: %s',
LOG.warning('%s is not a valid git repository. Details: %s', chart_path, e)
git_path, e)
else:
try:
shutil.rmtree(git_path)
except OSError as e:
LOG.warning('Could not delete the path %s. Details: %s',
git_path, e)
else: else:
LOG.warning('Could not delete the path %s. Is it a git repository?', LOG.warning('Could not find the chart path %s to delete.', chart_path)
git_path)