UNPKG

17.4 kBtext/coffeescriptView Raw
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
12fs = require "fs"
13{resolve, join} = require "path"
14
15# Panda Strike Libraries
16Configurator = 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" #|-----------------
25async = (require "when/generator").lift #|-----------------
26prompt = require "prompt" # interviewer generator
27{render} = require "mustache" # templating
28
29# Huxley Components
30api = require "./api-interface" # interface for huxley api
31config_helpers = require "./config-helpers" # helpers for config management
32
33templates_dir_relative = __dirname + "/../templates/"
34
35#===============================================================================
36# Helper Fucntions
37#===============================================================================
38# Just to ensure good error handling, catch any errors and bubble them up.
39catch_fail = (f) ->
40 try
41 f()
42 catch e
43 throw e
44
45# Output an Info Blurb and optional message.
46usage = 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.
58force = 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.
72pull_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
118prompt_setup = ->
119 prompt.message = ""
120 prompt.delimiter = ""
121 prompt.start()
122
123# makes prompt yield-able
124prompt_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
132mkdir_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
139copy_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
169append_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
191union_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
199render_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
215init_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.
234create_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.
257delete_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.
281poll_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.
302add_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>
333passive_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.
348rm_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#===============================================================================
378call ->
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