Skip to content

Commit 9b91b63

Browse files
committed
commands: info: add configurable output formats
1 parent cfe719e commit 9b91b63

File tree

3 files changed

+250
-13
lines changed

3 files changed

+250
-13
lines changed

stacker/actions/info.py

+122-12
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,149 @@
11
from __future__ import print_function
22
from __future__ import division
33
from __future__ import absolute_import
4+
import json
45
import logging
6+
import sys
57

68
from .base import BaseAction
79
from .. import exceptions
810

911
logger = logging.getLogger(__name__)
1012

1113

14+
class Exporter(object):
15+
def __init__(self, context):
16+
self.context = context
17+
18+
def start(self):
19+
pass
20+
21+
def start_stack(self, stack):
22+
pass
23+
24+
def end_stack(self, stack):
25+
pass
26+
27+
def write_output(self, key, value):
28+
pass
29+
30+
def finish(self):
31+
pass
32+
33+
34+
class JsonExporter(Exporter):
35+
def start(self):
36+
self.current_outputs = {}
37+
self.stacks = {}
38+
39+
def start_stack(self, stack):
40+
self.current_outputs = {}
41+
42+
def end_stack(self, stack):
43+
self.stacks[stack.name] = {
44+
"outputs": self.current_outputs,
45+
"fqn": stack.fqn
46+
}
47+
self.current_outputs = {}
48+
49+
def write_output(self, key, value):
50+
self.current_outputs[key] = value
51+
52+
def finish(self):
53+
json_data = json.dumps({'stacks': self.stacks}, indent=4)
54+
sys.stdout.write(json_data)
55+
sys.stdout.write('\n')
56+
sys.stdout.flush()
57+
58+
59+
class PlainExporter(Exporter):
60+
def start(self):
61+
self.current_stack = None
62+
63+
def start_stack(self, stack):
64+
self.current_stack = stack.name
65+
66+
def end_stack(self, stack):
67+
self.current_stack = None
68+
69+
def write_output(self, key, value):
70+
assert self.current_stack
71+
72+
line = '{}.{}={}\n'.format(self.current_stack, key, value)
73+
sys.stdout.write(line)
74+
75+
def finish(self):
76+
sys.stdout.flush()
77+
78+
79+
class LogExporter(Exporter):
80+
def start(self):
81+
logger.info('Outputs for stacks: %s', self.context.get_fqn())
82+
83+
def start_stack(self, stack):
84+
logger.info('%s:', stack.fqn)
85+
86+
def write_output(self, key, value):
87+
logger.info('\t{}: {}'.format(key, value))
88+
89+
90+
EXPORTER_CLASSES = {
91+
'json': JsonExporter,
92+
'log': LogExporter,
93+
'plain': PlainExporter
94+
}
95+
96+
OUTPUT_FORMATS = list(EXPORTER_CLASSES.keys())
97+
98+
1299
class Action(BaseAction):
13100
"""Get information on CloudFormation stacks.
14101
15102
Displays the outputs for the set of CloudFormation stacks.
16103
17104
"""
18105

19-
def run(self, *args, **kwargs):
20-
logger.info('Outputs for stacks: %s', self.context.get_fqn())
106+
def build_exporter(self, name):
107+
try:
108+
exporter_cls = EXPORTER_CLASSES[name]
109+
except KeyError:
110+
logger.error('Unknown output format "{}"'.format(name))
111+
return None
112+
113+
return exporter_cls(self.context)
114+
115+
def run(self, output_format='log', *args, **kwargs):
21116
if not self.context.get_stacks():
22117
logger.warn('WARNING: No stacks detected (error in config?)')
23-
for stack in self.context.get_stacks():
118+
return
119+
120+
try:
121+
exporter = self.build_exporter(output_format)
122+
except Exception:
123+
logger.exception('Failed to create exporter instance')
124+
return
125+
126+
exporter.start()
127+
128+
stacks = sorted(self.context.get_stacks(), key=lambda s: s.fqn)
129+
for stack in stacks:
24130
provider = self.build_provider(stack)
25131

26132
try:
27-
provider_stack = provider.get_stack(stack.fqn)
133+
outputs = provider.get_outputs(stack.fqn)
28134
except exceptions.StackDoesNotExist:
29135
logger.info('Stack "%s" does not exist.' % (stack.fqn,))
30136
continue
31137

32-
logger.info('%s:', stack.fqn)
33-
if 'Outputs' in provider_stack:
34-
for output in provider_stack['Outputs']:
35-
logger.info(
36-
'\t%s: %s',
37-
output['OutputKey'],
38-
output['OutputValue']
39-
)
138+
outputs = sorted(
139+
(output['OutputKey'], output['OutputValue'])
140+
for output in outputs)
141+
142+
exporter.start_stack(stack)
143+
144+
for key, value in outputs:
145+
exporter.write_output(key, value)
146+
147+
exporter.end_stack(stack)
148+
149+
exporter.finish()

stacker/commands/stacker/info.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ def add_arguments(self, parser):
2020
"specified more than once. If not specified "
2121
"then stacker will work on all stacks in the "
2222
"config file.")
23+
parser.add_argument("--output-format", action="store", type=str,
24+
choices=info.OUTPUT_FORMATS,
25+
help="Write out stack information in the given "
26+
"export format. Use it if you intend to "
27+
"parse the result programatically.")
2328

2429
def run(self, options, **kwargs):
2530
super(Info, self).run(options, **kwargs)
2631
action = info.Action(options.context,
2732
provider_builder=options.provider_builder)
2833

29-
action.execute()
34+
action.execute(output_format=options.output_format)
3035

3136
def get_context_kwargs(self, options, **kwargs):
3237
return {"stack_names": options.stacks}

stacker/tests/actions/test_info.py

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from __future__ import print_function
2+
from __future__ import division
3+
from __future__ import absolute_import
4+
import json
5+
import unittest
6+
7+
from mock import Mock, patch
8+
from six import StringIO
9+
from testfixtures import LogCapture
10+
11+
from stacker.actions.info import Action
12+
from stacker.tests.actions.test_build import TestProvider
13+
from stacker.tests.factories import mock_context, MockProviderBuilder
14+
15+
16+
def mock_stack(name, fqn, **kwargs):
17+
m = Mock(fqn=fqn, **kwargs)
18+
m.name = name
19+
return m
20+
21+
22+
class TestInfoAction(unittest.TestCase):
23+
def _set_up_stacks(self):
24+
self.stacks = [
25+
mock_stack(name='vpc', fqn='namespace-vpc'),
26+
mock_stack(name='bucket', fqn='namespace-bucket'),
27+
mock_stack(name='role', fqn='separated-role'),
28+
mock_stack(name='dummy', fqn='namespace-dummy')
29+
]
30+
self.context.get_stacks = Mock(return_value=self.stacks)
31+
self.outputs = {
32+
'vpc': {
33+
'VpcId': 'vpc-123456',
34+
'VpcName': 'dev'
35+
},
36+
'bucket': {
37+
'BucketName': 'my-bucket'
38+
},
39+
'role': {
40+
'RoleName': 'my-role',
41+
'RoleArn': 'arn:aws:iam::123456789012:role/my-role'
42+
},
43+
'dummy': {}
44+
}
45+
46+
def _set_up_provider(self):
47+
self.provider = TestProvider()
48+
49+
def provider_outputs():
50+
for stack in self.stacks:
51+
outputs = [{'OutputKey': key, 'OutputValue': value}
52+
for key, value in self.outputs[stack.name].items()]
53+
yield stack.fqn, outputs
54+
55+
self.provider.set_outputs(dict(provider_outputs()))
56+
57+
def setUp(self):
58+
self.context = mock_context(namespace="namespace")
59+
self._set_up_stacks()
60+
self._set_up_provider()
61+
62+
def run_action(self, output_format):
63+
provider_builder = MockProviderBuilder(self.provider)
64+
action = Action(self.context, provider_builder=provider_builder)
65+
action.execute(output_format=output_format)
66+
67+
def test_output_json(self):
68+
with patch('sys.stdout', new=StringIO()) as fake_out:
69+
self.run_action(output_format='json')
70+
71+
json_data = json.loads(fake_out.getvalue().strip())
72+
self.maxDiff = None
73+
self.assertEqual(
74+
json_data,
75+
{
76+
'stacks': {
77+
'vpc': {
78+
'fqn': 'namespace-vpc',
79+
'outputs': self.outputs['vpc']
80+
},
81+
'bucket': {
82+
'fqn': 'namespace-bucket',
83+
'outputs': self.outputs['bucket']
84+
},
85+
'role': {
86+
'fqn': 'separated-role',
87+
'outputs': self.outputs['role']
88+
},
89+
'dummy': {
90+
'fqn': 'namespace-dummy',
91+
'outputs': self.outputs['dummy']
92+
}
93+
}
94+
})
95+
96+
def test_output_plain(self):
97+
with patch('sys.stdout', new=StringIO()) as fake_out:
98+
self.run_action(output_format='plain')
99+
100+
lines = fake_out.getvalue().strip().splitlines()
101+
102+
for stack_name, outputs in self.outputs.items():
103+
for key, value in outputs.items():
104+
line = '{}.{}={}'.format(stack_name, key, value)
105+
self.assertIn(line, lines)
106+
107+
def test_output_log(self):
108+
log_name = 'stacker.actions.info'
109+
with LogCapture(log_name) as logs:
110+
self.run_action(output_format='log')
111+
112+
def msg(s):
113+
return log_name, 'INFO', s
114+
115+
def msgs():
116+
yield msg('Outputs for stacks: namespace')
117+
for stack in sorted(self.stacks, key=lambda s: s.fqn):
118+
yield msg(stack.fqn + ':')
119+
for key, value in sorted(self.outputs[stack.name].items()):
120+
yield msg('\t{}: {}'.format(key, value))
121+
122+
logs.check(*msgs())

0 commit comments

Comments
 (0)