Skip to content

Commit 64323d3

Browse files
committed
commands: info: add configurable output formats
1 parent 053ad19 commit 64323d3

File tree

3 files changed

+210
-9
lines changed

3 files changed

+210
-9
lines changed

stacker/actions/info.py

+113-8
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,127 @@
22
from __future__ import division
33
from __future__ import absolute_import
44
import logging
5+
import json
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+
line = '{}.{}={}\n'.format(self.current_stack, key, value)
71+
sys.stdout.write(line)
72+
73+
def finish(self):
74+
sys.stdout.flush()
75+
76+
77+
class LogExporter(Exporter):
78+
def start(self):
79+
logger.info('Outputs for stacks: %s', self.context.get_fqn())
80+
81+
def start_stack(self, stack):
82+
logger.info('%s:', stack.fqn)
83+
84+
def write_output(self, key, value):
85+
logger.info('\t{}: {}'.format(key, value))
86+
87+
88+
EXPORTER_CLASSES = {
89+
'json': JsonExporter,
90+
'log': LogExporter,
91+
'plain': PlainExporter
92+
}
93+
94+
OUTPUT_FORMATS = list(EXPORTER_CLASSES.keys())
95+
96+
1297
class Action(BaseAction):
1398
"""Get information on CloudFormation stacks.
1499
15100
Displays the outputs for the set of CloudFormation stacks.
16101
17102
"""
18103

19-
def run(self, *args, **kwargs):
20-
logger.info('Outputs for stacks: %s', self.context.get_fqn())
104+
@classmethod
105+
def build_exporter(cls, name):
106+
try:
107+
exporter_cls = EXPORTER_CLASSES[name]
108+
except KeyError:
109+
logger.error('Unknown output format "{}"'.format(name))
110+
raise
111+
112+
try:
113+
return exporter_cls()
114+
except Exception:
115+
logger.exception('Failed to create exporter instance')
116+
raise
117+
118+
def run(self, output_format='log', *args, **kwargs):
21119
if not self.context.get_stacks():
22120
logger.warn('WARNING: No stacks detected (error in config?)')
121+
return
122+
123+
exporter = self.build_exporter(output_format)
124+
exporter.start(self.context)
125+
23126
for stack in self.context.get_stacks():
24127
provider = self.build_provider(stack)
25128

@@ -29,11 +132,13 @@ def run(self, *args, **kwargs):
29132
logger.info('Stack "%s" does not exist.' % (stack.fqn,))
30133
continue
31134

32-
logger.info('%s:', stack.fqn)
135+
exporter.start_stack(stack)
136+
33137
if 'Outputs' in provider_stack:
34138
for output in provider_stack['Outputs']:
35-
logger.info(
36-
'\t%s: %s',
37-
output['OutputKey'],
38-
output['OutputValue']
39-
)
139+
exporter.write_output(output['OutputKey'],
140+
output['OutputValue'])
141+
142+
exporter.end_stack(stack)
143+
144+
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

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
10+
from stacker.context import Context, Config
11+
from stacker.actions.info import (
12+
JsonExporter,
13+
PlainExporter
14+
)
15+
16+
17+
def stack_mock(name, **kwargs):
18+
m = Mock(**kwargs)
19+
m.name = name
20+
return m
21+
22+
23+
class TestExporters(unittest.TestCase):
24+
def setUp(self):
25+
self.context = Context(config=Config({"namespace": "namespace"}))
26+
self.stacks = [
27+
stack_mock(name='vpc', fqn='namespace-test-1'),
28+
stack_mock(name='bucket', fqn='namespace-test-2'),
29+
stack_mock(name='role', fqn='namespace-test-3')
30+
]
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:::'
42+
}
43+
}
44+
45+
def run_export(self, exporter):
46+
exporter.start()
47+
48+
for stack in self.stacks:
49+
exporter.start_stack(stack)
50+
for key, value in self.outputs[stack.name].items():
51+
exporter.write_output(key, value)
52+
exporter.end_stack(stack)
53+
54+
exporter.finish()
55+
56+
def test_json(self):
57+
exporter = JsonExporter(self.context)
58+
with patch('sys.stdout', new=StringIO()) as fake_out:
59+
self.run_export(exporter)
60+
61+
json_data = json.loads(fake_out.getvalue().strip())
62+
self.assertEqual(
63+
json_data,
64+
{
65+
u'stacks': {
66+
u'vpc': {
67+
u'fqn': u'namespace-vpc',
68+
u'outputs': self.outputs['vpc']
69+
},
70+
'bucket': {
71+
u'fqn': u'namespace-bucket',
72+
u'outputs': self.outputs['bucket']
73+
},
74+
u'role': {
75+
u'fqn': u'namespace-role',
76+
u'outputs': self.outputs['role']
77+
}
78+
}
79+
})
80+
81+
def test_plain(self):
82+
exporter = PlainExporter(self.context)
83+
with patch('sys.stdout', new=StringIO()) as fake_out:
84+
self.run_export(exporter)
85+
86+
lines = fake_out.getvalue().strip().splitlines()
87+
88+
for stack_name, outputs in self.outputs.items():
89+
for key, value in outputs.items():
90+
line = '{}.{}={}'.format(stack_name, key, value)
91+
self.assertIn(line, lines)

0 commit comments

Comments
 (0)