diff --git a/moto/batch/responses.py b/moto/batch/responses.py
index e626b7d4c..7fb606184 100644
--- a/moto/batch/responses.py
+++ b/moto/batch/responses.py
@@ -27,7 +27,7 @@ class BatchResponse(BaseResponse):
elif not hasattr(self, '_json'):
try:
self._json = json.loads(self.body)
- except json.JSONDecodeError:
+ except ValueError:
print()
return self._json
diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py
index e5ab7255d..6ec821b42 100644
--- a/moto/cloudformation/models.py
+++ b/moto/cloudformation/models.py
@@ -13,6 +13,7 @@ from .utils import (
generate_changeset_id,
generate_stack_id,
yaml_tag_constructor,
+ validate_template_cfn_lint,
)
from .exceptions import ValidationError
@@ -270,6 +271,9 @@ class CloudFormationBackend(BaseBackend):
next_token = str(token + 100) if len(all_exports) > token + 100 else None
return exports, next_token
+ def validate_template(self, template):
+ return validate_template_cfn_lint(template)
+
def _validate_export_uniqueness(self, stack):
new_stack_export_names = [x.name for x in stack.exports]
export_names = self.exports.keys()
diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py
index fe436939f..9e67e931a 100644
--- a/moto/cloudformation/responses.py
+++ b/moto/cloudformation/responses.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import json
+import yaml
from six.moves.urllib.parse import urlparse
from moto.core.responses import BaseResponse
@@ -295,6 +296,32 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(LIST_EXPORTS_RESPONSE)
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 = """
+
+
+
+
+{{ description }}
+
+
+"""
CREATE_STACK_RESPONSE_TEMPLATE = """
diff --git a/moto/cloudformation/utils.py b/moto/cloudformation/utils.py
index f3b8874ed..f963ce7c8 100644
--- a/moto/cloudformation/utils.py
+++ b/moto/cloudformation/utils.py
@@ -3,6 +3,9 @@ import uuid
import six
import random
import yaml
+import os
+
+from cfnlint import decode, core
def generate_stack_id(stack_name):
@@ -38,3 +41,33 @@ def yaml_tag_constructor(loader, tag, node):
key = 'Fn::{}'.format(tag[1:])
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
diff --git a/setup.py b/setup.py
index d1717cd13..ce4fe27fa 100755
--- a/setup.py
+++ b/setup.py
@@ -37,6 +37,7 @@ install_requires = [
"jsondiff==1.1.2",
"aws-xray-sdk!=0.96,>=0.93",
"responses>=0.9.0",
+ "cfn-lint"
]
extras_require = {
diff --git a/tests/test_cloudformation/test_validate.py b/tests/test_cloudformation/test_validate.py
new file mode 100644
index 000000000..e2c3af05d
--- /dev/null
+++ b/tests/test_cloudformation/test_validate.py
@@ -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