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 20, 2019
1 parent 053ad19 commit 64323d3
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 9 deletions.
121 changes: 113 additions & 8 deletions stacker/actions/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,127 @@
from __future__ import division
from __future__ import absolute_import
import logging
import json
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):
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())
@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)

Expand All @@ -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()
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}
91 changes: 91 additions & 0 deletions stacker/tests/actions/test_info.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 64323d3

Please sign in to comment.