1 | fs = require 'fs'
|
2 | path = require 'path'
|
3 | colors = require 'colors'
|
4 | commander = require 'commander'
|
5 | CoffeeScript = require 'coffee-script'
|
6 | {spawn, exec} = require 'child_process'
|
7 | parseTemplate = require './parseTemplate'
|
8 | commandHelper = require './commandHelper'
|
9 | coffinChar = '\u26B0'.grey
|
10 | checkChar = '\u2713'.green
|
11 | crossChar = '\u2717'.red
|
12 |
|
13 | validateArgs = ->
|
14 | valid = true
|
15 | if commander.args.length is 0
|
16 | console.error "You need to specify a coffin template to act on."
|
17 | valid = false
|
18 | if commander.validate? or commander.createStack? or commander.updateStack?
|
19 | if commander.print?
|
20 | console.error "I can't run that command if you're just printing to the console."
|
21 | valid = false
|
22 | if not process.env.AWS_CLOUDFORMATION_HOME and not commander['cfn-home']?
|
23 | console.error "Either an AWS_CLOUDFORMATION_HOME environment variable or a --cfnHome switch is required."
|
24 | valid = false
|
25 | if not valid
|
26 | process.stdout.write commander.helpInformation()
|
27 | process.exit 0
|
28 |
|
29 | compileTemplate = (source, params, callback) =>
|
30 | pre = "require('../lib/coffin') ->\n"
|
31 | fs.readFile source, (err, code) =>
|
32 | if err
|
33 | console.error "#{source} not found"
|
34 | process.exit 1
|
35 | tabbedLines = []
|
36 | if !params?
|
37 | params = []
|
38 | tabbedLines.push " @ARGV = #{JSON.stringify params}"
|
39 | (tabbedLines.push(' ' + line) for line in code.toString().split '\n')
|
40 | tabbedLines.push ' return'
|
41 | code = tabbedLines.join '\n'
|
42 | code = pre + code
|
43 | compiled = CoffeeScript.compile code, {source, bare: true}
|
44 | template = eval compiled, source
|
45 | templateString = if commander.pretty then JSON.stringify template, null, 2 else JSON.stringify template
|
46 | callback? templateString
|
47 |
|
48 | decompileCfnTemplate = (source, callback) ->
|
49 | fs.readFile source, "utf8", (err, cfnTemp) ->
|
50 | if err
|
51 | console.log "#{source} not found"
|
52 | process.exit 1
|
53 | decompiled = parseTemplate JSON.parse(cfnTemp)
|
54 | callback decompiled
|
55 |
|
56 | writeJsonTemplate = (json, templatePath, callback) ->
|
57 | write = ->
|
58 | json = ' ' if json.length <= 0
|
59 | fs.writeFile templatePath, json, (err) ->
|
60 | if err?
|
61 | console.error "failed to write to #{templatePath}"
|
62 | console.error err.message
|
63 | process.exit 1
|
64 | callback?()
|
65 | base = path.dirname templatePath
|
66 | path.exists base, (exists) ->
|
67 | if exists then write() else exec "mkdir -p #{base}", write
|
68 |
|
69 | generateTempFileName = ->
|
70 | e = process.env
|
71 | tmpDir = e.TMPDIR || e.TMP || e.TEMP || '/tmp'
|
72 | now = new Date()
|
73 | dateStamp = now.getYear()
|
74 | dateStamp <<= 4
|
75 | dateStamp |= now.getMonth()
|
76 | dateStamp <<= 5
|
77 | dateStamp |= now.getDay()
|
78 | rand = (Math.random() * 0x100000000 + 1).toString(36)
|
79 | name = "#{dateStamp.toString(36)}-#{process.pid.toString(36)}-#{rand}.template"
|
80 | path.join tmpDir, name
|
81 |
|
82 | generateOutputFileName = (source, extension) ->
|
83 | base = commander.output || path.dirname source
|
84 | filename = path.basename(source, path.extname(source)) + extension
|
85 | path.join base, filename
|
86 |
|
87 | buildCfnPath = ->
|
88 | cfnHome = commander['cfn-home'] || process.env.AWS_CLOUDFORMATION_HOME
|
89 | return path.normalize path.join cfnHome, 'bin'
|
90 |
|
91 | validateTemplate = (templatePath, callback) =>
|
92 | validateExec = spawn path.join(buildCfnPath(), 'cfn-validate-template'), ['--template-file', templatePath]
|
93 | errorText = ''
|
94 | resultText = ''
|
95 | validateExec.stderr.on 'data', (data) -> errorText += data.toString()
|
96 | validateExec.stdout.on 'data', (data) -> resultText += data.toString()
|
97 | validateExec.on 'exit', (code) ->
|
98 | if code is 0
|
99 | process.stdout.write "#{checkChar}\n"
|
100 | process.stdout.write resultText
|
101 | else
|
102 | process.stdout.write "#{crossChar}\n"
|
103 | process.stderr.write errorText
|
104 | callback?(code)
|
105 |
|
106 | updateOrCreateStack = (name, templatePath, compiled, callback) =>
|
107 | args = ['--template-file', templatePath, '--stack-name', name]
|
108 | if commandHelper.doesTemplateReferenceIAM compiled
|
109 | args.push '-c'
|
110 | args.push 'CAPABILITY_IAM'
|
111 | updateExec = spawn "#{buildCfnPath()}/cfn-update-stack", args
|
112 | updateErrorText = ''
|
113 | resultText = ''
|
114 | updateExec.stderr.on 'data', (data) -> updateErrorText += data.toString()
|
115 | updateExec.stdout.on 'data', (data) -> resultText += data.toString()
|
116 | updateExec.on 'exit', (code) ->
|
117 | existsSyncFunc = if fs.existsSync? then fs.existsSync else path.existsSync
|
118 | if existsSyncFunc "#{buildCfnPath()}/cfn-update-stack"
|
119 | if code is 0
|
120 | process.stdout.write "stack '#{name}' (updated) #{checkChar}\n"
|
121 | process.stdout.write resultText
|
122 | callback? code
|
123 | return
|
124 | if updateErrorText.match(/^cfn-update-stack: Malformed input-No updates are to be performed/)?
|
125 | process.stdout.write "stack '#{name}' (no changes)\n"
|
126 | process.stdout.write resultText
|
127 | callback? 0
|
128 | return
|
129 | if not updateErrorText.match(/^cfn-update-stack: Malformed input-Stack with ID\/name/)?
|
130 | console.error updateErrorText
|
131 | callback? code
|
132 | return
|
133 | createStack name, templatePath, compiled, callback
|
134 |
|
135 | createStack = (name, templatePath, compiled, callback) =>
|
136 | args = ['--template-file', templatePath, '--stack-name', name]
|
137 | if commandHelper.doesTemplateReferenceIAM compiled
|
138 | args.push '-c'
|
139 | args.push 'CAPABILITY_IAM'
|
140 | createExec = spawn "#{buildCfnPath()}/cfn-create-stack", args
|
141 | errorText = ''
|
142 | resultText = ''
|
143 | createExec.stdout.on 'data', (data) -> resultText += data.toString()
|
144 | createExec.stderr.on 'data', (data) -> errorText += data.toString()
|
145 | createExec.on 'exit', (code) ->
|
146 | if code isnt 0
|
147 | if errorText.match(/^cfn-create-stack: Malformed input-AlreadyExistsException/)?
|
148 | process.stderr.write "stack '#{name}' already exists #{crossChar}\n"
|
149 | return
|
150 | process.stderr.write errorText
|
151 | return
|
152 | process.stdout.write "stack '#{name}' (created) #{checkChar}\n"
|
153 | process.stdout.write resultText
|
154 | callback? code
|
155 |
|
156 |
|
157 | pretty =
|
158 | switch: '-p, --pretty'
|
159 | text: 'Add spaces and newlines to the resulting json to make it a little prettier'
|
160 |
|
161 | commander.version require('./coffin').version
|
162 | commander.usage '[options] <coffin template>'
|
163 |
|
164 | commander.option '-o, --output [dir]', 'Directory to output compiled file(s) to'
|
165 | commander.option '--cfn-home [dir]', 'The home of your AWS Cloudformation tools. Defaults to your AWS_CLOUDFORMATION_HOME environment variable.'
|
166 | commander.option pretty.switch, pretty.text
|
167 |
|
168 | printCommand = commander.command 'print [template]'
|
169 | printCommand.description 'Print the compiled template.'
|
170 | printCommand.action (template, params...) ->
|
171 | validateArgs()
|
172 | compileTemplate template, params, (compiled) ->
|
173 | console.log compiled
|
174 |
|
175 | validateCommand = commander.command 'validate [template]'
|
176 | validateCommand.description 'Validate the compiled template. Either an AWS_CLOUDFORMATION_HOME environment variable or a --cfn-home switch is required.'
|
177 | validateCommand.action (template, params...) ->
|
178 | validateArgs()
|
179 | compileTemplate template, params, (compiled) ->
|
180 | process.stdout.write "#{coffinChar} #{template} "
|
181 | tempFileName = generateTempFileName()
|
182 | writeJsonTemplate compiled, tempFileName, ->
|
183 | validateTemplate tempFileName, (resultCode) ->
|
184 |
|
185 | stackCommand = commander.command 'stack [name] [template]'
|
186 | stackCommand.description 'Create or update the named stack using the compiled template. Either an AWS_CLOUDFORMATION_HOME environment variable or a --cfn-home switch is required.'
|
187 | stackCommand.action (name, template, params...) ->
|
188 | validateArgs()
|
189 | compileTemplate template, params, (compiled) ->
|
190 | tempFileName = generateTempFileName()
|
191 | writeJsonTemplate compiled, tempFileName, ->
|
192 | process.stdout.write "#{coffinChar} #{template} -> "
|
193 | updateOrCreateStack name, tempFileName, compiled, (resultCode) ->
|
194 |
|
195 | compileCommand = commander.command 'compile [template]'
|
196 | compileCommand.description 'Compile and write the template. The output file will have the same name as the coffin template plus a shiny new ".template" extension.'
|
197 | compileCommand.action (template, params...) ->
|
198 | validateArgs()
|
199 | compileTemplate template, params, (compiled) ->
|
200 | process.stdout.write "#{coffinChar} #{template} -> "
|
201 | fileName = generateOutputFileName template, ".template"
|
202 | writeJsonTemplate compiled, fileName, ->
|
203 | process.stdout.write "#{fileName}\n"
|
204 |
|
205 | decompileCommand = commander.command 'decompile [cfn-template]'
|
206 | decompileCommand.description 'experimental - Convert the given cloud formation template to coffin (or as best as we can). It will output a file of the same name with ".coffin" extension.'
|
207 | decompileCommand.action (cfnTemplate) ->
|
208 | validateArgs()
|
209 | decompileCfnTemplate cfnTemplate, (decompiled) ->
|
210 | process.stdout.write "#{coffinChar} #{cfnTemplate} -> "
|
211 | fileName = generateOutputFileName cfnTemplate, ".coffin"
|
212 | writeJsonTemplate decompiled, fileName, ->
|
213 | process.stdout.write "#{fileName}\n"
|
214 |
|
215 |
|
216 |
|
217 | showHelp = ->
|
218 | process.stdout.write commander.helpInformation()
|
219 | process.exit 1
|
220 | commander.command('').action showHelp
|
221 | commander.command('*').action showHelp
|
222 |
|
223 | if process.argv.length <=2
|
224 | showHelp()
|
225 |
|
226 | module.exports.run = ->
|
227 | commander.parse process.argv
|