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+ #
54import json
6- import jsonschema
7- import jmespath
8- import hcl2
95import logging
10- from pathlib import Path
116import subprocess
7+ from pathlib import Path
128from 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
1719log = logging .getLogger ("tfdevops" )
1820
1921DEFAULT_STACK_NAME = "GuruStack"
2022DEFAULT_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
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):
368376def 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):
499511def 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+
517542class Translator :
518543
519544 id = None
@@ -819,18 +844,4 @@ def get_properties(self, tf):
819844
820845
821846if __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