Skip to content

Commit

Permalink
Merge pull request #36 from remind101/handle_skipped_stack_rollbacks
Browse files Browse the repository at this point in the history
Fix unchanged rollback stack issue
  • Loading branch information
phobologic committed Jun 19, 2015
2 parents d6d4d36 + acefd4f commit 58c68be
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 31 deletions.
28 changes: 15 additions & 13 deletions stacker/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from aws_helper.connection import ConnectionManager
from boto.exception import BotoServerError, S3ResponseError

from .plan import Plan, INPROGRESS_STATUSES, STATUS_SUBMITTED, COMPLETE_STATUSES
from .plan import (
Plan, INPROGRESS_STATUSES, SUBMITTED, SKIPPED, PENDING, COMPLETE_STATUSES
)
from .util import get_bucket_location, load_object_from_string

logger = logging.getLogger(__name__)
Expand All @@ -18,7 +20,8 @@ def __init__(self, parameters, *args, **kwargs):
message = 'Missing required parameters: %s' % (
', '.join(parameters),
)
super(MissingParameterException, self).__init__(message, *args, **kwargs)
super(MissingParameterException, self).__init__(message, *args,
**kwargs)


class ParameterDoesNotExist(Exception):
Expand Down Expand Up @@ -311,7 +314,7 @@ def create_stack(self, full_name, template_url, parameters, tags):
template_url=template_url,
parameters=parameters, tags=tags,
capabilities=['CAPABILITY_IAM'])
return True
return SUBMITTED

def update_stack(self, full_name, template_url, parameters, tags):
""" Updates an existing stack in CloudFormation. """
Expand All @@ -320,12 +323,12 @@ def update_stack(self, full_name, template_url, parameters, tags):
self.conn.cloudformation.update_stack(
full_name, template_url=template_url, parameters=parameters,
tags=tags, capabilities=['CAPABILITY_IAM'])
return True
return SUBMITTED
except BotoServerError as e:
if 'No updates are to be performed.' in e.message:
logger.info("Stack %s did not change, not updating.",
full_name)
return True
return SKIPPED
raise

def launch_stack(self, stack_name, blueprint):
Expand All @@ -349,16 +352,15 @@ def launch_stack(self, stack_name, blueprint):
required_params = [k for k, v in blueprint.required_parameters]
parameters = handle_missing_parameters(parameters, required_params,
stack)
submitted = False
status = PENDING
if not stack:
submitted = self.create_stack(full_name, template_url, parameters,
tags)
status = self.create_stack(full_name, template_url, parameters,
tags)
else:
submitted = self.update_stack(full_name, template_url, parameters,
tags)
status = self.update_stack(full_name, template_url, parameters,
tags)

if submitted:
stack_context.submit()
stack_context.set_status(status)

def get_outputs(self, stack_name, force=False):
""" Gets all the outputs from a given stack in CloudFormation.
Expand Down Expand Up @@ -397,7 +399,7 @@ def sync_plan_status(self):
local_status = stack_context.status
# We only update local status on stacks that have been marked
# locally as submitted
if not local_status == STATUS_SUBMITTED:
if not local_status == SUBMITTED:
logger.debug("Stack %s not submitted yet.", stack_name)
continue
cf_status = self.get_stack_status(full_name)
Expand Down
58 changes: 40 additions & 18 deletions stacker/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
)
import logging

from collections import namedtuple

logger = logging.getLogger(__name__)

INPROGRESS_STATUSES = ('CREATE_IN_PROGRESS',
'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS',
'UPDATE_IN_PROGRESS')
COMPLETE_STATUSES = ('CREATE_COMPLETE', 'UPDATE_COMPLETE')

STATUS_SUBMITTED = 1
STATUS_COMPLETE = 2
Status = namedtuple('Status', ['name', 'code'])
PENDING = Status('pending', 0)
SUBMITTED = Status('submitted', 1)
COMPLETE = Status('complete', 2)
SKIPPED = Status('skipped', 3)


class BlueprintContext(object):
Expand All @@ -27,18 +32,22 @@ def __init__(self, name, class_path, namespace, requires=None,
self._requires = set(requires)

self.blueprint = None
self.status = None
self.status = PENDING

def __repr__(self):
return self.name

@property
def completed(self):
return self.status == STATUS_COMPLETE
return self.status == COMPLETE

@property
def skipped(self):
return self.status == SKIPPED

@property
def submitted(self):
return self.status >= STATUS_SUBMITTED
return self.status.code >= SUBMITTED.code

@property
def requires(self):
Expand All @@ -55,13 +64,18 @@ def requires(self):
requires.add(stack_name)
return requires

def set_status(self, status):
logger.debug("Setting %s state to %s.", self.name, status.name)
self.status = status

def complete(self):
logger.debug("Setting %s state to complete.", self.name)
self.status = STATUS_COMPLETE
self.set_status(COMPLETE)

def submit(self):
logger.debug("Setting %s state to submitted.", self.name)
self.status = STATUS_SUBMITTED
self.set_status(SUBMITTED)

def skip(self):
self.set_status(SKIPPED)


class Plan(OrderedDict):
Expand All @@ -85,24 +99,32 @@ def submit(self, items):
for i in items:
self[i].submit()

def list_completed(self):
result = OrderedDict()
for k, record in self.items():
if record.status == STATUS_COMPLETE:
result[k] = record
return result
def skip(self, items):
items = self._parse_items(items)
for i in items:
self[i].skip()

def list_pending(self):
def list_status(self, status):
result = OrderedDict()
for k, record in self.items():
if record.status != STATUS_COMPLETE:
if record.status == status:
result[k] = record
return result

def list_completed(self):
return self.list_status(COMPLETE)

def list_submitted(self):
return self.list_status(SUBMITTED)

def list_skipped(self):
return self.list_status(SKIPPED)

def list_pending(self):
""" Pending is any task that isn't COMPLETE or SKIPPED. """
result = OrderedDict()
for k, record in self.items():
if record.status == STATUS_SUBMITTED:
if record.status != COMPLETE and record.status != SKIPPED:
result[k] = record
return result

Expand Down
8 changes: 8 additions & 0 deletions stacker/tests/test_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,24 @@ def test_add(self):
def test_status(self):
self.assertEqual(len(self.plan.list_submitted()), 0)
self.assertEqual(len(self.plan.list_completed()), 0)
self.assertEqual(len(self.plan.list_skipped()), 0)
self.assertEqual(len(self.plan.list_pending()), 4)
self.plan.submit('vpc.1')
self.assertEqual(len(self.plan.list_submitted()), 1)
self.assertEqual(len(self.plan.list_completed()), 0)
self.assertEqual(len(self.plan.list_skipped()), 0)
self.assertEqual(len(self.plan.list_pending()), 4)
self.plan.complete('vpc.1')
self.assertEqual(len(self.plan.list_submitted()), 0)
self.assertEqual(len(self.plan.list_completed()), 1)
self.assertEqual(len(self.plan.list_skipped()), 0)
self.assertEqual(len(self.plan.list_pending()), 3)
self.assertFalse(self.plan.completed)
self.plan.skip('vpc.2')
self.assertEqual(len(self.plan.list_submitted()), 0)
self.assertEqual(len(self.plan.list_completed()), 1)
self.assertEqual(len(self.plan.list_skipped()), 1)
self.assertEqual(len(self.plan.list_pending()), 2)
for i in range(4):
self.plan.complete("vpc.%d" % i)
self.assertTrue(self.plan.completed)

0 comments on commit 58c68be

Please sign in to comment.