Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

Build Upgrade and Misc. #5

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,6 @@ ENV/

# mypy
.mypy_cache/

# IntelliJ IDE
.idea
2 changes: 1 addition & 1 deletion .release
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
release=0.1.0
tag=v0.1.0
pre_tag_command=cd cloudformation && jq --arg version @@RELEASE@@ '.Parameters.CFNCustomProviderZipFileName.Default = ( "lambdas/cfn-custom-provider-" + $version + ".zip") ' cfn-resource-provider.json > x && mv x cfn-resource-provider.json
pre_tag_command=cd cloudformation && jq --arg version @@RELEASE@@ '.Parameters.CFNCustomProviderZipFileName.Default = ( "lambdas/${shell basename ${PWD}}-" + $version + ".zip") ' cfn-resource-provider.json > x && mv x cfn-resource-provider.json
10 changes: 10 additions & 0 deletions Dockerfile.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM python:3.7
ARG PACKAGE=cfn-custom-provider-template
WORKDIR /${PACKAGE}

RUN apt-get update && apt-get install -y jq && apt-get install -y zip
RUN pip install --upgrade pip
# install awscli here to get a more recent version
RUN pip install pipenv awscli

ADD . .
19 changes: 7 additions & 12 deletions Dockerfile.lambda
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
FROM python:3.7
RUN apt-get update && apt-get install -y zip
WORKDIR /lambda
WORKDIR /cfn-provider

ADD requirements.txt /tmp
RUN pip install --quiet -t /lambda -r /tmp/requirements.txt
ADD src/ /lambda/

RUN find /lambda -type d -print0 | xargs -0 chmod ugo+rx && \
find /lambda -type f -print0 | xargs -0 chmod ugo+r

RUN python -m compileall -q /lambda
ADD requirements.txt .
ADD src ./src
ADD build.sh .

ARG ZIPFILE=lambda.zip
RUN zip --quiet -9r /${ZIPFILE} .
RUN chmod +x build.sh && ./build.sh

FROM scratch
ARG ZIPFILE
COPY --from=0 /${ZIPFILE} /
ARG ZIPFILE=lambda.zip
COPY --from=0 /cfn-provider/${ZIPFILE} /
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,15 @@
APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright {yyyy} {name of copyright owner}
Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
13 changes: 8 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
include Makefile.mk

NAME=cfn-custom-provider
S3_BUCKET_PREFIX=binxio-public
AWS_REGION=eu-central-1
NAME ?= $(shell basename $(PWD))
S3_BUCKET_PREFIX ?= binxio-public
AWS_REGION ?= eu-central-1
ALL_REGIONS=$(shell printf "import boto3\nprint('\\\n'.join(map(lambda r: r['RegionName'], boto3.client('ec2').describe_regions()['Regions'])))\n" | python | grep -v '^$(AWS_REGION)$$')

help:
Expand All @@ -24,7 +24,7 @@ deploy: target/$(NAME)-$(VERSION).zip
aws s3 --region $(AWS_REGION) \
cp --acl public-read \
s3://$(S3_BUCKET_PREFIX)-$(AWS_REGION)/lambdas/$(NAME)-$(VERSION).zip \
s3://$(S3_BUCKET_PREFIX)-$(AWS_REGION)/lambdas/$(NAME)-latest.zip
s3://$(S3_BUCKET_PREFIX)-$(AWS_REGION)/lambdas/$(NAME)-latest.zip

deploy-all-regions: deploy
@for REGION in $(ALL_REGIONS); do \
Expand Down Expand Up @@ -81,6 +81,7 @@ deploy-provider: target/$(NAME)-$(VERSION).zip
--template-file ./cloudformation/cfn-resource-provider.yaml \
--parameter-overrides \
S3BucketPrefix=$(S3_BUCKET_PREFIX) \
FunctionName=$(NAME) \
CFNCustomProviderZipFileName=lambdas/$(NAME)-$(VERSION).zip

delete-provider:
Expand All @@ -89,7 +90,9 @@ delete-provider:

demo:
aws cloudformation deploy --stack-name $(NAME)-demo \
--template-file ./cloudformation/demo-stack.yaml --capabilities CAPABILITY_NAMED_IAM
--template-file ./cloudformation/demo-stack.yaml --capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
FunctionName=$(NAME)

delete-demo:
aws cloudformation delete-stack --stack-name $(NAME)-demo
Expand Down
10 changes: 10 additions & 0 deletions Makefile.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
include Makefile

NAME ?= $(shell basename $(PWD))
S3_BUCKET_PREFIX ?= binxio-public
AWS_REGION ?= eu-central-1

target/$(NAME)-$(VERSION).zip: src/*.py requirements.txt
mkdir -p target/content
./build.sh
mv lambda.zip target/$(NAME)-$(VERSION).zip
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pytest = "*"

[packages]
cfn-resource-provider = ">=0.8.6"
boto3 = ">=1.14.14"

[requires]
python_version = "3.7"
13 changes: 13 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
mkdir build
pip install --quiet -t ./build -r requirements.txt
cp ./src/* ./build/

find ./build -type d -print0 | xargs -0 chmod ugo+rx && \
find ./build -type f -print0 | xargs -0 chmod ugo+r

python -m compileall -q ./build

cd build
zip --quiet -9r ../${ZIPFILE:-lambda.zip} .
cd ..
rm -rf build
51 changes: 44 additions & 7 deletions cloudformation/cfn-resource-provider.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ Parameters:
S3BucketPrefix:
Type: String
Default: ''
FunctionName:
Type: String
Default: cfn-custom-provider-template
CFNCustomProviderZipFileName:
Type: String
Default: lambdas/cfn-custom-provider-latest.zip
Default: lambdas/cfn-custom-provider-template-latest.zip
Conditions:
DoNotAttachToVpc: !Equals
- !Ref 'AppVPC'
Expand All @@ -36,10 +39,11 @@ Resources:
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
Policies:
- PolicyName: CFNCustomProviderPolicy
- PolicyName: !Sub "lambda-${FunctionName}-execution"
PolicyDocument:
Version: '2012-10-17'
Statement:
# Permissions Required by Custom Resource
- Effect: Allow
Action:
- ssm:GetParameter
Expand All @@ -50,19 +54,16 @@ Resources:
- kms:Encrypt
Resource:
- '*'
- Action:
- logs:*
Resource: arn:aws:logs:*:*:*
Effect: Allow
CFNCustomProvider:
Type: AWS::Lambda::Function
Properties:
Description: Custom CloudFormation Provider implementation
Code:
S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}'
S3Key: !Ref 'CFNCustomProviderZipFileName'
FunctionName: cfn-custom-provider
FunctionName: !Ref 'FunctionName'
Handler: provider.handler
Timeout: 900
MemorySize: 128
Role: !GetAtt 'LambdaRole.Arn'
VpcConfig: !If
Expand All @@ -72,3 +73,39 @@ Resources:
- !Ref 'DefaultSecurityGroup'
SubnetIds: !Ref 'PrivateSubnets'
Runtime: python3.7
# Logging group and permissions
CFNCustomProviderLogGroup:
Type: AWS::Logs::LogGroup
DependsOn:
- CFNCustomProvider
Properties:
LogGroupName: !Sub /aws/lambda/${CFNCustomProvider}
RetentionInDays: 7
LambdaLoggingPolicy:
Type: AWS::IAM::Policy
Properties:
Roles:
- !Ref 'LambdaRole'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !GetAtt 'CFNCustomProviderLogGroup.Arn'
PolicyName: !Sub "lambda-${FunctionName}-logging"
# Required for asynchronous re-invoke
LambdaReinvokePolicy:
Type: AWS::IAM::Policy
Properties:
Roles:
- !Ref 'LambdaRole'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: !GetAtt 'CFNCustomProvider.Arn'
PolicyName: !Sub "lambda-${FunctionName}-invoke"
6 changes: 5 additions & 1 deletion cloudformation/demo-stack.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: Custom Provider CloudFormation
Parameters:
FunctionName:
Type: String
Default: cfn-custom-provider-template
Resources:
Custom:
Type: Custom::Custom
Properties:
Value: bye bye World!
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:cfn-custom-provider'
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${FunctionName}'
Outputs:
Value:
Description: The value returned by the Custom::Custom resource
Expand Down
32 changes: 30 additions & 2 deletions src/cfn_custom_provider.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List, Dict, Union

from cfn_resource_provider import ResourceProvider

#
Expand All @@ -8,15 +10,20 @@
"required": ["Value"],
"properties": {
"Value": {
# WARNING: uses JSON `array` and `object` NOT python `list` and `dict`
"type": "string",
# "minimum": 0, "maximum": 1, # "integer" type only
# "pattern": "^[_A-Za-z][A-Za-z0-9_$]*$", # "string" values only
# "minLength": 1, "maxLength": 32,
# "enum": ["ValidOption1", "ValidOption2"]
# "minimum": 0, maximum: 1, # "integer" type only
# "default": "", # use python natives for "integer" and "boolean" types
"description": "this value will be made accessible through Fn::GetAtt, property 'Value'",
}
},
}


# TODO: Rename provider to <Name>Provider if you want to use a CloudFormation type like "Custom:<Name>"
class CustomProvider(ResourceProvider):
def __init__(self):
super(ResourceProvider, self).__init__()
Expand All @@ -25,6 +32,15 @@ def __init__(self):
def convert_property_types(self):
self.heuristic_convert_property_types(self.properties)

# TODO: Remove override if no complex validation is required
def is_valid_request(self):
if not super().is_valid_request():
return False
# insert complex validation
return True

# TODO: Implement create, update, and delete
# TODO: Make sure methods are resilient to retries. CF will send two retries, on per minute if it hasn't received a response.
def create(self):
"""Create the requested object and set a Resource ID. See `update` for behaviors based on Resource ID."""
value = self.get("Value")
Expand All @@ -35,7 +51,7 @@ def create(self):
def update(self):
"""Perform one of two update operations:

1. In-place Update: Make an update without changing the Resource ID. On success, the new Resource is used.
1. In-place Update: Make an update without changing the Resource ID. On success, the updated Resource is used.
On failure, `update` is called again with the original parameters.
2. Replacement: Create a new object and return a new Resource ID. On success, `delete` is called with the old
Resource ID. On failure, `delete` is called with the new Resource ID.
Expand All @@ -48,6 +64,18 @@ def delete(self):
"""Remove the object indicated by the Resource ID."""
self.success("nothing to do")

# TODO: If the API call is asynchronous, use this method to check for completion; otherwise delete the override
def is_ready(self):
"""indicates whether the resource is ready"""
if self.request_type == 'Create':
return True
elif self.request_type == 'Update':
return True
elif self.request_type == 'Delete':
return True
else:
raise ValueError(f"No is_ready method for Request Type: {self.request_type}")


provider = CustomProvider()

Expand Down
18 changes: 17 additions & 1 deletion src/provider.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import os
import logging

from cfn_resource_provider import ResourceProvider

from . import cfn_custom_provider

logging.basicConfig(level=os.getenv('LOG_LEVEL', 'INFO'))
Expand All @@ -12,4 +15,17 @@ def handler(request, context):
if request["ResourceType"] == "Custom::Custom":
return cfn_custom_provider.handler(request, context)
else:
raise ValueError(f'No handler found for custom resource {request["ResourceType"]}')
# try to provide reasonable responses to CF request if Resource Type is not supported
provider = ResourceProvider()
provider.set_request(request, context)
if provider.request_type == 'Delete' and provider.physical_resource_id in ['create-not-found', 'deleted']:
provider.success(f'Clean rollback when provider is not found on create.')
provider.physical_resource_id = 'deleted'
elif provider.request_type == 'Create':
provider.fail(f'Provider not found on create: {request["ResourceType"]}')
# used to indicate a clean rollback (i.e. no resources needing deleted)
provider.physical_resource_id = 'create-not-found'
else:
provider.fail(f'Provider not found for resource: {request["ResourceType"]}')
provider.send_response()
raise KeyError(f'No handler found for resource: {request["ResourceType"]}')