Add workaround for AWS S3 API, verify md5 hash, exit non-zero on validate failure

This commit is contained in:
Stefan Reimer 2021-03-11 19:25:02 +01:00
parent f6e3df67bb
commit 7a66cf3ec5
5 changed files with 35 additions and 3 deletions

View File

@ -1,5 +1,10 @@
# Changelog # Changelog
## 0.9.9
- Add workaround for inconsistent AWS API S3 GetBucketLocation
- validate now exits with non-zero exit code if valiation of any template failed
- the embedded md5 hash in templates are now verified reading the template
## 0.9.8 ## 0.9.8
- Remove support for FortyTwo legacy mode as AWS now behaves as it should - Remove support for FortyTwo legacy mode as AWS now behaves as it should
- Add support for embedded custom output yaml format and removed hardcoded kubezero output template - Add support for embedded custom output yaml format and removed hardcoded kubezero output template

View File

@ -2,7 +2,7 @@ import logging
__author__ = "Stefan Reimer" __author__ = "Stefan Reimer"
__email__ = "stefan@zero-downtimet.net" __email__ = "stefan@zero-downtimet.net"
__version__ = "0.9.8" __version__ = "0.9.9"
# Set up logging to ``/dev/null`` like a library is supposed to. # Set up logging to ``/dev/null`` like a library is supposed to.

View File

@ -78,7 +78,9 @@ def validate(cb, stack_names, multi):
stacks = _find_stacks(cb, stack_names, multi) stacks = _find_stacks(cb, stack_names, multi)
for s in stacks: for s in stacks:
s.validate() ret = s.validate()
if ret:
sys.exit(ret)
@click.command() @click.command()

View File

@ -12,3 +12,7 @@ class InvalidProjectDir(Exception):
class InvalidHook(Exception): class InvalidHook(Exception):
"""My documentation""" """My documentation"""
class ChecksumError(Exception):
"""My documentation"""

View File

@ -15,7 +15,7 @@ from .utils import dict_merge, search_refs, ensure_dir, get_s3_url
from .connection import BotoConnection from .connection import BotoConnection
from .jinja import JinjaEnv, read_config_file from .jinja import JinjaEnv, read_config_file
from . import __version__ from . import __version__
from .exceptions import ParameterNotFound, ParameterIllegalValue from .exceptions import ParameterNotFound, ParameterIllegalValue, ChecksumError
from .hooks import exec_hooks from .hooks import exec_hooks
import cfnlint.core import cfnlint.core
@ -207,6 +207,20 @@ class Stack(object):
except KeyError: except KeyError:
pass pass
# Get checksum
if not self.md5:
try:
self.md5 = self.cfn_data['Metadata']['Template']['Hash']
# Verify embedded md5 hash
source_cfn = re.sub('Hash: [0-9a-f]{32}', 'Hash: __HASH__', self.cfn_template)
our_md5 = hashlib.md5(source_cfn.encode('utf-8')).hexdigest()
if (our_md5 != self.md5):
raise ChecksumError("Template hash checksum mismatch! Expected: {} Got: {}".format(self.md5, our_md5)) from None
except KeyError:
raise ChecksumError("Template missing Hash checksum!") from None
# Add CloudBender dependencies # Add CloudBender dependencies
include = [] include = []
search_refs(self.cfn_data, include, self.mode) search_refs(self.cfn_data, include, self.mode)
@ -315,6 +329,7 @@ class Stack(object):
self.cfn_data = yaml.load(self.cfn_template, Loader=SafeLoaderIgnoreUnknown) self.cfn_data = yaml.load(self.cfn_template, Loader=SafeLoaderIgnoreUnknown)
self._parse_metadata() self._parse_metadata()
else: else:
logger.debug('Using cached cfn template %s.', self.stackname) logger.debug('Using cached cfn template %s.', self.stackname)
@ -356,8 +371,10 @@ class Stack(object):
if len(matches): if len(matches):
for match in matches: for match in matches:
logger.error(formatter._format(match)) logger.error(formatter._format(match))
return 1
else: else:
logger.info("Passed.") logger.info("Passed.")
return 0
def get_outputs(self, include='.*', values=False): def get_outputs(self, include='.*', values=False):
""" gets outputs of the stack """ """ gets outputs of the stack """
@ -766,6 +783,10 @@ class Stack(object):
# so we need the region, AWS as usual # so we need the region, AWS as usual
(bucket, path) = get_s3_url(self.template_bucket_url, self.rel_path, self.stackname + ".yaml") (bucket, path) = get_s3_url(self.template_bucket_url, self.rel_path, self.stackname + ".yaml")
bucket_region = self.connection_manager.call('s3', 'get_bucket_location', {'Bucket': bucket}, profile=self.profile, region=self.region)['LocationConstraint'] bucket_region = self.connection_manager.call('s3', 'get_bucket_location', {'Bucket': bucket}, profile=self.profile, region=self.region)['LocationConstraint']
# If bucket is in us-east-1 AWS returns 'none' cause reasons grrr
if not bucket_region:
bucket_region = 'us-east-1'
kwargs['TemplateURL'] = 'https://{}.s3.{}.amazonaws.com/{}'.format(bucket, bucket_region, path) kwargs['TemplateURL'] = 'https://{}.s3.{}.amazonaws.com/{}'.format(bucket, bucket_region, path)
else: else:
kwargs['TemplateBody'] = self.cfn_template kwargs['TemplateBody'] = self.cfn_template