diff --git a/stacker/actions/info.py b/stacker/actions/info.py index 1508de2f0..1060257c8 100644 --- a/stacker/actions/info.py +++ b/stacker/actions/info.py @@ -2,6 +2,8 @@ from __future__ import division from __future__ import absolute_import import logging +import json +import sys from .base import BaseAction from .. import exceptions @@ -9,6 +11,89 @@ 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): + 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. @@ -16,10 +101,28 @@ class Action(BaseAction): """ - def run(self, *args, **kwargs): - logger.info('Outputs for stacks: %s', self.context.get_fqn()) + @classmethod + def build_exporter(cls, name): + try: + exporter_cls = EXPORTER_CLASSES[name] + except KeyError: + logger.error('Unknown output format "{}"'.format(name)) + raise + + try: + return exporter_cls() + except Exception: + logger.exception('Failed to create exporter instance') + raise + + def run(self, output_format='log', *args, **kwargs): if not self.context.get_stacks(): logger.warn('WARNING: No stacks detected (error in config?)') + return + + exporter = self.build_exporter(output_format) + exporter.start(self.context) + for stack in self.context.get_stacks(): provider = self.build_provider(stack) @@ -29,11 +132,13 @@ def run(self, *args, **kwargs): logger.info('Stack "%s" does not exist.' % (stack.fqn,)) continue - logger.info('%s:', stack.fqn) + exporter.start_stack(stack) + if 'Outputs' in provider_stack: for output in provider_stack['Outputs']: - logger.info( - '\t%s: %s', - output['OutputKey'], - output['OutputValue'] - ) + exporter.write_output(output['OutputKey'], + output['OutputValue']) + + exporter.end_stack(stack) + + exporter.finish() diff --git a/stacker/commands/stacker/info.py b/stacker/commands/stacker/info.py index ac847bbec..cf9663615 100644 --- a/stacker/commands/stacker/info.py +++ b/stacker/commands/stacker/info.py @@ -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} diff --git a/stacker/tests/actions/test_info.py b/stacker/tests/actions/test_info.py new file mode 100644 index 000000000..7b77c5022 --- /dev/null +++ b/stacker/tests/actions/test_info.py @@ -0,0 +1,91 @@ +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 stacker.context import Context, Config +from stacker.actions.info import ( + JsonExporter, + PlainExporter +) + + +def stack_mock(name, **kwargs): + m = Mock(**kwargs) + m.name = name + return m + + +class TestExporters(unittest.TestCase): + def setUp(self): + self.context = Context(config=Config({"namespace": "namespace"})) + self.stacks = [ + stack_mock(name='vpc', fqn='namespace-test-1'), + stack_mock(name='bucket', fqn='namespace-test-2'), + stack_mock(name='role', fqn='namespace-test-3') + ] + self.outputs = { + 'vpc': { + 'VpcId': 'vpc-123456', + 'VpcName': 'dev' + }, + 'bucket': { + 'BucketName': 'my-bucket' + }, + 'role': { + 'RoleName': 'my-role', + 'RoleArn': 'arn:::' + } + } + + def run_export(self, exporter): + exporter.start() + + for stack in self.stacks: + exporter.start_stack(stack) + for key, value in self.outputs[stack.name].items(): + exporter.write_output(key, value) + exporter.end_stack(stack) + + exporter.finish() + + def test_json(self): + exporter = JsonExporter(self.context) + with patch('sys.stdout', new=StringIO()) as fake_out: + self.run_export(exporter) + + json_data = json.loads(fake_out.getvalue().strip()) + self.assertEqual( + json_data, + { + u'stacks': { + u'vpc': { + u'fqn': u'namespace-vpc', + u'outputs': self.outputs['vpc'] + }, + 'bucket': { + u'fqn': u'namespace-bucket', + u'outputs': self.outputs['bucket'] + }, + u'role': { + u'fqn': u'namespace-role', + u'outputs': self.outputs['role'] + } + } + }) + + def test_plain(self): + exporter = PlainExporter(self.context) + with patch('sys.stdout', new=StringIO()) as fake_out: + self.run_export(exporter) + + 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)