Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/ssm parameters unit test example #274

Merged
merged 7 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ repos:
rev: v2.3.0
hooks:
- id: check-yaml
exclude: ^tests/templates
exclude: ^(tests/templates|examples/unit)
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: local
hooks:

- id: isort
name: isort
entry: poetry run isort
language: system
types: [python]

- id: black
name: black
entry: poetry run black
Expand Down
1 change: 0 additions & 1 deletion .scripts/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from typing import Any, Dict

import requests

import semver

repo: str = "cloud-radar"
Expand Down
6 changes: 3 additions & 3 deletions examples/unit/conditions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This example shows two main scenarios:
* Conditional resources were created or not.


It uses [this AWS Labs sample template](https://github.com/awslabs/aws-cloudformation-templates/blob/1de97eda75e3b876b8fcb0166d2d4c0691bdcdf5/aws/services/SQS/SQSStandardQueue.json), that includes a condition for if an SQS Queue should have a Dead Letter Queue configured for it or not. This uses a pretty simple condition that simply checks if a parameter value was set to `true`, but this can be as compicated as your template requires.
It uses [this AWS Labs sample template](https://github.com/awslabs/aws-cloudformation-templates/blob/1de97eda75e3b876b8fcb0166d2d4c0691bdcdf5/aws/services/SQS/SQSStandardQueue.json), that includes a condition for if an SQS Queue should have a Dead Letter Queue configured for it or not. This uses a pretty simple condition that simply checks if a parameter value was set to `true`, but this can be as complicated as your template requires.

```json
"Conditions": {
Expand All @@ -22,7 +22,7 @@ It uses [this AWS Labs sample template](https://github.com/awslabs/aws-cloudform
```


The rendered stack includes a number of method for determining if your conditions worked as epected.
The rendered stack includes a number of method for determining if your conditions worked as expected.

These methods can be used to assert if named resources exist or not:
```python
Expand Down Expand Up @@ -68,7 +68,7 @@ A test case is able to check that when the DLQ was not created, that this redriv
assert main_queue.get_property_value("RedrivePolicy") == ''
```

Or inversly that it was set and referenced the correct target queue:
Or inversely that it was set and referenced the correct target queue:
```python
# When the second queue is being created, the SQSQueue should have a
# redrive policy set referring to it
Expand Down
79 changes: 79 additions & 0 deletions examples/unit/parameters/IAM_Users_Groups_and_Policies.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Example file taken from
# https://github.com/awslabs/aws-cloudformation-templates/blob/master/aws/services/IAM/IAM_Users_Groups_and_Policies.yaml
#
# This has been modified to add an AllowedPattern to the Password parameter
AWSTemplateFormatVersion: '2010-09-09'
Metadata:
License: Apache-2.0
Description: 'AWS CloudFormation Sample Template IAM_Users_Groups_and_Policies: Sample
template showing how to create IAM users, groups and policies. It creates a single
user that is a member of a users group and an admin group. The groups each have
different IAM policies associated with them. Note: This example also creates an
AWSAccessKeyId/AWSSecretKey pair associated with the new user. The example is somewhat
contrived since it creates all of the users and groups, typically you would be creating
policies, users and/or groups that contain references to existing users or groups
in your environment. Note that you will need to specify the CAPABILITY_IAM flag
when you create the stack to allow this template to execute. You can do this through
the AWS management console by clicking on the check box acknowledging that you understand
this template creates IAM resources or by specifying the CAPABILITY_IAM flag to
the cfn-create-stack command line tool or CreateStack API call.'
Parameters:
Password:
NoEcho: 'true'
Type: String
Description: New account password
MinLength: '1'
MaxLength: '41'
ConstraintDescription: the password must be between 1 and 41 characters, alphanumeric
AllowedPattern: "^[a-zA-Z0-9]*$"
Resources:
CFNUser:
Type: AWS::IAM::User
Properties:
LoginProfile:
Password: !Ref 'Password'
CFNUserGroup:
Type: AWS::IAM::Group
CFNAdminGroup:
Type: AWS::IAM::Group
Users:
Type: AWS::IAM::UserToGroupAddition
Properties:
GroupName: !Ref 'CFNUserGroup'
Users: [!Ref 'CFNUser']
Admins:
Type: AWS::IAM::UserToGroupAddition
Properties:
GroupName: !Ref 'CFNAdminGroup'
Users: [!Ref 'CFNUser']
CFNUserPolicies:
Type: AWS::IAM::Policy
Properties:
PolicyName: CFNUsers
PolicyDocument:
Statement:
- Effect: Allow
Action: ['cloudformation:Describe*', 'cloudformation:List*', 'cloudformation:Get*']
Resource: '*'
Groups: [!Ref 'CFNUserGroup']
CFNAdminPolicies:
Type: AWS::IAM::Policy
Properties:
PolicyName: CFNAdmins
PolicyDocument:
Statement:
- Effect: Allow
Action: cloudformation:*
Resource: '*'
Groups: [!Ref 'CFNAdminGroup']
CFNKeys:
Type: AWS::IAM::AccessKey
Properties:
UserName: !Ref 'CFNUser'
Outputs:
AccessKey:
Value: !Ref 'CFNKeys'
Description: AWSAccessKeyId of new user
SecretKey:
Value: !GetAtt [CFNKeys, SecretAccessKey]
Description: AWSSecretAccessKey of new user
131 changes: 131 additions & 0 deletions examples/unit/parameters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@

# What does this example cover?

One of the features that lead me to adopting Cloud Radar was it's support for rendering intrinsic functions in a CloudFormation template, based on supplied parameters. As part of this Cloud Radar can validate that mandatory parameters are provided, and that all parameters are valid based on any defined [constraints](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#parameters-section-structure-properties).


This supports all the types of CloudFormation parameters:
* [String & Number](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#parameters-section-structure-properties)
* [AWS-specific](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-specific-parameter-types)
* [SSM Parameter Types](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-ssm-parameter-types)


You can use this feature to validate that any parameter configuration files are valid, and also use assertions to confirm that invalid values would be rejected. Parameters can either be supplied as a `dict` of Key/Value pairs, or loaded from configuration files. At this point the [CodePipeline artifact](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudformation/deploy/index.html#supported-json-syntax) and [CloudFormation CLI](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudformation/create-stack.html) formats can be loaded in from files.

The complete set of files for these examples are in this directory.

# Supplying Parameters

As noted above, parameter values can be supplied in a few different ways.

Inline:
```python
template.create_stack(params={
"MyBucket": "bad-ssm-path-$*£&@*"
})
```

From configuration files (this example uses a path relative to the test case file):
```python
config_path = Path(__file__).parent / "invalid_params_regex.cf.json"

template.create_stack(parameters_file=config_path)
```

## Configuration File Formats
Examples of the two configuration formats are shown below.

CodePipeline:
```json
{
"Parameters": {
"Password": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY"
}
}
```

CloudFormation CLI:
```json
[
{
"ParameterKey": "Password",
"ParameterValue": "Abhd%k*"
}
]
```


## Validating Parameters

A test that creates a stack with a set of parameters and does not raise a `ValueError` of any sort means the parameters have passed the validation constraints the template defined. You can then assert that properties in the "created" resources match expectations.

```python
def test_valid_params(template: Template):
config_path = Path(__file__).parent / "valid_params.json"

stack = template.create_stack(parameters_file=config_path)

# No error at this point means that validation rules have
# passed, go on to check the resource properties
user_resource = stack.get_resource("CFNUser")
login_profile_props = user_resource.get_property_value("LoginProfile")
assert login_profile_props["Password"] == "aSuperSecurePassword"
```

You can validate that a parameter fails validation with a specific error. When building this up, it is sometimes easier to assert that it just raises a `ValueError` then output the error message so that the test can be updated to ensure the expected validation error is being encountered instead of a different one.

```python
def test_invalid_params(template: Template):
config = load_config("invalid_params.json")

with pytest.raises(
ValueError,
match=(
"Value abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY is longer "
"than the maximum length for parameter Password"
),
):
template.create_stack(config["Parameters"])
```

## SSM Parameter Types

As well as validating that the SSM parameter key supplied matches the pattern for what an SSM parameter key should look like, Cloud Radar will substitute in values. The substitution of SSM parameters is also supported through [Dynamic References](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html).

The example template in the ssm directory includes this as one of the parameters for the template:
```yaml
Parameters:
MyBucket:
Type: 'AWS::SSM::Parameter::Value<String>'
Description: The bucket name where all the data will be put into.
Default: /my_parameters/bucket/name
```

Any place that this is included in a `!Ref` or a `!Sub` will have a value substituted in correctly, just like for other parameter types. This requires that when you define your `Template` in your test case that you include values that should be returned when SSM keys are referenced.

```python
template_path = Path(__file__).parent / "SSM_Parameter_example.yaml"

return Template.from_yaml(
template_path.resolve(),
dynamic_references={
"ssm": {
"/my_parameters/database/name": "my-great-database",
"/my_parameters/bucket/name": "my-great-s3-bucket",
}
},
)
```


If a parameter key is used which was not defined with the template, then a Key Error will be raised. You can test that unexpected parameters result in an error like this:

```python
with pytest.raises(
KeyError,
match="Key /an/ssm/key/that/does/not/exist not included in dynamic references configuration for service ssm"
):
template.create_stack(params={
"MyBucket": "/an/ssm/key/that/does/not/exist"
})
```
5 changes: 5 additions & 0 deletions examples/unit/parameters/invalid_params_length.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"Parameters": {
"Password": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY"
}
}
6 changes: 6 additions & 0 deletions examples/unit/parameters/invalid_params_regex.cf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"ParameterKey": "Password",
"ParameterValue": "Abhd%k*"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"ParameterKey": "Password",
"ParameterValue": "Abhd%k*"
}
]
28 changes: 28 additions & 0 deletions examples/unit/parameters/ssm/SSM_Parameter_example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: A simple example of a template with SSM parameters.
Parameters:
MyBucket:
Type: 'AWS::SSM::Parameter::Value<String>'
Description: The bucket name where all the data will be put into.
Default: /my_parameters/bucket/name

MyDatabase:
Type: 'AWS::SSM::Parameter::Value<String>'
Description: The name of the database where the table will be created.
Default: /my_parameters/database/name

Resources:
MyTable:
Type: 'AWS::Glue::Table'
Properties:
DatabaseName: !Ref MyDatabase # Reference the MyDatabase SSM parameter
TableInput:
# This name includes a Dynamic Reference to show the different ways that an
# SSM value could be imported
Name: "{{resolve:ssm:/my_parameters/database/name}}_my_table"
StorageDescriptor:
Columns:
- Name: column_1
Type: string
Location: !Sub 's3://${MyBucket}/test' # Reference the MyBucket SSM parameter
Loading
Loading