Skip to content

Commit

Permalink
commands: info: add configurable output formats
Browse files Browse the repository at this point in the history
  • Loading branch information
danielkza committed Mar 14, 2019
1 parent cfe719e commit 9b91b63
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 13 deletions.
134 changes: 122 additions & 12 deletions stacker/actions/info.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,149 @@
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import json
import logging
import sys

from .base import BaseAction
from .. import exceptions

logger = logging.getLogger(__name__)


class Exporter(object):
def __init__(self, context):
self.context = context

def start(self):
pass

def start_stack(self, stack):
pass

def end_stack(self, stack):
pass

def write_output(self, key, value):
pass

def finish(self):
pass


class JsonExporter(Exporter):
def start(self):
self.current_outputs = {}
self.stacks = {}

def start_stack(self, stack):
self.current_outputs = {}

def end_stack(self, stack):
self.stacks[stack.name] = {
"outputs": self.current_outputs,
"fqn": stack.fqn
}
self.current_outputs = {}

def write_output(self, key, value):
self.current_outputs[key] = value

def finish(self):
json_data = json.dumps({'stacks': self.stacks}, indent=4)
sys.stdout.write(json_data)
sys.stdout.write('\n')
sys.stdout.flush()


class PlainExporter(Exporter):
def start(self):
self.current_stack = None

def start_stack(self, stack):
self.current_stack = stack.name

def end_stack(self, stack):
self.current_stack = None

def write_output(self, key, value):
assert self.current_stack

line = '{}.{}={}\n'.format(self.current_stack, key, value)
sys.stdout.write(line)

def finish(self):
sys.stdout.flush()


class LogExporter(Exporter):
def start(self):
logger.info('Outputs for stacks: %s', self.context.get_fqn())

def start_stack(self, stack):
logger.info('%s:', stack.fqn)

def write_output(self, key, value):
logger.info('\t{}: {}'.format(key, value))


EXPORTER_CLASSES = {
'json': JsonExporter,
'log': LogExporter,
'plain': PlainExporter
}

OUTPUT_FORMATS = list(EXPORTER_CLASSES.keys())


class Action(BaseAction):
"""Get information on CloudFormation stacks.
Displays the outputs for the set of CloudFormation stacks.
"""

def run(self, *args, **kwargs):
logger.info('Outputs for stacks: %s', self.context.get_fqn())
def build_exporter(self, name):
try:
exporter_cls = EXPORTER_CLASSES[name]
except KeyError:
logger.error('Unknown output format "{}"'.format(name))
return None

return exporter_cls(self.context)

def run(self, output_format='log', *args, **kwargs):
if not self.context.get_stacks():
logger.warn('WARNING: No stacks detected (error in config?)')
for stack in self.context.get_stacks():
return

try:
exporter = self.build_exporter(output_format)
except Exception:
logger.exception('Failed to create exporter instance')
return

exporter.start()

stacks = sorted(self.context.get_stacks(), key=lambda s: s.fqn)
for stack in stacks:
provider = self.build_provider(stack)

try:
provider_stack = provider.get_stack(stack.fqn)
outputs = provider.get_outputs(stack.fqn)
except exceptions.StackDoesNotExist:
logger.info('Stack "%s" does not exist.' % (stack.fqn,))
continue

logger.info('%s:', stack.fqn)
if 'Outputs' in provider_stack:
for output in provider_stack['Outputs']:
logger.info(
'\t%s: %s',
output['OutputKey'],
output['OutputValue']
)
outputs = sorted(
(output['OutputKey'], output['OutputValue'])
for output in outputs)

exporter.start_stack(stack)

for key, value in outputs:
exporter.write_output(key, value)

exporter.end_stack(stack)

exporter.finish()
7 changes: 6 additions & 1 deletion stacker/commands/stacker/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ def add_arguments(self, parser):
"specified more than once. If not specified "
"then stacker will work on all stacks in the "
"config file.")
parser.add_argument("--output-format", action="store", type=str,
choices=info.OUTPUT_FORMATS,
help="Write out stack information in the given "
"export format. Use it if you intend to "
"parse the result programatically.")

def run(self, options, **kwargs):
super(Info, self).run(options, **kwargs)
action = info.Action(options.context,
provider_builder=options.provider_builder)

action.execute()
action.execute(output_format=options.output_format)

def get_context_kwargs(self, options, **kwargs):
return {"stack_names": options.stacks}
122 changes: 122 additions & 0 deletions stacker/tests/actions/test_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import json
import unittest

from mock import Mock, patch
from six import StringIO
from testfixtures import LogCapture

from stacker.actions.info import Action
from stacker.tests.actions.test_build import TestProvider
from stacker.tests.factories import mock_context, MockProviderBuilder


def mock_stack(name, fqn, **kwargs):
m = Mock(fqn=fqn, **kwargs)
m.name = name
return m


class TestInfoAction(unittest.TestCase):
def _set_up_stacks(self):
self.stacks = [
mock_stack(name='vpc', fqn='namespace-vpc'),
mock_stack(name='bucket', fqn='namespace-bucket'),
mock_stack(name='role', fqn='separated-role'),
mock_stack(name='dummy', fqn='namespace-dummy')
]
self.context.get_stacks = Mock(return_value=self.stacks)
self.outputs = {
'vpc': {
'VpcId': 'vpc-123456',
'VpcName': 'dev'
},
'bucket': {
'BucketName': 'my-bucket'
},
'role': {
'RoleName': 'my-role',
'RoleArn': 'arn:aws:iam::123456789012:role/my-role'
},
'dummy': {}
}

def _set_up_provider(self):
self.provider = TestProvider()

def provider_outputs():
for stack in self.stacks:
outputs = [{'OutputKey': key, 'OutputValue': value}
for key, value in self.outputs[stack.name].items()]
yield stack.fqn, outputs

self.provider.set_outputs(dict(provider_outputs()))

def setUp(self):
self.context = mock_context(namespace="namespace")
self._set_up_stacks()
self._set_up_provider()

def run_action(self, output_format):
provider_builder = MockProviderBuilder(self.provider)
action = Action(self.context, provider_builder=provider_builder)
action.execute(output_format=output_format)

def test_output_json(self):
with patch('sys.stdout', new=StringIO()) as fake_out:
self.run_action(output_format='json')

json_data = json.loads(fake_out.getvalue().strip())
self.maxDiff = None
self.assertEqual(
json_data,
{
'stacks': {
'vpc': {
'fqn': 'namespace-vpc',
'outputs': self.outputs['vpc']
},
'bucket': {
'fqn': 'namespace-bucket',
'outputs': self.outputs['bucket']
},
'role': {
'fqn': 'separated-role',
'outputs': self.outputs['role']
},
'dummy': {
'fqn': 'namespace-dummy',
'outputs': self.outputs['dummy']
}
}
})

def test_output_plain(self):
with patch('sys.stdout', new=StringIO()) as fake_out:
self.run_action(output_format='plain')

lines = fake_out.getvalue().strip().splitlines()

for stack_name, outputs in self.outputs.items():
for key, value in outputs.items():
line = '{}.{}={}'.format(stack_name, key, value)
self.assertIn(line, lines)

def test_output_log(self):
log_name = 'stacker.actions.info'
with LogCapture(log_name) as logs:
self.run_action(output_format='log')

def msg(s):
return log_name, 'INFO', s

def msgs():
yield msg('Outputs for stacks: namespace')
for stack in sorted(self.stacks, key=lambda s: s.fqn):
yield msg(stack.fqn + ':')
for key, value in sorted(self.outputs[stack.name].items()):
yield msg('\t{}: {}'.format(key, value))

logs.check(*msgs())

0 comments on commit 9b91b63

Please sign in to comment.