Skip to content

Commit

Permalink
feat: add cloudformation loader (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
julianolf authored Nov 27, 2023
1 parent cb1d5ed commit 528791b
Show file tree
Hide file tree
Showing 7 changed files with 418 additions and 0 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ classifiers = [
requires-python = ">=3.8"
dependencies = [
"fastapi == 0.104.1",
"PyYAML == 6.0.1",
]
71 changes: 71 additions & 0 deletions src/cloudformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from pathlib import Path
from typing import Any, Dict, List, Optional

import yaml

PREFIX = "Fn::"
WITHOUT_PREFIX = ("Ref", "Condition")


class CFTemplateNotFound(FileNotFoundError):
pass


class CFBadTag(TypeError):
pass


class CFBadNode(ValueError):
pass


class CFLoader(yaml.SafeLoader):
pass


def multi_constructor(loader: CFLoader, tag_suffix: str, node: yaml.nodes.Node) -> Dict[str, Any]:
tag = tag_suffix

if tag not in WITHOUT_PREFIX:
tag = f"{PREFIX}{tag}"

if tag == "Fn::GetAtt":
return {tag: construct_getatt(node)}
elif isinstance(node, yaml.ScalarNode):
return {tag: loader.construct_scalar(node)}
elif isinstance(node, yaml.SequenceNode):
return {tag: loader.construct_sequence(node)}
elif isinstance(node, yaml.MappingNode):
return {tag: loader.construct_mapping(node)}

raise CFBadTag(f"!{tag} <{type(node)}>")


def construct_getatt(node: yaml.nodes.Node) -> List[Any]:
if isinstance(node.value, str):
return node.value.split(".", 1)
elif isinstance(node.value, list):
return [s.value for s in node.value]

raise CFBadNode(f"Type <{type(node.value)}>")


CFLoader.add_multi_constructor("!", multi_constructor)


def load(template: Optional[str] = None) -> Dict[str, Any]:
path: Optional[Path] = None

if isinstance(template, str):
path = Path(template)
else:
paths = (Path("template.yml"), Path("template.yaml"))
path_generator = (p for p in paths if p.is_file())
path = next(path_generator, None)

if path is None or not path.is_file():
filename = template or "[template.yml, template.yaml]"
raise CFTemplateNotFound(filename)

with path.open() as fp:
return yaml.load(fp, CFLoader)
37 changes: 37 additions & 0 deletions tests/fixtures/templates/example1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-api
Sample SAM Template for sam-api
Globals:
Function:
Timeout: 3

Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.11
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get

Outputs:
HelloWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn
77 changes: 77 additions & 0 deletions tests/fixtures/templates/example2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
sam-api
Sample SAM Template for sam-api
Parameters:
Environment:
Type: String
Default: development

Mappings:
Environments:
development:
LogLevel: DEBUG
staging:
LogLevel: WARNING
production:
LogLevel: ERROR

Globals:
Function:
Runtime: python3.11
Timeout: 3
Environment:
Variables:
ENVIRONMENT:
Ref: Environment
LOG_LEVEL:
Fn::FindInMap:
- Environments
- Ref: Environment
- LogLevel

Resources:
ApiGateway:
Type: AWS::Serverless::Api
Properties:
Name: sam-api
StageName: v1

ApiGatewayRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action:
- sts:AssumeRole

HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
RestApiId: !Ref ApiGateway

Outputs:
HelloWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value:
Fn::Sub: "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/v1/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value:
Fn::GetAtt: HelloWorldFunction.Arn
65 changes: 65 additions & 0 deletions tests/fixtures/templates/example3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
sam-api
Sample SAM Template for sam-api
Parameters:
Environment:
Type: String
Default: development

Mappings:
Environments:
development:
LogLevel: DEBUG
staging:
LogLevel: WARNING
production:
LogLevel: ERROR

Globals:
Function:
Runtime: python3.11
Timeout: 3
Environment:
Variables:
ENVIRONMENT:
Ref: Environment
LOG_LEVEL:
Fn::FindInMap:
- Environments
- Ref: Environment
- LogLevel

Resources:
ApiGateway:
Type: AWS::Serverless::Api
Properties:
Name: sam-api
StageName: v1
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: "./swagger.yml"

ApiGatewayRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action:
- sts:AssumeRole

HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
80 changes: 80 additions & 0 deletions tests/fixtures/templates/swagger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
openapi: "3.0.1"
info:
title: "SAM API"
description: "Sample SAM API"
version: "v1.0"
contact:
email: [email protected]

servers:
- url: https://hello.mydomain.com
description: Sample SAM API

x-amazon-apigateway-request-validator: all
x-amazon-apigateway-request-validators:
all:
validateRequestBody: true
validateRequestParameters: true

x-amazon-apigateway-cors:
allowOrigins:
- "*"
allowMethods:
- "OPTIONS"
- "GET"
allowHeaders:
- "Content-Type"

components:
schemas:
HelloResponse:
type: object
properties:
message:
type: string
examples:
HelloResponse:
summary: An example of hello message
value:
message: "Hello World!"

tags:
- name: Hello

paths:
/hello/{name}:
get:
operationId: sayHello
tags:
- Hello
summary: Say hello
description: Returns a greeting message
parameters:
- in: path
name: name
required: true
schema:
type: string
description: Your name
responses:
'200':
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/HelloResponse"
examples:
address_response:
$ref: "#/components/examples/HelloResponse"
'400':
description: "Bad Request"
'500':
description: "Internal Server Error"
x-amazon-apigateway-integration:
passthroughBehavior: when_no_match
httpMethod: POST
type: aws_proxy
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations"
credentials:
Fn::Sub: "arn:aws:iam::${AWS::AccountId}:role/apigateway-invoke-lambda-role"
Loading

0 comments on commit 528791b

Please sign in to comment.