1 | #===============================================================================
|
2 | # Huxley - Command-Line Interface for the Awesome Deployment Framework, Huxley.
|
3 | #===============================================================================
|
4 | # This file specifies the Command-Line Interface for Huxley. Huxley keeps track
|
5 | # of configuration data, so by entering short and simple commands into the
|
6 | # command-line, you can manage complex deployments. This CLI interfaces with
|
7 | # the Huxley API, a wrapper around a number of open-source deployment components.
|
8 | #===============================================================================
|
9 | # Modules
|
10 | #===============================================================================
|
11 | # Core Libraries
|
12 | fs = require "fs"
|
13 | {resolve, join} = require "path"
|
14 |
|
15 | # Panda Strike Libraries
|
16 | Configurator = require "panda-config" # data file parsing
|
17 | {read, shell, merge, partial,
|
18 | map, sleep} =
|
19 | require "fairmont" # utility functions
|
20 |
|
21 |
|
22 | # Third Party Libraries
|
23 | {promise} = require "when" #|promise library
|
24 | {call} = require "when/generator" #|-----------------
|
25 | async = (require "when/generator").lift #|-----------------
|
26 | prompt = require "prompt" # interviewer generator
|
27 | {render} = require "mustache" # templating
|
28 |
|
29 | # Huxley Components
|
30 | api = require "./api-interface" # interface for huxley api
|
31 | config_helpers = require "./config-helpers" # helpers for config management
|
32 |
|
33 | templates_dir_relative = __dirname + "/../templates/"
|
34 |
|
35 | #===============================================================================
|
36 | # Helper Fucntions
|
37 | #===============================================================================
|
38 | # Just to ensure good error handling, catch any errors and bubble them up.
|
39 | catch_fail = (f) ->
|
40 | try
|
41 | f()
|
42 | catch e
|
43 | throw e
|
44 |
|
45 | # Output an Info Blurb and optional message.
|
46 | usage = async (entry, message) ->
|
47 | docs = yield read( resolve( __dirname, "..", "docs", entry ) )
|
48 | if message?
|
49 | throw "#{message}\n" + docs
|
50 | else
|
51 | throw docs
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | # Sometimes we don't care if a promise is rejected, we just need to wait for it to be done one way
|
57 | # or another. This function accepts an input promise but always resolves.
|
58 | force = async (f, args...) ->
|
59 | try
|
60 | yield f args...
|
61 | catch e
|
62 | null
|
63 |
|
64 |
|
65 | # In Huxley, configuration data is stored in multiple places. This function focuses on
|
66 | # two levels of configuartion.
|
67 | # (1) The first is the user's home configuration, located in the huxley dotfile
|
68 | # within their $HOME directory (holds persistent data attached to their account).
|
69 | # (2) The second is the application-level configuration, located in the repo's huxley
|
70 | # manifest file. TODO: Currently, we only look in the execution path, so we require the
|
71 | # CLI to be run in the repo's root directory. This should use an env variable like Node and git.
|
72 | pull_configuration = async () ->
|
73 | catch_fail ->
|
74 | if fs.existsSync join process.env.HOME, ".huxley"
|
75 | # Load the configuration from the $HOME directory.
|
76 | constructor = Configurator.make
|
77 | prefix: "."
|
78 | format: "yaml"
|
79 | paths: [ process.env.HOME ]
|
80 |
|
81 | home_config = constructor.make name: "huxley"
|
82 | yield home_config.load()
|
83 | else
|
84 | throw "You must establish a dotfile configuration in your $HOME directory, ~/.huxley"
|
85 |
|
86 | if fs.existsSync join process.cwd(), "huxley.yaml"
|
87 | # Load the application level configuration.
|
88 | constructor = Configurator.make
|
89 | extension: ".yaml"
|
90 | format: "yaml"
|
91 | paths: [ process.cwd() ]
|
92 |
|
93 | app_config = constructor.make name: "huxley"
|
94 | yield app_config.load()
|
95 |
|
96 | # Create an object that is the union of the two configurations. Huxley observes
|
97 | # configuration scope, so application configuration will override values set in the home-configuration.
|
98 | if app_config?
|
99 | config = merge home_config.data, app_config.data
|
100 | else
|
101 | config = home_config.data
|
102 |
|
103 | # Return an object we can use to make requests, but also return the panda-config instances
|
104 | # in case we need to save something.
|
105 | return {
|
106 | config: config
|
107 | home_config: home_config
|
108 | app_config: app_config if app_config?
|
109 | }
|
110 |
|
111 |
|
112 | #===============================================================================
|
113 | # Templatizing - huxley "init" & "mixin"
|
114 | #===============================================================================
|
115 | # TODO: move to separate file
|
116 |
|
117 | # removes the default settings, starts prompt
|
118 | prompt_setup = ->
|
119 | prompt.message = ""
|
120 | prompt.delimiter = ""
|
121 | prompt.start()
|
122 |
|
123 | # makes prompt yield-able
|
124 | prompt_wrapper = (prompt_list) ->
|
125 | new Promise (resolve, reject) ->
|
126 | prompt.get prompt_list, (err, res) ->
|
127 | if err
|
128 | return rejecet err
|
129 | resolve res
|
130 |
|
131 | # create directory if doesn't already exists
|
132 | mkdir_idempt = (path) ->
|
133 | if !fs.existsSync path
|
134 | fs.mkdirSync path
|
135 | else
|
136 | console.log "Warning: Launch folder #{path} already exists.\nProceeding to create huxley.yaml"
|
137 |
|
138 | # copy file
|
139 | copy_file = async (from_path, from_filename, destination_path, destination_filename) ->
|
140 | fs.writeFileSync (destination_path + "/" + destination_filename + ".yaml"), ""
|
141 |
|
142 | from_configurator = Configurator.make
|
143 | paths: [ from_path ]
|
144 | extension: ".yaml"
|
145 | from_configuration = from_configurator.make name: from_filename
|
146 | yield from_configuration.load()
|
147 | from_config = from_configuration.data
|
148 |
|
149 | # edit huxley.yaml
|
150 | configurator = Configurator.make
|
151 | paths: [ destination_path ]
|
152 | extension: ".yaml"
|
153 | configuration = configurator.make name: destination_filename
|
154 | yield configuration.load()
|
155 | configuration.data = from_config
|
156 | configuration.save()
|
157 |
|
158 | # append_file templates_dir_relative + "/#{component_name}", "#{component_name}", process.cwd(), "huxley"
|
159 | # #union_overwrite prompt_response, process.cwd(), "huxley"
|
160 | # files = [ "Dockerfile.template", "#{component_name}.service.template", "#{component_name}.yaml" ]
|
161 | # #files = [ "#{component_name}.yaml" ]
|
162 | # for file in files
|
163 | # fs.writeFileSync process.cwd() + "/launch/#{component_name}/#{file}",
|
164 | # fs.readFileSync(templates_dir_relative + "#{component_name}/#{file}")
|
165 |
|
166 | # huxley.yaml composition by copying template
|
167 | # FIXME: assuming .yaml for now
|
168 |
|
169 | append_file = async (from_path, from_filename, destination_path, destination_filename) ->
|
170 | from_configurator = Configurator.make
|
171 | paths: [ from_path ]
|
172 | extension: ".yaml"
|
173 | from_configuration = from_configurator.make name: from_filename
|
174 | yield from_configuration.load()
|
175 | from_config = from_configuration.data
|
176 |
|
177 | # edit huxley.yaml
|
178 | configurator = Configurator.make
|
179 | paths: [ destination_path ]
|
180 | extension: ".yaml"
|
181 | configuration = configurator.make name: destination_filename
|
182 | yield configuration.load()
|
183 | #configuration.data.services[component_name] = from_config
|
184 | if configuration.data.services?
|
185 | configuration.data.services[from_filename] = from_config
|
186 | else
|
187 | configuration.data.services = {}
|
188 | configuration.data.services[from_filename] = from_config
|
189 | configuration.save()
|
190 |
|
191 | union_overwrite = async (data, file_path, file_name) ->
|
192 | from_configurator = Configurator.make
|
193 | paths: [ from_path ]
|
194 | extension: ".yaml"
|
195 | from_configuration = from_configurator.make name: from_filename
|
196 | yield from_configuration.load()
|
197 | from_config = from_configuration.data
|
198 |
|
199 | render_template_wrapper = async ({component_name, template_filename, output_filename}) ->
|
200 | template_path = join templates_dir_relative, "#{component_name}/#{template_filename}"
|
201 |
|
202 | # read in the default component.yaml (to make accessable as CSON)
|
203 | configurator = Configurator.make
|
204 | paths: [ process.cwd() ]
|
205 | extension: ".yaml"
|
206 | configuration = configurator.make name: "huxley"
|
207 | yield configuration.load()
|
208 |
|
209 | template = yield read resolve template_path
|
210 |
|
211 | rendered_string = yield render template, configuration.data
|
212 | yield write join( process.cwd(), "/launch/#{component_name}/#{output_filename}"), rendered_string
|
213 |
|
214 | # initialize mixin folders, copy over templates
|
215 | init_mixin = (component_name) ->
|
216 | mkdir_idempt process.cwd() + "/launch/#{component_name}"
|
217 |
|
218 | append_file templates_dir_relative + "/#{component_name}", "#{component_name}", process.cwd(), "huxley"
|
219 | #union_overwrite prompt_response, process.cwd(), "huxley"
|
220 | files = [ "Dockerfile.template", "#{component_name}.service.template", "#{component_name}.yaml" ]
|
221 | for file in files
|
222 | template_read_path = templates_dir_relative + "#{component_name}/#{file}"
|
223 | #console.log "****** template read path: ", template_read_path
|
224 | fs.writeFileSync process.cwd() + "/launch/#{component_name}/#{file}",
|
225 | fs.readFileSync(template_read_path)
|
226 |
|
227 |
|
228 |
|
229 | #===============================================================================
|
230 | # Sub-Command Handling
|
231 | #===============================================================================
|
232 | # This function prepares the "options" object to ask the API server to create a
|
233 | # CoreOS cluster using your AWS credentials.
|
234 | create_cluster = async (argv) ->
|
235 | # Detect if we should provide a help blurb.
|
236 | if argv[0] == "help" || argv[0] == "-h" || argv[0] == "--help"
|
237 | yield usage "cluster_create"
|
238 |
|
239 | # Start by reading configuration data from the local config files.
|
240 | {config, home_config} = yield pull_configuration()
|
241 |
|
242 | # Check to see if this cluster has already been registered in the API.
|
243 | yield config_helpers.check_create_cluster config, argv
|
244 |
|
245 | # Now use this raw configuration as context to build an "options" object for panda-hook.
|
246 | options = yield config_helpers.build_create_cluster config, argv
|
247 |
|
248 | # With our object built, call the Huxley API.
|
249 | response = yield api.create_cluster options
|
250 |
|
251 | # Save the cluster ID to the root-level configuration.
|
252 | yield config_helpers.update_create_cluster home_config, options, response
|
253 |
|
254 |
|
255 | # This function prepares the "options" object to ask the API server to delete a
|
256 | # CoreOS cluster using your AWS credentials.
|
257 | delete_cluster = async (argv) ->
|
258 | catch_fail ->
|
259 | # Detect if we should provide a help blurb.
|
260 | if argv.length == 0 || argv[0] == "help" || argv[0] == "-h" || argv[0] == "--help"
|
261 | yield usage "cluster_delete"
|
262 |
|
263 | # Start by reading configuration data from the local config files.
|
264 | {config, home_config} = yield pull_configuration()
|
265 |
|
266 | # Check to see if this cluster is registered in the API. We cannot delete what does not exist.
|
267 | yield config_helpers.check_delete_cluster config, argv
|
268 |
|
269 | # Now use this raw configuration as context to build an "options" object for panda-cluster.
|
270 | options = yield config_helpers.build_delete_cluster config, argv
|
271 |
|
272 | # With our object built, call the Huxley API.
|
273 | yield api.delete_cluster options
|
274 |
|
275 | # Save the deletion to the root-level configuration.
|
276 | yield config_helpers.update_delete_cluster home_config, argv
|
277 |
|
278 |
|
279 | # This function prepares the "options" object to ask the API server to poll AWS
|
280 | # about the status of your cluster.
|
281 | poll_cluster = async (argv) ->
|
282 | # Detect if we should provide a help blurb.
|
283 | if argv.length == 0 || argv[0] == "help" || argv[0] == "-h" || argv[0] == "--help"
|
284 | yield usage "cluster_poll"
|
285 |
|
286 | # Start by reading configuration data from the local config files.
|
287 | {config} = yield pull_configuration()
|
288 |
|
289 | # Check to see if this cluster is registered in the API. We cannot delete what does not exist.
|
290 | yield config_helpers.check_poll_cluster config, argv
|
291 |
|
292 | # Now use this raw configuration as context to build an "options" object for panda-cluster.
|
293 | options = yield config_helpers.build_poll_cluster config, argv
|
294 |
|
295 | # With our object built, call the Huxley API.
|
296 | yield api.poll_cluster options
|
297 |
|
298 |
|
299 |
|
300 | # This function prepares the "options" object to ask the API server to place a githook
|
301 | # on the cluster's hook server. Then it adds to the local machine's git aliases.
|
302 | add_remote = async (argv) ->
|
303 | catch_fail ->
|
304 | # Detect if we should provide a help blurb.
|
305 | if argv.length == 0 || argv[0] == "help" || argv[0] == "-h" || argv[0] == "--help"
|
306 | yield usage "remote_add"
|
307 |
|
308 | # Start by reading configuration data from the local config files.
|
309 | {config, app_config} = yield pull_configuration()
|
310 |
|
311 | # Check to see if this remote has already been registered in the API.
|
312 | yield config_helpers.check_add_remote config, argv
|
313 |
|
314 | # Now use this raw configuration as context to build an "options" object for panda-hook.
|
315 | options = yield config_helpers.build_add_remote config, argv
|
316 |
|
317 | # With our object built, call the Huxley API.
|
318 | console.log "Installing.... One moment."
|
319 | response = yield api.add_remote options
|
320 |
|
321 | # Now, add a "git remote" alias using the cluster name. The first command is allowed to fail.
|
322 | yield force shell, "git remote rm #{argv[0]}"
|
323 | yield shell "git remote add #{argv[0]} ssh://#{options.hook_address}/root/repos/#{options.repo_name}.git"
|
324 |
|
325 | # Save the remote ID to app-level configuration.
|
326 | yield config_helpers.update_add_remote app_config, argv, response
|
327 |
|
328 |
|
329 | # Not everything we place onto the cluster needs to trigger a cascade of deployment events.
|
330 | # Sometimes we just need to store data at the scope of the cluster and have it available to
|
331 | # be pulled when required. Compared to what we do with other repos on the hook server,
|
332 | # these are referred to as "passive" repositories, available at git:<hook-server>:3000/passive/<repo-name>
|
333 | passive_remote = async (argv) ->
|
334 | catch_fail ->
|
335 | # Detect if we should provide a help blurb.
|
336 | if argv.length == 0 || argv[0] == "help" || argv[0] == "-h" || argv[0] == "--help"
|
337 | yield usage "remote_passive"
|
338 |
|
339 | # Start by reading configuration data from the local config files.
|
340 | {config} = yield pull_configuration()
|
341 |
|
342 | # For now, this doesn't need to be routed though the API server. Execute a series of shell commands.
|
343 | yield run_passive_remote config, argv
|
344 |
|
345 |
|
346 | # This function prepares the "options" object to ask the API server to remove a githook
|
347 | # on the cluster's hook server. Then it removes one of the local machine's git aliases.
|
348 | rm_remote = async (argv) ->
|
349 | catch_fail ->
|
350 | # Detect if we should provide a help blurb.
|
351 | if argv.length == 0 || argv[0] == "help" || argv[0] == "-h" || argv[0] == "--help"
|
352 | yield usage "remote_passive"
|
353 |
|
354 | # Start by reading configuration data from the local config files.
|
355 | {config, app_config} = yield pull_configuration()
|
356 |
|
357 | # Check to see if this remote is registered in the API. We cannot delete what does not exist.
|
358 | yield config_helpers.check_rm_remote config, argv
|
359 |
|
360 | # Now use this raw configuration as context to build an "options" object for panda-hook.
|
361 | options = yield config_helpers.build_rm_remote config, argv
|
362 |
|
363 | # With our object built, call the Huxley API.
|
364 | response = yield api.rm_remote options
|
365 |
|
366 | # Remove a git remote alias using the cluster name. This command is allowed to fail.
|
367 | yield force shell, "git remote rm #{argv[0]}"
|
368 |
|
369 | # Remove the remote ID from the app-level configuration.
|
370 | yield config_helpers.update_rm_remote app_config, argv
|
371 |
|
372 |
|
373 |
|
374 |
|
375 | #===============================================================================
|
376 | # Main - Command-Line Interface
|
377 | #===============================================================================
|
378 | call ->
|
379 | # Chop off the argument array so that only the useful arguments remain.
|
380 | argv = process.argv[2..]
|
381 |
|
382 | # Deliver an info blurb if neccessary.
|
383 | if argv.length == 0 || argv[0] == "-h" || argv[0] == "--help" || argv[0] == "help"
|
384 | yield usage "main"
|
385 |
|
386 | # Now, look for the specified sub-command, assemble a configuration object, and hit the API.
|
387 | prompt_setup()
|
388 |
|
389 | try
|
390 | switch argv[0]
|
391 |
|
392 | when "cluster"
|
393 | switch argv[1]
|
394 | when "create"
|
395 | yield create_cluster argv[2..]
|
396 | when "delete"
|
397 | yield delete_cluster argv[2..]
|
398 | when "poll"
|
399 | yield poll_cluster argv[2..]
|
400 | else
|
401 | # When the command cannot be identified, display the help guide.
|
402 | yield usage "cluster", "\nError: Command Not Found: #{argv[1]} \n"
|
403 |
|
404 | when "init"
|
405 | copy_file templates_dir_relative, "default-config", process.cwd(), "huxley"
|
406 | # create /launch dir
|
407 | launch_dir = process.cwd() + "/launch"
|
408 | mkdir_idempt launch_dir
|
409 |
|
410 | when "mixin"
|
411 | switch argv[1]
|
412 |
|
413 | when "node"
|
414 | init_mixin "node"
|
415 | when "redis"
|
416 | # TODO: prompt for info, overwrite default
|
417 | init_mixin "redis"
|
418 | when "es" || "elasticsearch"
|
419 | # TODO: prompt for info, overwrite default
|
420 | init_mixin "elasticsearch"
|
421 | else
|
422 | # When the mixin cannot be identified, display the help guide.
|
423 | yield usage "mixin", "\nError: Unknown Mixin: #{argv[1]} \n"
|
424 |
|
425 | when "remote"
|
426 | switch argv[1]
|
427 | when "add"
|
428 | yield add_remote argv[2..]
|
429 | when "passive"
|
430 | yield passive_remote argv[2..]
|
431 | when "rm"
|
432 | yield rm_remote argv[2..]
|
433 | else
|
434 | # When the command cannot be identified, display the help guide.
|
435 | usage "remote", "\nError: Command Not Found: #{argv[1]} \n"
|
436 |
|
437 |
|
438 | else
|
439 | # When the command cannot be identified, display the help guide.
|
440 | yield usage "main", "\nError: Command Not Found: #{argv[0]} \n"
|
441 | catch error
|
442 | console.log "*****Apologies, there was an error: \n", error
|