Cloud-Radar
Write unit and functional tests for AWS Cloudformation.
Report Bug
·
Request Feature
·
Guide
Table of Contents
-
About The Project
-
Getting Started
- Usage
- Roadmap
- Contributing
- License
- Contact
- Acknowledgements
About The Project
Cloud-Radar is a python module that allows testing of Cloudformation Templates/Stacks using Python.
Unit Testing
You can now unit test the logic contained inside your Cloudformation template. Cloud-Radar takes your template, the desired region and some parameters. We render the template into its final state and pass it back to you.
You can Test:
- That Conditionals in your template evaluate to the correct value.
- Conditional resources were created or not.
- That resources have the correct properties.
- That resources are named as expected because of
!Sub
.
You can test all this locally without worrying about AWS Credentials.
A number of these tests can be configured in a common way to apply to all templates through the use of the hooks functionality.
Functional Testing
This project is a wrapper around Taskcat. Taskcat is a great tool for ensuring your Cloudformation Template can be deployed in multiple AWS Regions. Cloud-Radar enhances Taskcat by making it easier to write more complete functional tests.
Here's How:
- You can interact with the deployed resources directly with tools you already know like boto3.
- You can control the lifecycle of the stack. This allows testing if resources were retained after the stacks were deleted.
- You can run tests without hardcoding them in a taskcat config file.
This project is new and it's possible not all features or functionality of Taskcat/Cloudformation are supported (see Roadmap). If you find something missing or have a use case that isn't covered then please let me know =)
Built With
Getting Started
Cloud-Radar is available as an easy to install pip package.
Prerequisites
Cloud-Radar requires python >= 3.8
Installation
- Install with pip.
pip install cloud-radar
Usage
Unit Testing (Click to expand)
Using Cloud-Radar starts by importing it into your test file or framework. We will use this Template for an example shown below. More scenario based examples are currently being built up in the examples/unit directory of this project.
from pathlib import Path
from cloud_radar.cf.unit import Template
template_path = Path("tests/templates/log_bucket/log_bucket.yaml")
template = Template.from_yaml(template_path.resolve())
params = {"BucketPrefix": "testing", "KeepBucket": "TRUE"}
stack = template.create_stack(params, region="us-west-2")
stack.no_resource("LogsBucket")
bucket = stack.get_resource("RetainLogsBucket")
assert "DeletionPolicy" in bucket
assert bucket["DeletionPolicy"] == "Retain"
bucket_name = bucket.get_property_value("BucketName")
assert "us-west-2" in bucket_name
The AWS pseudo parameters are all class attributes and can be modified before rendering a template.
Template.AccountId = '8675309'
Note: Region should only be changed to change the default value. To change the region during testing pass the desired region to render(region='us-west-2')
The default values for pseudo parameters:
Name | Default Value |
---|
AccountId | "555555555555" |
NotificationARNs | [] |
NoValue | "" |
Partition | "aws" |
Region | "us-east-1" |
StackId | "" |
StackName | "" |
URLSuffix | "amazonaws.com" |
Note: Bold variables are not fully implemented yet see the Roadmap | |
At the point of creating the Template
instance additional configuration is required to be provided if you are using certain approaches to resolving values.
If you use Fn::ImportValue, a dictionary of key/value pairs is required containing all the keys that your template uses. If an import name is referenced by the template which is not included in this dictionary, an error will be raised.
imports = {
"FakeKey": "FakeValue"
}
template = Template(template_content, imports=imports)
If you use Dynamic References, a dictionary containing the service and key/value pairs is required containing all the dynamic references that your template uses. If a dynamic reference is included in the template and not contained in the configuration object, an error will be raised.
template_content = {
"Resources": {
"Foo": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": (
"mgt-{{resolve:ssm:/account/current/short_name}}-launch-role-pol"
),
},
},
},
}
dynamic_references = {
"ssm": {
"/account/current/short_name": "dummy"
}
}
template = Template(template_content, dynamic_references=dynamic_references)
A real unit testing example using Pytest can be seen here
Functional Testing (Click to expand)
Using Cloud-Radar starts by importing it into your test file or framework.
from pathlib import Path
from cloud_radar.cf.e2e import Stack
template_path = Path("tests/templates/log_bucket/log_bucket.yaml")
params = {"BucketPrefix": "testing", "KeepBucket": "False"}
regions = ['us-west-2']
with Stack(template_path, params, regions) as stacks:
for stack in stacks:
print(f"Testing {stack.name}")
bucket_name = ""
for output in stack.outputs:
if output.key == "LogsBucketName":
bucket_name = output.value
break
assert "logs" in bucket_name
assert stack.region.name in bucket_name
print(f"Created bucket: {bucket_name}")
You can use taskcat tokens in your parameter values.
parameters = {
"BucketPrefix": "taskcat-$[taskcat_random-string]",
"KeepBucket": "FALSE",
}
You can skip the context manager. Here is an example for unittest
import unittest
from cloud-radar.cf.e2e import Stack
class TestLogBucket(unittest.TestCase):
@classmethod
def setUpClass(cls):
template_path = Path("tests/templates/log_bucket/log_bucket.yaml")
cls.test = Stack(template_path)
cls.test.create()
@classmethod
def tearDownClass(cls):
cls.test.delete()
def test_bucket(self):
stacks = self.__class__.test.stacks
for stack in stacks:
All the properties and methods of a stack instance.
A real functional testing example using Pytest can be seen here
Roadmap
Project
- Add Logo
- Easier to pick regions for testing
Unit
- Add full functionality to pseudo variables.
- Variables like
Partition
, URLSuffix
should change if the region changes. - Variables like
StackName
and StackId
should have a better default than ""
- Handle References to resources that shouldn't exist.
- It's currently possible that a
!Ref
to a Resource stays in the final template even if that resource is later removed because of a conditional.
Functional
- Add the ability to update a stack instance to Taskcat.
See the open issues for a list of proposed features (and known issues).
Contributing
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
This project uses poetry to manage dependencies and pre-commit to run formatting, linting and tests. You will need to have both installed to your system as well as python 3.12.
- Fork the Project
- Setup environment (
poetry install
) - Setup commit hooks (
pre-commit install
) - Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
License
Distributed under the Apache-2.0 License. See LICENSE.txt for more information.
Contact
Levi - @shady_cuz
Acknowledgements