-
Notifications
You must be signed in to change notification settings - Fork 167
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
commands: info: add configurable output formats
- Loading branch information
Showing
3 changed files
with
250 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |