Skip to content

Commit

Permalink
Merge pull request #110 from cfstacks/feature/diff
Browse files Browse the repository at this point in the history
Feature/diff
  • Loading branch information
alekna authored Sep 17, 2018
2 parents a449a2a + a2f0634 commit 9cca35d
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 22 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ __pycache__/
# Distribution / packaging
.Python
env/
venv/
build/
develop-eggs/
dist/
Expand Down
10 changes: 10 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
awscli>=1.11.130
configargparse>=0.9.3
PyYAML<=3.13,>=3.10
Jinja2>=2.7.3
boto>=2.40.0
tabulate>=0.7.5
setuptools
pytz
tzlocal
moto
2 changes: 1 addition & 1 deletion stacks/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '0.4.2'
__version__ = '0.4.3'
__licence__ = 'MIT'
__url__ = 'https://stacks.tools'
__maintainer__ = 'Vaidas Jablonskis'
Expand Down
14 changes: 13 additions & 1 deletion stacks/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

def throttling_retry(func):
"""Retry when AWS is throttling API calls"""

def retry_call(*args, **kwargs):
retries = 0
while True:
Expand All @@ -13,12 +14,13 @@ def retry_call(*args, **kwargs):
return retval
except BotoServerError as err:
if (err.code == 'Throttling' or err.code == 'RequestLimitExceeded') and retries <= 3:
sleep = 3 * (2**retries)
sleep = 3 * (2 ** retries)
print('Being throttled. Retrying after {} seconds..'.format(sleep))
time.sleep(sleep)
retries += 1
else:
raise err

return retry_call


Expand Down Expand Up @@ -87,3 +89,13 @@ def get_stack_resource(conn, stack_name, logical_id):
if r.logical_resource_id == logical_id:
return r.physical_resource_id
return None


@throttling_retry
def get_stack_template(conn, stack_name):
"""Return a template body of live stack"""
try:
template = conn.get_template(stack_name)
return template['GetTemplateResponse']['GetTemplateResult']['TemplateBody'], []
except BotoServerError as e:
return None, [e.message]
44 changes: 34 additions & 10 deletions stacks/cf.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
"""
Cloudformation related functions
"""
# An attempt to support python 2.7.x
from __future__ import print_function

import builtins
import difflib
import hashlib
import json
import sys
import time
from collections import Mapping, Set, Sequence
from datetime import datetime
from fnmatch import fnmatch
from operator import attrgetter
from os import path
from collections import Mapping, Set, Sequence

import boto
import jinja2
Expand All @@ -25,8 +23,7 @@
from jinja2 import meta
from tabulate import tabulate

from stacks.aws import get_stack_tag
from stacks.aws import throttling_retry
from stacks.aws import get_stack_tag, get_stack_template, throttling_retry
from stacks.states import FAILED_STACK_STATES, COMPLETE_STACK_STATES, ROLLBACK_STACK_STATES, IN_PROGRESS_STACK_STATES

YES = ['y', 'Y', 'yes', 'YES', 'Yes']
Expand Down Expand Up @@ -180,15 +177,14 @@ def create_stack(conn, stack_name, tpl_file, config, update=False, dry=False, cr
if metadata:
tags = _extract_tags(metadata)
tags.update(default_tags)
name_from_metadata = metadata.get('name')
disable_rollback = metadata.get('disable_rollback')
name_from_metadata = metadata.get('name', None)
disable_rollback = metadata.get('disable_rollback', None)
else:
name_from_metadata = None
tags = default_tags
disable_rollback = None

if not stack_name:
stack_name = name_from_metadata
stack_name = stack_name or name_from_metadata
if not stack_name:
print('Stack name must be specified via command line argument or stack metadata.')
sys.exit(1)
Expand Down Expand Up @@ -397,3 +393,31 @@ def validate_template(tpl):
"/{} 'null' values are not allowed in templates".format('/'.join(map(str, k)))
)
return errors


def print_stack_diff(conn, stack_name, tpl_file, config):
local_template, metadata, errors = gen_template(tpl_file, config)

if metadata:
name_from_metadata = metadata.get('name', None)
else:
name_from_metadata = None

stack_name = stack_name or name_from_metadata
if not stack_name:
print('Stack name must be specified via command line argument or stack metadata.')
sys.exit(1)
if errors:
for err in errors:
print('ERROR: ' + err)

live_template, errors = get_stack_template(conn, stack_name)
if errors:
for err in errors:
print('ERROR: ' + err)
sys.exit(1)

if local_template == live_template:
return
for line in difflib.ndiff(live_template.split('\n'), local_template.split('\n')):
print(line)
16 changes: 16 additions & 0 deletions stacks/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ def parse_options():
help='Poll for new events until stopped (overrides -n)')
parser_events.add_argument('-n', '--lines', default=100, type=int)

# diff subparser
parser_create = subparsers.add_parser('diff', help='Print diff of current vs compiled template')
parser_create.add_argument('-t', '--template', required=True, type=configargparse.FileType())
# noinspection PyArgumentList
parser_create.add_argument('-c', '--config', default='config.yaml',
env_var='STACKS_CONFIG', required=False,
type=_is_file)
# noinspection PyArgumentList
parser_create.add_argument('--config-dir', default='config.d',
env_var='STACKS_CONFIG_DIR', required=False,
type=_is_dir)
parser_create.add_argument('name', nargs='?', default=None)
# noinspection PyArgumentList
parser_create.add_argument('-e', '--env', env_var='STACKS_ENV', required=False, default=None)
parser_create.add_argument('-P', '--property', required=False, action='append')

return parser, parser.parse_args()


Expand Down
23 changes: 13 additions & 10 deletions stacks/main.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
# An attempt to support python 2.7.x
from __future__ import print_function

import sys
import os
import signal
import pytz
import sys
from datetime import datetime

import boto.cloudformation
import boto.ec2
import boto.vpc
import boto.route53
import boto.cloudformation
import boto.s3
import boto.vpc
import pytz

from stacks import cli
from stacks import aws
from stacks import cf
from stacks import cli
from stacks.config import config_load
from stacks.config import get_region_name
from stacks.config import get_default_region_name
from stacks.config import get_region_name
from stacks.config import print_config
from stacks.config import profile_exists
from stacks.config import validate_properties
from stacks.config import print_config
from stacks.states import FAILED_STACK_STATES, ROLLBACK_STACK_STATES


Expand Down Expand Up @@ -151,6 +148,12 @@ def main():
if args.subcommand == 'events':
cf.print_events(cf_conn, args.name, args.events_follow, args.lines)

if args.subcommand == 'diff':
if args.property:
properties = validate_properties(args.property)
config.update(properties)
cf.print_stack_diff(cf_conn, args.name, args.template, config)


def handler(signum, _):
print('Signal {} received. Stopping.'.format(signum))
Expand Down

0 comments on commit 9cca35d

Please sign in to comment.