Skip to content

Commit 7f55de9

Browse files
committed
add ci
1 parent 6aa712c commit 7f55de9

File tree

6 files changed

+168
-48
lines changed

6 files changed

+168
-48
lines changed

.github/workflows/ci.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: "CI"
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
Lint:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v2
16+
- uses: actions/setup-python@v1
17+
with:
18+
python-version: 3.8
19+
- name: Linting
20+
run: |
21+
pip install pre-commit
22+
pre-commit run --all-files
23+
24+
Tests:
25+
needs: Lint
26+
runs-on: ubuntu-latest
27+
strategy:
28+
matrix:
29+
python-version: [3.8, 3.9]
30+
steps:
31+
- uses: actions/checkout@v2
32+
- name: Set up Python ${{ matrix.python-version }}
33+
uses: actions/setup-python@v2
34+
with:
35+
python-version: ${{ matrix.python-version }}
36+
37+
- name: Install poetry
38+
shell: bash
39+
run: |
40+
pip install poetry
41+
42+
- name: Configure poetry
43+
shell: bash
44+
run: poetry config virtualenvs.in-project true
45+
46+
- name: Set up cache
47+
uses: actions/cache@v1
48+
id: cache
49+
with:
50+
path: .venv
51+
key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }}
52+
53+
- name: Ensure cache is healthy
54+
if: steps.cache.outputs.cache-hit == 'true'
55+
shell: bash
56+
run: poetry run pip --version >/dev/null 2>&1 || rm -rf .venv
57+
58+
- name: Install dependencies
59+
shell: bash
60+
run: poetry install
61+
62+
- name: Pytest
63+
shell: bash
64+
run: poetry run pytest -v tests

.pre-commit-config.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
exclude: ".(json|md)"
2+
repos:
3+
- repo: https://github.com/psf/black
4+
rev: 19.10b0
5+
hooks:
6+
- id: black
7+
8+
- repo: https://gitlab.com/pycqa/flake8
9+
rev: 3.7.8
10+
hooks:
11+
- id: flake8
12+
13+
- repo: https://github.com/timothycrosley/isort
14+
rev: 4.3.21-2
15+
hooks:
16+
- id: isort
17+
18+
- repo: https://github.com/pre-commit/pre-commit-hooks
19+
rev: v2.3.0
20+
hooks:
21+
- id: trailing-whitespace
22+
exclude: ^tests/.*/fixtures/.*
23+
- id: end-of-file-fixer
24+
exclude: ^tests/.*/fixtures/.*
25+
- id: debug-statements

readme.md

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
# tfdevops
22

3-
Terraform support for AWS DevOps Guru.
3+
Terraform support for AWS DevOps Guru. The service natively only supports Cloudformation stacks.
44
https://aws.amazon.com/devops-guru/features/
55

6+
This project provides support for terraform users by automatically
7+
converting terraform state to an imported cloudformation stack
8+
and optionally enabling it with devops guru.
69

7-
Unfortunately the DevOps Guru service team only supports app level stack enablement via cloudformation, meaning its useless
8-
to the majority of AWS users.
9-
10-
This project provides support for terraform centric organizations to use it by automatically
11-
converting terraform state to an imported cloudformation stack and optionally enabling it with devops guru.
12-
13-
Note it only supports roughly 25 resources per the pricing page.
10+
Note AWS DevOps Guru only supports roughly 25 resources.
1411
https://aws.amazon.com/devops-guru/pricing/
1512

1613

17-
tfdevops also corrects a major usability issue of cloudformation, by providing client side schema validation
18-
of templates.
14+
## How it works
1915

20-
Enjoy.
16+
- Translates terraform state into a cloudformation template with a retain deletion policy
17+
- Creates a cloudformation stack with imported resources
18+
- Enrolls the stack into AWS DevOps Guru
2119

2220
## Usage
2321

@@ -69,3 +67,17 @@ path.
6967
1. Is this a generic terraform to cloudformation converter?
7068

7169
No, while it has some facilities that resemble that, its very targeted at simply producing enough cfn to make devops guru work.
70+
71+
## Supported resources
72+
73+
74+
At the moment tfdevops supports the following resources
75+
76+
- AWS::StepFunctions::StateMachine
77+
- AWS::ECS::Service
78+
- AWS::SQS::Queue
79+
- AWS::SNS::Topic
80+
- AWS::RDS::DBInstance
81+
- AWS::Lambda::Function
82+
- AWS::Events::Rule
83+
- AWS::DynamoDB::Table

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[flake8]
2+
max-line-length = 100
3+

tests/test_tfdevops.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from tfdevops.cli import Translator, TF_CFN_MAP
2+
3+
4+
def test_translator_map():
5+
assert set(Translator.get_translator_map()) == set(TF_CFN_MAP)

tfdevops/cli.py

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1-
import boto3
2-
from botocore.exceptions import ClientError, WaiterError
3-
from botocore.waiter import WaiterModel, create_waiter_with_client
4-
import click
1+
# Copyright Stacklet, Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
54
import json
6-
import jsonschema
7-
import jmespath
8-
import hcl2
95
import logging
10-
from pathlib import Path
116
import subprocess
7+
from pathlib import Path
128
from urllib import parse
139

10+
import boto3
11+
import click
12+
import jmespath
13+
import jsonschema
14+
from botocore.exceptions import ClientError, WaiterError
15+
from botocore.waiter import WaiterModel, create_waiter_with_client
1416

1517
__author__ = "Kapil Thangavelu <https://twitter.com/kapilvt>"
1618

1719
log = logging.getLogger("tfdevops")
1820

1921
DEFAULT_STACK_NAME = "GuruStack"
2022
DEFAULT_CHANGESET_NAME = "GuruImport"
23+
DEFAULT_S3_ENCRYPT = "AES256"
2124

2225
# manually construct waiter models for change sets since the service
2326
# team didn't bother to publish one in their smithy models, perhaps
@@ -70,9 +73,10 @@
7073

7174

7275
@click.group()
73-
def cli():
76+
@click.option("-v", "--verbose", is_flag=True)
77+
def cli(verbose):
7478
"""Terraform to Cloudformation and AWS DevOps Guru"""
75-
logging.basicConfig(level=logging.INFO)
79+
logging.basicConfig(level=verbose and logging.DEBUG or logging.INFO)
7680

7781

7882
@cli.command()
@@ -102,10 +106,14 @@ def deploy(template, resources, stack_name, guru, template_url, change_name):
102106
stack_info = client.describe_stacks(StackName=stack_name)["Stacks"][0]
103107
log.info("Found existing stack, state:%s", stack_info["StackStatus"])
104108
except ClientError:
105-
# somewhat bonkers the service team hasn't put a proper customized exception in place for a common error issue.
106-
# ala they have one for client.exceptions.StackNotFoundException but didn't bother
109+
# somewhat annoying the service team hasn't put a proper customized
110+
# exception in place for a common error issue. ala they have one for
111+
# client.exceptions.StackNotFoundException but didn't bother
107112
# to actually use it for this, or its histerical raison compatibility.
108-
# botocore.exceptions.ClientError: An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id GuruStack does not exist
113+
# This unfortunately means we have to catch a very generic client error.
114+
# ie. we're trying to catch errors like this.
115+
# botocore.exceptions.ClientError: An error occurred (ValidationError) when
116+
# calling the DescribeStacks operation: Stack with id GuruStack does not exist
109117
stack_info = None
110118

111119
# so for each stack and each resource we have to deal with the complexity
@@ -123,7 +131,7 @@ def deploy(template, resources, stack_name, guru, template_url, change_name):
123131
# Its gets worse when you consider the compatibility complexity matrix
124132
# on the various versions and bugs, like the lack of a proper error code
125133
# for stack not found above.
126-
# Nonetheless, we perserve and try to present a humane interface.
134+
# Nonetheless, we persevere and try to present a humane interface.
127135
#
128136
# Stack State Enumeration:
129137
# CREATE_COMPLETE
@@ -184,7 +192,7 @@ def deploy(template, resources, stack_name, guru, template_url, change_name):
184192
cinfo = client.describe_change_set(
185193
StackName=stack_name, ChangeSetName=change_name
186194
)
187-
except client.exceptions.ChangeSetNotFoundException:
195+
except (client.exceptions.ChangeSetNotFoundException, ClientError):
188196
cinfo = None
189197

190198
if cinfo and cinfo["Status"] == "FAILED":
@@ -368,8 +376,9 @@ def validate(template):
368376
def cfn(module, template, resources, types, s3_path):
369377
"""Export a cloudformation template and importable resources
370378
371-
s3 path only needs to be specified when handling resources with verbose definitions (step functions)
372-
or a large cardinality of resources which would overflow cloudformation's api limits on templates (50k).
379+
s3 path only needs to be specified when handling resources with verbose
380+
definitions (step functions) or a large cardinality of resources which would
381+
overflow cloudformation's api limits on templates (50k).
373382
"""
374383
state = get_state_resources(module)
375384
type_map = get_type_mapping()
@@ -421,9 +430,9 @@ def cfn(module, template, resources, types, s3_path):
421430
)
422431

423432
# overflow to s3 for actual deployment on large templates
424-
serialized_template = json.dumps(ctemplate).encode('utf8')
433+
serialized_template = json.dumps(ctemplate).encode("utf8")
425434

426-
if s3_path: # and len(serialized_template) > 49000:
435+
if s3_path: # and len(serialized_template) > 49000:
427436
s3_url = format_template_url(
428437
s3_client,
429438
format_s3_path(
@@ -434,7 +443,9 @@ def cfn(module, template, resources, types, s3_path):
434443
)
435444
log.info("wrote s3 template url: %s", s3_url)
436445
elif len(serialized_template) > 49000:
437-
log.warning("template too large for local deploy, pass --s3-path to deploy from s3")
446+
log.warning(
447+
"template too large for local deploy, pass --s3-path to deploy from s3"
448+
)
438449

439450
template.write(json.dumps(ctemplate, indent=2))
440451

@@ -480,8 +491,9 @@ def write_s3_key(client, s3_path, key, content):
480491
result = client.put_object(
481492
Bucket=kinfo["Bucket"],
482493
Key=kinfo["Key"],
494+
# this is the default but i've seen some orgs try to force this via request policy checks
483495
ACL="private",
484-
ServerSideEncryption="AES256",
496+
ServerSideEncryption=DEFAULT_S3_ENCRYPT,
485497
Body=content,
486498
)
487499
if result.get("VersionId"):
@@ -499,7 +511,7 @@ def format_s3_path(kinfo):
499511
def format_template_url(client, s3_path):
500512
parsed = parse.urlparse(s3_path)
501513
bucket = parsed.netloc
502-
key = parsed.path.strip('/')
514+
key = parsed.path.strip("/")
503515
version_id = None
504516
if parsed.query:
505517
query = parse.parse_qs(parsed.query)
@@ -514,6 +526,19 @@ def format_template_url(client, s3_path):
514526
return url.format(bucket=bucket, key=key, version_id=version_id, region=region)
515527

516528

529+
TF_CFN_MAP = {
530+
"cloudwatch_event_rule": "AWS::Events::Rule",
531+
"db_instance": "AWS::RDS::DBInstance",
532+
"sns_topic": "AWS::SNS::Topic",
533+
"sqs_queue": "AWS::SQS::Queue",
534+
"lambda_function": "AWS::Lambda::Function",
535+
"sfn_state_machine": "AWS::StepFunctions::StateMachine",
536+
"cloudwatch_event_rule": "AWS::Events::Rule",
537+
"ecs_service": "AWS::ECS::Service",
538+
"dynamodb_table": "AWS::DynamoDB::Table",
539+
}
540+
541+
517542
class Translator:
518543

519544
id = None
@@ -819,18 +844,4 @@ def get_properties(self, tf):
819844

820845

821846
if __name__ == "__main__":
822-
try:
823-
cli()
824-
except WaiterError as e:
825-
log.warning(
826-
"failed waiting for async operation error\n reason: %s\n response: %s"
827-
% (e, e.last_response)
828-
)
829-
raise
830-
except SystemExit:
831-
raise
832-
except Exception:
833-
import traceback, pdb, sys
834-
835-
traceback.print_exc()
836-
pdb.post_mortem(sys.exc_info()[-1])
847+
cli()

0 commit comments

Comments
 (0)