Skip to content

Commit

Permalink
Merge pull request #424 from ericzbeard/macros
Browse files Browse the repository at this point in the history
Macros
  • Loading branch information
ericzbeard authored May 1, 2024
2 parents 8be06da + 44de87b commit c59fc3d
Show file tree
Hide file tree
Showing 115 changed files with 1,910 additions and 33,766 deletions.
619 changes: 619 additions & 0 deletions .pylintrc

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions aws/services/CloudFormation/MacrosExamples/.cfnlintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ignore_templates:
- **/example.yaml
ignore_checks:
- W3002 # This is for having to deploy templates using aws cloudformation package
- W3005
- I1022 # Prefer sub over join on empty delimiter
- I3011 # Set explicit values for UpdateReplacePolicy / DeletionPolicy on potentially stateful resource
include_checks:
- I
regions:
- ALL_REGIONS
50 changes: 34 additions & 16 deletions aws/services/CloudFormation/MacrosExamples/Boto3/README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,54 @@
# How to install and use the Boto3 macro in your AWS account

The `Boto3` macro adds the ability to create CloudFormation resources that represent operations performed by [boto3](http://boto3.readthedocs.io/). Each `Boto3` resource represents one function call.
The `Boto3` macro adds the ability to create CloudFormation resources that
represent operations performed by [boto3](http://boto3.readthedocs.io/). Each
`Boto3` resource represents one function call.

A typical use case for this macro might be, for example, to provide some basic configuration of resources.
A typical use case for this macro might be, for example, to provide some basic
configuration of resources.

## Deploying

1. You will need an S3 bucket to store the CloudFormation artifacts:
* If you don't have one already, create one with `aws s3 mb s3://<bucket name>`

2. Package the CloudFormation template. The provided template uses [the AWS Serverless Application Model](https://aws.amazon.com/about-aws/whats-new/2016/11/introducing-the-aws-serverless-application-model/) so must be transformed before you can deploy it.
2. Package the CloudFormation template. The provided template uses [the AWS
Serverless Application
Model](https://aws.amazon.com/about-aws/whats-new/2016/11/introducing-the-aws-serverless-application-model/)
so must be transformed before you can deploy it.

```shell
aws cloudformation package \
--template-file macro.template \
--template-file macro.yaml \
--s3-bucket <your bucket name here> \
--output-template-file packaged.template
--output-template-file packaged.yaml
```

3. Deploy the packaged CloudFormation template to a CloudFormation stack:

```shell
aws cloudformation deploy \
--stack-name boto3-macro \
--template-file packaged.template \
--template-file packaged.yaml \
--capabilities CAPABILITY_IAM
```

4. To test out the macro's capabilities, try launching the provided example template:
```shell
aws cloudformation deploy \
--stack-name ExecutionRoleBuilderCFnMacro \
--template-file ExecutionRoleBuilderCFnMacro.packaged.template \
--stack-name boto3-macro-example \
--template-file example.packaged.yaml \
--capabilities CAPABILITY_IAM
```
## Usage
To make use of the macro, add `Transform: Boto3` to the top level of your CloudFormation template.
To make use of the macro, add `Transform: Boto3` to the top level of your
CloudFormation template.
Here is a trivial example template that adds a readme file to a new CodeCommit repository:
Here is a trivial example template that adds a readme file to a new CodeCommit
repository:
```yaml
Transform: Boto3
Expand All @@ -66,11 +74,15 @@ Resources:
### Resource type
The resource `Type` is used to identify a [boto3 client](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/clients.html) and the method of that client to execute.
The resource `Type` is used to identify a [boto3
client](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/clients.html)
and the method of that client to execute.
The `Type` must start with `Boto3::` and be followed by the name of a client, a `.` and finally the name of a method.
The `Type` must start with `Boto3::` and be followed by the name of a client, a
`.` and finally the name of a method.
The client name will be converted to lower case so that you can use resource names that look similar to other CloudFormation resource types.
The client name will be converted to lower case so that you can use resource
names that look similar to other CloudFormation resource types.
Examples:
* `Boto3::CodeCommit.put_file`
Expand All @@ -79,7 +91,9 @@ Examples:
### Resource mode
The resource may contain a `Mode` property which specifies whether the boto3 call should be made on `Create`, `Update`, `Delete` or any combination of those.
The resource may contain a `Mode` property which specifies whether the boto3
call should be made on `Create`, `Update`, `Delete` or any combination of
those.
The `Mode` may either be a string or a list of strings. For example:
Expand All @@ -89,11 +103,15 @@ The `Mode` may either be a string or a list of strings. For example:
### Resource properties
The `Properties` of the resource will be passed to the specified boto3 method as arguments. The name of each property will be modified so that it started with a lower-case character so that you can use property names that look similar to other CloudFormation resource properties.
The `Properties` of the resource will be passed to the specified boto3 method
as arguments. The name of each property will be modified so that it started
with a lower-case character so that you can use property names that look
similar to other CloudFormation resource properties.
### Controlling the order of execution
You can use the standard CloudFormation property `DependsOn` when you need to ensure that your `Boto3` resources are executed in the correct order.
You can use the standard CloudFormation property `DependsOn` when you need to
ensure that your `Boto3` resources are executed in the correct order.
## Examples
Expand Down
28 changes: 28 additions & 0 deletions aws/services/CloudFormation/MacrosExamples/Boto3/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"Transform": "Boto3",
"Resources": {
"Repo": {
"Type": "AWS::CodeCommit::Repository",
"Properties": {
"RepositoryName": "my-repo"
}
},
"AddReadme": {
"Type": "Boto3::CodeCommit.put_file",
"Properties": {
"RepositoryName": {
"Fn::GetAtt": [
"Repo",
"Name"
]
},
"BranchName": "master",
"FileContent": "Hello, world",
"FilePath": "README.md",
"CommitMessage": "Add another README.md",
"Name": "CloudFormation"
},
"Mode": "Create"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Resources:

AddReadme:
Type: Boto3::CodeCommit.put_file
Mode: Create
Properties:
RepositoryName: !GetAtt Repo.Name
BranchName: master
FileContent: "Hello, world"
FileContent: Hello, world
FilePath: README.md
CommitMessage: Add another README.md
Name: CloudFormation
Mode: Create
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
Custom resource cfn-response module.
"""

from __future__ import print_function
import json
import urllib3

SUCCESS = "SUCCESS"
FAILED = "FAILED"

http = urllib3.PoolManager()

#pylint: disable=too-many-arguments,too-many-locals
def send(event, context, response_status,
response_data, physical_resource_id=None, no_echo=False, reason=None):
"Send a response to CloudFormation regarding the status of the custom resource."
response_url = event['ResponseURL']

print(response_url)

default_reason = "See the details in CloudWatch Log Stream: {}"

response_body = {
'Status' : response_status,
'Reason' : reason or default_reason.format(context.log_stream_name),
'PhysicalResourceId' : physical_resource_id or context.log_stream_name,
'StackId' : event['StackId'],
'RequestId' : event['RequestId'],
'LogicalResourceId' : event['LogicalResourceId'],
'NoEcho' : no_echo,
'Data' : response_data
}

json_response_body = json.dumps(response_body)

print("Response body:")
print(json_response_body)

headers = {
'content-type' : '',
'content-length' : str(len(json_response_body))
}

try:
response = http.request('PUT', response_url,
headers=headers, body=json_response_body)
print("Status code:", response.status)


except Exception as e:
print("send(..) failed executing http.request(..):", e)

29 changes: 11 additions & 18 deletions aws/services/CloudFormation/MacrosExamples/Boto3/lambda/macro.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

"Implements the Boto3 CloudFormation Macro"
import os

PREFIX = "Boto3::"

LAMBDA_ARN = os.environ["LAMBDA_ARN"]

def handle_template(request_id, template):
for name, resource in template.get("Resources", {}).items():
def handle_template(template):
"Handle a template, replacing any Boto3::* resources with Custom::Boto3"

for _, resource in template.get("Resources", {}).items():
if resource["Type"].startswith(PREFIX):
resource.update({
"Type": "Custom::Boto3",
Expand All @@ -36,13 +26,16 @@ def handle_template(request_id, template):

return template

def handler(event, context):

def handler(event, _):
"Handle a CloudFormation event"

fragment = event["fragment"]
status = "success"

try:
fragment = handle_template(event["requestId"], event["fragment"])
except Exception as e:
fragment = handle_template(event["fragment"])
except Exception:
status = "failure"

return {
Expand Down
59 changes: 17 additions & 42 deletions aws/services/CloudFormation/MacrosExamples/Boto3/lambda/resource.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,28 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import urllib.request
import boto3
"Implements the CloudFormation resource handler for the Boto3 macro"

import json
import boto3

def sendResponse(event, context, status, message):
body = json.dumps({
"Status": status,
"Reason": message,
"StackId": event['StackId'],
"RequestId": event['RequestId'],
"LogicalResourceId": event['LogicalResourceId'],
"PhysicalResourceId": event["ResourceProperties"]["Action"],
"Data": {},
})
request = urllib.request.Request(event['ResponseURL'], data=body.encode('utf-8'))
request.add_header('Content-Type', 'application/json')
request.add_header('Content-Length', len(body))
request.get_method = lambda: 'PUT'

with urllib.request.urlopen(request) as response:
pass
from custom_response import send, FAILED, SUCCESS

def execute(action, properties):
action = action.split(".")
"Executes the requested action"
actions = action.split(".")

if len(action) != 2:
return "FAILED", "Invalid boto3 call: {}".format(".".join(action))
if len(actions) != 2:
return "FAILED", f"Invalid boto3 call: {action}"

client, function = action[0], action[1]
client, function = actions[0], actions[1]

try:
client = boto3.client(client.lower())
except Exception as e:
return "FAILED", "boto3 error: {}".format(e)
return "FAILED", f"boto3 error: {e}"

try:
function = getattr(client, function)
except Exception as e:
return "FAILED", "boto3 error: {}".format(e)
return "FAILED", f"boto3 error: {e}"

properties = {
key[0].lower() + key[1:]: value
Expand All @@ -59,24 +32,26 @@ def execute(action, properties):
try:
function(**properties)
except Exception as e:
return "FAILED", "boto3 error: {}".format(e)
return "FAILED", f"boto3 error: {e}"

return "SUCCESS", "Completed successfully"

def handler(event, context):
"Handle a CloudFormation event"

print("Received request:", json.dumps(event, indent=4))

request = event["RequestType"]
properties = event["ResourceProperties"]

if any(prop not in properties for prop in ("Action", "Properties")):
print("Bad properties", properties)
return sendResponse(event, context, "FAILED", "Missing required parameters")
return send(event, context, FAILED, {}, reason="Missing required parameters")

mode = properties["Mode"]

if request == mode or request in mode:
status, message = execute(properties["Action"], properties["Properties"])
return sendResponse(event, context, status, message)
return send(event, context, status, {}, reason=message)

return sendResponse(event, context, "SUCCESS", "No action taken")
return send(event, context, SUCCESS, {}, reason="No action taken")
Loading

0 comments on commit c59fc3d

Please sign in to comment.