Adding cloudformation-validate. Cfn-lint does the heavy lifting.
This commit is contained in:
parent
71a054af92
commit
b66965e6e8
@ -27,7 +27,7 @@ class BatchResponse(BaseResponse):
|
|||||||
elif not hasattr(self, '_json'):
|
elif not hasattr(self, '_json'):
|
||||||
try:
|
try:
|
||||||
self._json = json.loads(self.body)
|
self._json = json.loads(self.body)
|
||||||
except json.JSONDecodeError:
|
except ValueError:
|
||||||
print()
|
print()
|
||||||
return self._json
|
return self._json
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ from .utils import (
|
|||||||
generate_changeset_id,
|
generate_changeset_id,
|
||||||
generate_stack_id,
|
generate_stack_id,
|
||||||
yaml_tag_constructor,
|
yaml_tag_constructor,
|
||||||
|
validate_template_cfn_lint,
|
||||||
)
|
)
|
||||||
from .exceptions import ValidationError
|
from .exceptions import ValidationError
|
||||||
|
|
||||||
@ -270,6 +271,9 @@ class CloudFormationBackend(BaseBackend):
|
|||||||
next_token = str(token + 100) if len(all_exports) > token + 100 else None
|
next_token = str(token + 100) if len(all_exports) > token + 100 else None
|
||||||
return exports, next_token
|
return exports, next_token
|
||||||
|
|
||||||
|
def validate_template(self, template):
|
||||||
|
return validate_template_cfn_lint(template)
|
||||||
|
|
||||||
def _validate_export_uniqueness(self, stack):
|
def _validate_export_uniqueness(self, stack):
|
||||||
new_stack_export_names = [x.name for x in stack.exports]
|
new_stack_export_names = [x.name for x in stack.exports]
|
||||||
export_names = self.exports.keys()
|
export_names = self.exports.keys()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import yaml
|
||||||
from six.moves.urllib.parse import urlparse
|
from six.moves.urllib.parse import urlparse
|
||||||
|
|
||||||
from moto.core.responses import BaseResponse
|
from moto.core.responses import BaseResponse
|
||||||
@ -294,6 +295,32 @@ class CloudFormationResponse(BaseResponse):
|
|||||||
template = self.response_template(LIST_EXPORTS_RESPONSE)
|
template = self.response_template(LIST_EXPORTS_RESPONSE)
|
||||||
return template.render(exports=exports, next_token=next_token)
|
return template.render(exports=exports, next_token=next_token)
|
||||||
|
|
||||||
|
def validate_template(self):
|
||||||
|
cfn_lint = self.cloudformation_backend.validate_template(self._get_param('TemplateBody'))
|
||||||
|
if cfn_lint:
|
||||||
|
raise ValidationError(cfn_lint[0].message)
|
||||||
|
description = ""
|
||||||
|
try:
|
||||||
|
description = json.loads(self._get_param('TemplateBody'))['Description']
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
description = yaml.load(self._get_param('TemplateBody'))['Description']
|
||||||
|
except (yaml.ParserError, KeyError):
|
||||||
|
pass
|
||||||
|
template = self.response_template(VALIDATE_STACK_RESPONSE_TEMPLATE)
|
||||||
|
return template.render(description=description)
|
||||||
|
|
||||||
|
|
||||||
|
VALIDATE_STACK_RESPONSE_TEMPLATE = """<ValidateTemplateResponse>
|
||||||
|
<ValidateTemplateResult>
|
||||||
|
<Capabilities></Capabilities>
|
||||||
|
<CapabilitiesReason></CapabilitiesReason>
|
||||||
|
<DeclaredTransforms></DeclaredTransforms>
|
||||||
|
<Description>{{ description }}</Description>
|
||||||
|
<Parameters></Parameters>
|
||||||
|
</ValidateTemplateResult>
|
||||||
|
</ValidateTemplateResponse>"""
|
||||||
|
|
||||||
CREATE_STACK_RESPONSE_TEMPLATE = """<CreateStackResponse>
|
CREATE_STACK_RESPONSE_TEMPLATE = """<CreateStackResponse>
|
||||||
<CreateStackResult>
|
<CreateStackResult>
|
||||||
|
@ -3,6 +3,9 @@ import uuid
|
|||||||
import six
|
import six
|
||||||
import random
|
import random
|
||||||
import yaml
|
import yaml
|
||||||
|
import os
|
||||||
|
|
||||||
|
from cfnlint import decode, core
|
||||||
|
|
||||||
|
|
||||||
def generate_stack_id(stack_name):
|
def generate_stack_id(stack_name):
|
||||||
@ -38,3 +41,33 @@ def yaml_tag_constructor(loader, tag, node):
|
|||||||
key = 'Fn::{}'.format(tag[1:])
|
key = 'Fn::{}'.format(tag[1:])
|
||||||
|
|
||||||
return {key: _f(loader, tag, node)}
|
return {key: _f(loader, tag, node)}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_template_cfn_lint(template):
|
||||||
|
|
||||||
|
# Save the template to a temporary file -- cfn-lint requires a file
|
||||||
|
filename = "file.tmp"
|
||||||
|
with open(filename, "w") as file:
|
||||||
|
file.write(template)
|
||||||
|
abs_filename = os.path.abspath(filename)
|
||||||
|
|
||||||
|
# decode handles both yaml and json
|
||||||
|
template, matches = decode.decode(abs_filename, False)
|
||||||
|
|
||||||
|
# Set cfn-lint to info
|
||||||
|
core.configure_logging(None)
|
||||||
|
|
||||||
|
# Initialize the ruleset to be applied (no overrules, no excludes)
|
||||||
|
rules = core.get_rules([], [], [])
|
||||||
|
|
||||||
|
# Use us-east-1 region (spec file) for validation
|
||||||
|
regions = ['us-east-1']
|
||||||
|
|
||||||
|
# Process all the rules and gather the errors
|
||||||
|
matches = core.run_checks(
|
||||||
|
abs_filename,
|
||||||
|
template,
|
||||||
|
rules,
|
||||||
|
regions)
|
||||||
|
|
||||||
|
return matches
|
||||||
|
1
setup.py
1
setup.py
@ -24,6 +24,7 @@ install_requires = [
|
|||||||
"jsondiff==1.1.1",
|
"jsondiff==1.1.1",
|
||||||
"aws-xray-sdk<0.96,>=0.93",
|
"aws-xray-sdk<0.96,>=0.93",
|
||||||
"responses>=0.9.0",
|
"responses>=0.9.0",
|
||||||
|
"cfn-lint"
|
||||||
]
|
]
|
||||||
|
|
||||||
extras_require = {
|
extras_require = {
|
||||||
|
115
tests/test_cloudformation/test_validate.py
Normal file
115
tests/test_cloudformation/test_validate.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
import boto3
|
||||||
|
from nose.tools import raises
|
||||||
|
import botocore
|
||||||
|
|
||||||
|
|
||||||
|
from moto.cloudformation.exceptions import ValidationError
|
||||||
|
from moto.cloudformation.models import FakeStack
|
||||||
|
from moto.cloudformation.parsing import resource_class_from_type, parse_condition, Export
|
||||||
|
from moto.sqs.models import Queue
|
||||||
|
from moto.s3.models import FakeBucket
|
||||||
|
from moto.cloudformation.utils import yaml_tag_constructor
|
||||||
|
from boto.cloudformation.stack import Output
|
||||||
|
from moto import mock_cloudformation, mock_s3, mock_sqs, mock_ec2
|
||||||
|
|
||||||
|
json_template = {
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Description": "Stack 1",
|
||||||
|
"Resources": {
|
||||||
|
"EC2Instance1": {
|
||||||
|
"Type": "AWS::EC2::Instance",
|
||||||
|
"Properties": {
|
||||||
|
"ImageId": "ami-d3adb33f",
|
||||||
|
"KeyName": "dummy",
|
||||||
|
"InstanceType": "t2.micro",
|
||||||
|
"Tags": [
|
||||||
|
{
|
||||||
|
"Key": "Description",
|
||||||
|
"Value": "Test tag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": "Name",
|
||||||
|
"Value": "Name tag for tests"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# One resource is required
|
||||||
|
json_bad_template = {
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Description": "Stack 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy_template_json = json.dumps(json_template)
|
||||||
|
dummy_bad_template_json = json.dumps(json_bad_template)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
def test_boto3_json_validate_successful():
|
||||||
|
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
|
||||||
|
response = cf_conn.validate_template(
|
||||||
|
TemplateBody=dummy_template_json,
|
||||||
|
)
|
||||||
|
assert response['Description'] == "Stack 1"
|
||||||
|
assert response['Parameters'] == []
|
||||||
|
assert response['ResponseMetadata']['HTTPStatusCode'] == 200
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
def test_boto3_json_invalid_missing_resource():
|
||||||
|
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
|
||||||
|
try:
|
||||||
|
cf_conn.validate_template(
|
||||||
|
TemplateBody=dummy_bad_template_json,
|
||||||
|
)
|
||||||
|
assert False
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
assert str(e) == 'An error occurred (ValidationError) when calling the ValidateTemplate operation: Stack' \
|
||||||
|
' with id Missing top level item Resources to file module does not exist'
|
||||||
|
assert True
|
||||||
|
|
||||||
|
|
||||||
|
yaml_template = """
|
||||||
|
AWSTemplateFormatVersion: '2010-09-09'
|
||||||
|
Description: Simple CloudFormation Test Template
|
||||||
|
Resources:
|
||||||
|
S3Bucket:
|
||||||
|
Type: AWS::S3::Bucket
|
||||||
|
Properties:
|
||||||
|
AccessControl: PublicRead
|
||||||
|
BucketName: cf-test-bucket-1
|
||||||
|
"""
|
||||||
|
|
||||||
|
yaml_bad_template = """
|
||||||
|
AWSTemplateFormatVersion: '2010-09-09'
|
||||||
|
Description: Simple CloudFormation Test Template
|
||||||
|
"""
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
def test_boto3_yaml_validate_successful():
|
||||||
|
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
|
||||||
|
response = cf_conn.validate_template(
|
||||||
|
TemplateBody=yaml_template,
|
||||||
|
)
|
||||||
|
assert response['Description'] == "Simple CloudFormation Test Template"
|
||||||
|
assert response['Parameters'] == []
|
||||||
|
assert response['ResponseMetadata']['HTTPStatusCode'] == 200
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
def test_boto3_yaml_invalid_missing_resource():
|
||||||
|
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
|
||||||
|
try:
|
||||||
|
cf_conn.validate_template(
|
||||||
|
TemplateBody=yaml_bad_template,
|
||||||
|
)
|
||||||
|
assert False
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
assert str(e) == 'An error occurred (ValidationError) when calling the ValidateTemplate operation: Stack' \
|
||||||
|
' with id Missing top level item Resources to file module does not exist'
|
||||||
|
assert True
|
Loading…
Reference in New Issue
Block a user