moonshot 0.7.7 → 1.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/moonshot +11 -0
- data/lib/default/Moonfile.rb +0 -0
- data/lib/moonshot.rb +33 -6
- data/lib/moonshot/always_use_default_source.rb +17 -0
- data/lib/moonshot/artifact_repository/s3_bucket_via_github_releases.rb +140 -3
- data/lib/moonshot/ask_user_source.rb +38 -0
- data/lib/moonshot/build_mechanism/github_release.rb +1 -1
- data/lib/moonshot/build_mechanism/script.rb +1 -1
- data/lib/moonshot/command.rb +64 -0
- data/lib/moonshot/command_line.rb +150 -0
- data/lib/moonshot/commands/build.rb +12 -0
- data/lib/moonshot/commands/console.rb +19 -0
- data/lib/moonshot/commands/create.rb +37 -0
- data/lib/moonshot/commands/delete.rb +12 -0
- data/lib/moonshot/commands/deploy.rb +12 -0
- data/lib/moonshot/commands/doctor.rb +12 -0
- data/lib/moonshot/commands/list.rb +16 -0
- data/lib/moonshot/commands/new.rb +99 -0
- data/lib/moonshot/commands/parameter_arguments.rb +27 -0
- data/lib/moonshot/commands/push.rb +12 -0
- data/lib/moonshot/commands/ssh.rb +12 -0
- data/lib/moonshot/commands/status.rb +12 -0
- data/lib/moonshot/commands/update.rb +29 -0
- data/lib/moonshot/commands/version.rb +12 -0
- data/lib/moonshot/config.rb +0 -0
- data/lib/moonshot/controller.rb +106 -42
- data/lib/moonshot/controller_config.rb +31 -13
- data/lib/moonshot/deployment_mechanism/code_deploy.rb +17 -7
- data/lib/moonshot/json_stack_template.rb +17 -0
- data/lib/moonshot/parameter_collection.rb +50 -0
- data/lib/moonshot/parent_stack_parameter_loader.rb +51 -0
- data/lib/moonshot/resources.rb +3 -3
- data/lib/moonshot/resources_helper.rb +2 -2
- data/lib/moonshot/ssh_command.rb +31 -0
- data/lib/moonshot/ssh_config.rb +1 -1
- data/lib/moonshot/stack.rb +66 -77
- data/lib/moonshot/stack_list_printer.rb +21 -0
- data/lib/moonshot/stack_lister.rb +16 -6
- data/lib/moonshot/stack_parameter.rb +64 -0
- data/lib/moonshot/stack_parameter_printer.rb +3 -49
- data/lib/moonshot/stack_template.rb +13 -25
- data/lib/moonshot/task.rb +10 -0
- data/lib/moonshot/tools/asg_rollout.rb +1 -1
- data/lib/moonshot/tools/asg_rollout/instance_health.rb +1 -1
- data/lib/moonshot/tools/asg_rollout_config.rb +1 -1
- data/lib/moonshot/yaml_stack_template.rb +17 -0
- metadata +51 -9
- data/lib/moonshot/cli.rb +0 -220
- data/lib/moonshot/environment_parser.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19e43423a8173b2f19405834ed09736d99a39b51
|
4
|
+
data.tar.gz: 4b344bc4416972f25320e35510e381aae44bfe7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9807b9a1a23c3f395077ef83b5c950b43b0194fa1e981a1300381ef43a638f3950756ada942fb43b85105fc643d0232f939f2414ded42ee54dea30f5a5be6a83
|
7
|
+
data.tar.gz: 26bd43f893b325342bd7b6825b3045f0cbb88593efc1bc3916adb5751940ab70b116af6a5ef77a6a144869492048f2e5c4713d8694ab58d70bd8329a1b2f78d2
|
data/bin/moonshot
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'moonshot'
|
3
|
+
|
4
|
+
# This is the main entry point for the `moonshot` command-line tool.
|
5
|
+
begin
|
6
|
+
Moonshot::CommandLine.new.run!
|
7
|
+
rescue => e
|
8
|
+
warn "#{e} (at #{e.backtrace.first})"
|
9
|
+
raise e if ENV['MOONSHOT_BACKTRACE']
|
10
|
+
exit 1
|
11
|
+
end
|
File without changes
|
data/lib/moonshot.rb
CHANGED
@@ -2,15 +2,25 @@ require 'English'
|
|
2
2
|
require 'aws-sdk'
|
3
3
|
require 'logger'
|
4
4
|
require 'thor'
|
5
|
+
require 'interactive-logger'
|
5
6
|
|
6
7
|
module Moonshot
|
7
|
-
|
8
|
+
class << self
|
9
|
+
attr_writer :config
|
8
10
|
end
|
9
|
-
|
11
|
+
|
12
|
+
def self.config
|
13
|
+
@config ||= Moonshot::ControllerConfig.new
|
14
|
+
block_given? ? yield(@config) : @config
|
15
|
+
end
|
16
|
+
|
17
|
+
module ArtifactRepository
|
18
|
+
end
|
19
|
+
module BuildMechanism
|
10
20
|
end
|
11
|
-
module DeploymentMechanism
|
21
|
+
module DeploymentMechanism
|
12
22
|
end
|
13
|
-
module Plugins
|
23
|
+
module Plugins
|
14
24
|
end
|
15
25
|
end
|
16
26
|
|
@@ -20,17 +30,34 @@ end
|
|
20
30
|
'doctor_helper',
|
21
31
|
'resources',
|
22
32
|
'resources_helper',
|
23
|
-
'environment_parser',
|
24
33
|
|
25
34
|
# Core
|
26
35
|
'interactive_logger_proxy',
|
36
|
+
'command_line',
|
37
|
+
'command',
|
38
|
+
'ssh_command',
|
39
|
+
'commands/build',
|
40
|
+
'commands/console',
|
41
|
+
'commands/create',
|
42
|
+
'commands/delete',
|
43
|
+
'commands/deploy',
|
44
|
+
'commands/doctor',
|
45
|
+
'commands/list',
|
46
|
+
'commands/push',
|
47
|
+
'commands/ssh',
|
48
|
+
'commands/status',
|
49
|
+
'commands/update',
|
50
|
+
'commands/version',
|
27
51
|
'controller',
|
28
52
|
'controller_config',
|
29
|
-
'cli',
|
30
53
|
'stack',
|
31
54
|
'stack_config',
|
32
55
|
'stack_lister',
|
33
56
|
'stack_events_poller',
|
57
|
+
'merge_strategy',
|
58
|
+
'default_strategy',
|
59
|
+
'ask_user_source',
|
60
|
+
'always_use_default_source',
|
34
61
|
|
35
62
|
# Built-in mechanisms
|
36
63
|
'artifact_repository/s3_bucket',
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Moonshot
|
2
|
+
# The AlwaysUseDefaultSource will always use the previous value in
|
3
|
+
# the stack, or use the default value during stack creation. This is
|
4
|
+
# useful if plugins provide the value for a parameter, and we don't
|
5
|
+
# want to prompt the user for an override. Of course, overrides from
|
6
|
+
# answer files or command-line arguments will always apply.
|
7
|
+
class AlwaysUseDefaultSource
|
8
|
+
def get(sp)
|
9
|
+
unless sp.default?
|
10
|
+
raise "Parameter #{sp.name} does not have a default, cannot use AlwaysUseDefaultSource!"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Don't do anything, the default will apply on create, and the
|
14
|
+
# previous value will be used on update.
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'moonshot/artifact_repository/s3_bucket'
|
2
2
|
require 'moonshot/shell'
|
3
|
+
require 'digest'
|
3
4
|
require 'securerandom'
|
4
5
|
require 'semantic'
|
5
6
|
require 'tmpdir'
|
@@ -7,7 +8,7 @@ require 'tmpdir'
|
|
7
8
|
module Moonshot::ArtifactRepository
|
8
9
|
# S3 Bucket repository backed by GitHub releases.
|
9
10
|
# If a SemVer package isn't found in S3, it is copied from GitHub releases.
|
10
|
-
class S3BucketViaGithubReleases < S3Bucket
|
11
|
+
class S3BucketViaGithubReleases < S3Bucket # rubocop:disable ClassLength
|
11
12
|
include Moonshot::BuildMechanism
|
12
13
|
include Moonshot::Shell
|
13
14
|
|
@@ -57,6 +58,12 @@ module Moonshot::ArtifactRepository
|
|
57
58
|
def attach_release_asset(version, file)
|
58
59
|
# -m '' leaves message unchanged.
|
59
60
|
cmd = "hub release edit #{version} -m '' --attach=#{file}"
|
61
|
+
|
62
|
+
# If there is a checksum file, attach it as well. We only support MD5
|
63
|
+
# since that's what S3 uses.
|
64
|
+
checksum_file = File.basename(file, '.tar.gz') + '.md5'
|
65
|
+
cmd += " --attach=#{checksum_file}" if File.exist?(checksum_file)
|
66
|
+
|
60
67
|
sh_step(cmd)
|
61
68
|
end
|
62
69
|
|
@@ -70,13 +77,143 @@ module Moonshot::ArtifactRepository
|
|
70
77
|
def github_to_s3(version, s3_name)
|
71
78
|
Dir.mktmpdir('github_to_s3', Dir.getwd) do |tmpdir|
|
72
79
|
Dir.chdir(tmpdir) do
|
73
|
-
|
74
|
-
file = Dir.glob("*#{version}*.tar.gz").fetch(0)
|
80
|
+
file = download_from_github(version)
|
75
81
|
upload_to_s3(file, s3_name)
|
76
82
|
end
|
77
83
|
end
|
78
84
|
end
|
79
85
|
|
86
|
+
# Uploads the file to s3 and verifies the checksum.
|
87
|
+
#
|
88
|
+
# @param file [String] File to be uploaded to s3.
|
89
|
+
# @param key [String] Name of the object to be created on s3.
|
90
|
+
# @raise [RuntimeError] If the file fails to upload correctly after 3
|
91
|
+
# attempts.
|
92
|
+
def upload_to_s3(file, key)
|
93
|
+
attempts = 0
|
94
|
+
begin
|
95
|
+
super
|
96
|
+
|
97
|
+
unless (checksum = checksum_file(file)).nil?
|
98
|
+
verify_s3_checksum(key, checksum, attempt: attempts)
|
99
|
+
end
|
100
|
+
rescue RuntimeError => e
|
101
|
+
unless (attempts += 1) > 3
|
102
|
+
# Wait 10 seconds before trying again.
|
103
|
+
sleep 10
|
104
|
+
retry
|
105
|
+
end
|
106
|
+
|
107
|
+
raise e
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Downloads the release build from github and verifies the checksum.
|
112
|
+
#
|
113
|
+
# @param version [String] Version to be downloaded
|
114
|
+
# @param [String] Build file downloaded.
|
115
|
+
# @raise [RuntimeError] If the file fails to download correctly after 3
|
116
|
+
# attempts.
|
117
|
+
def download_from_github(version)
|
118
|
+
attempts = 0
|
119
|
+
begin
|
120
|
+
# Make sure the directory is empty before downloading the release.
|
121
|
+
FileUtils.rm(Dir.glob('*'))
|
122
|
+
|
123
|
+
# Download the release and find the actual build file.
|
124
|
+
sh_out("hub release download #{version}")
|
125
|
+
file = Dir.glob("*#{version}*.tar.gz").fetch(0)
|
126
|
+
|
127
|
+
unless (checksum = checksum_file(file)).nil?
|
128
|
+
verify_download_checksum(file, checksum, attempt: attempts)
|
129
|
+
end
|
130
|
+
rescue RuntimeError => e
|
131
|
+
attempts += 1
|
132
|
+
retry unless attempts > 3
|
133
|
+
raise e
|
134
|
+
end
|
135
|
+
|
136
|
+
file
|
137
|
+
end
|
138
|
+
|
139
|
+
# Find the checksum file for a release, if there is one.
|
140
|
+
#
|
141
|
+
# @param build_file [String] Build file to get the checksum for.
|
142
|
+
# @return [String] Checksum file or nil.
|
143
|
+
def checksum_file(build_file)
|
144
|
+
basename = File.basename(build_file, '.tar.gz')
|
145
|
+
Dir.glob("#{basename}.md5").fetch(0, nil)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Verifies the checksum for a file downloaded from github.
|
149
|
+
#
|
150
|
+
# @param build_file [String] Build file to verify.
|
151
|
+
# @param checksum_file [String] Checksum file to verify the build.
|
152
|
+
# @param attempt [Integer] The attempt for this verification.
|
153
|
+
def verify_download_checksum(build_file, checksum_file, attempt: 0)
|
154
|
+
expected = File.read(checksum_file)
|
155
|
+
actual = Digest::MD5.file(build_file).hexdigest
|
156
|
+
if actual != expected
|
157
|
+
log.error("GitHub fie #{build_file} checksum should be #{expected} " \
|
158
|
+
"but was #{actual}.")
|
159
|
+
backup_failed_github_file(build_file, attempt)
|
160
|
+
raise "Checksum for #{build_file} could not be verified."
|
161
|
+
end
|
162
|
+
|
163
|
+
log.info('Verified downloaded file checksum.')
|
164
|
+
end
|
165
|
+
|
166
|
+
# Backs up the failed file from a github verification.
|
167
|
+
#
|
168
|
+
# @param build_file [String] The build file to backup.
|
169
|
+
# @param attempt [Integer] Which attempt to verify the file failed.
|
170
|
+
def backup_failed_github_file(build_file, attempt)
|
171
|
+
basename = File.basename(build_file, '.tar.gz')
|
172
|
+
destination = File.join(Dir.tmpdir, basename,
|
173
|
+
".gh.failure.#{attempt}.tar.gz")
|
174
|
+
FileUtils.cp(build_file, destination)
|
175
|
+
log.info("Copied #{build_file} to #{destination}")
|
176
|
+
end
|
177
|
+
|
178
|
+
# Verifies the checksum for a file uploaded to s3.
|
179
|
+
#
|
180
|
+
# Uses a HEAD request and uses the etag, which is an MD5 hash.
|
181
|
+
#
|
182
|
+
# @param s3_name [String] The object's name on s3.
|
183
|
+
# @param checksum_file [String] Checksum file to verify the build.
|
184
|
+
# @param attempt [Integer] The attempt for this verification.
|
185
|
+
def verify_s3_checksum(s3_name, checksum_file, attempt: 0)
|
186
|
+
headers = s3_client.head_object(
|
187
|
+
key: s3_name,
|
188
|
+
bucket: @bucket_name
|
189
|
+
)
|
190
|
+
expected = File.read(checksum_file)
|
191
|
+
actual = headers.etag.tr('"', '')
|
192
|
+
if actual != expected
|
193
|
+
log.error("S3 file #{s3_name} checksum should be #{expected} but " \
|
194
|
+
"was #{actual}.")
|
195
|
+
backup_failed_s3_file(s3_name, attempt)
|
196
|
+
raise "Checksum for #{s3_name} could not be verified."
|
197
|
+
end
|
198
|
+
|
199
|
+
log.info('Verified uploaded file checksum.')
|
200
|
+
end
|
201
|
+
|
202
|
+
# Backs up the failed file from an s3 verification.
|
203
|
+
#
|
204
|
+
# @param s3_name [String] The object's name on s3.
|
205
|
+
# @param attempt [Integer] Which attempt to verify the file failed.
|
206
|
+
def backup_failed_s3_file(s3_name, attempt)
|
207
|
+
basename = File.basename(s3_name, '.tar.gz')
|
208
|
+
destination = "#{Dir.tmpdir}/#{basename}.s3.failure.#{attempt}.tar.gz"
|
209
|
+
s3_client.get_object(
|
210
|
+
response_target: destination,
|
211
|
+
key: s3_name,
|
212
|
+
bucket: @bucket_name
|
213
|
+
)
|
214
|
+
log.info("Copied #{s3_name} to #{destination}")
|
215
|
+
end
|
216
|
+
|
80
217
|
def doctor_check_hub_release_download
|
81
218
|
sh_out('hub release download --help')
|
82
219
|
rescue
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module Moonshot
|
4
|
+
class AskUserSource
|
5
|
+
def get(sp)
|
6
|
+
return unless Moonshot.config.interactive
|
7
|
+
|
8
|
+
@sp = sp
|
9
|
+
|
10
|
+
prompt
|
11
|
+
loop do
|
12
|
+
input = gets.chomp
|
13
|
+
|
14
|
+
if String(input).empty? && @sp.default?
|
15
|
+
# We will use the default value, print it here so the output is clear.
|
16
|
+
puts 'Using default value.'
|
17
|
+
return
|
18
|
+
elsif String(input).empty?
|
19
|
+
puts "Cannot proceed without value for #{@sp.name}!"
|
20
|
+
else
|
21
|
+
@sp.set(String(input))
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
prompt
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def prompt
|
32
|
+
print "(#{@sp.name})".light_black
|
33
|
+
print " #{@sp.description}" unless @sp.description.empty?
|
34
|
+
print " [#{@sp.default}]".light_black if @sp.default?
|
35
|
+
print ': '
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -72,7 +72,7 @@ module Moonshot::BuildMechanism
|
|
72
72
|
say("#{@changes}\n\n")
|
73
73
|
|
74
74
|
q = "Do you want to tag and release this commit as #{version}? [y/n]"
|
75
|
-
raise
|
75
|
+
raise 'Release declined.' unless yes?(q)
|
76
76
|
end
|
77
77
|
|
78
78
|
def git_tag(tag, sha, annotation)
|
@@ -41,7 +41,7 @@ class Moonshot::BuildMechanism::Script
|
|
41
41
|
|
42
42
|
def post_build_hook(_version)
|
43
43
|
unless File.exist?(@output_file) # rubocop:disable GuardClause
|
44
|
-
raise
|
44
|
+
raise 'Build command did not produce output file!'
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Moonshot
|
4
|
+
# A Command that is automatically registered with the Moonshot::CommandLine
|
5
|
+
class Command
|
6
|
+
module ClassMethods
|
7
|
+
# TODO: Can we auto-generate usage for commands with no positional arguments, at least?
|
8
|
+
attr_accessor :usage, :description
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.inherited(base)
|
12
|
+
Moonshot::CommandLine.register(base)
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
def parser
|
17
|
+
@use_interactive_logger = true
|
18
|
+
|
19
|
+
OptionParser.new do |o|
|
20
|
+
o.banner = "Usage: moonshot #{self.class.usage}"
|
21
|
+
|
22
|
+
o.on('-v', '--[no-]verbose', 'Show debug logging') do |v|
|
23
|
+
Moonshot.config.interactive_logger.debug = true if v
|
24
|
+
end
|
25
|
+
|
26
|
+
o.on('-nNAME', '--environment=NAME', 'Which environment to operate on.') do |v|
|
27
|
+
Moonshot.config.environment_name = v
|
28
|
+
end
|
29
|
+
|
30
|
+
o.on('--[no-]interactive-logger', TrueClass, 'Enable or disable fancy logging') do |v|
|
31
|
+
@use_interactive_logger = v
|
32
|
+
end
|
33
|
+
|
34
|
+
o.on('--[no-]show-all-events', FalseClass, 'Show all stack events during update') do |v|
|
35
|
+
Moonshot.config.show_all_stack_events = v
|
36
|
+
end
|
37
|
+
|
38
|
+
o.on('-pPARENT_STACK', '--parent=PARENT_STACK',
|
39
|
+
'Parent stack to import parameters from') do |v|
|
40
|
+
Moonshot.config.parent_stacks = [v]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Build a Moonshot::Controller from the CLI options.
|
48
|
+
def controller
|
49
|
+
controller = Moonshot::Controller.new
|
50
|
+
|
51
|
+
# Apply CLI options to configuration defined by Moonfile.
|
52
|
+
controller.config = Moonshot.config
|
53
|
+
|
54
|
+
# Degrade to a more compatible logger if the terminal seems outdated,
|
55
|
+
# or at the users request.
|
56
|
+
if !$stdout.isatty || !@use_interactive_logger
|
57
|
+
log = Logger.new(STDOUT)
|
58
|
+
controller.config.interactive_logger = InteractiveLoggerProxy.new(log)
|
59
|
+
end
|
60
|
+
|
61
|
+
controller
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Moonshot
|
4
|
+
# This class implements the command-line `moonshot` tool.
|
5
|
+
class CommandLine
|
6
|
+
def self.register(klass)
|
7
|
+
@classes ||= []
|
8
|
+
@classes << klass
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.registered_commands
|
12
|
+
@classes || []
|
13
|
+
end
|
14
|
+
|
15
|
+
def run! # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength, PerceivedComplexity
|
16
|
+
# Commands defined as Moonshot::Commands require a properly
|
17
|
+
# configured Moonshot.rb and supporting files. Without them, we only
|
18
|
+
# support `--help` and `new`.
|
19
|
+
return if handle_early_commands
|
20
|
+
|
21
|
+
# Find the Moonfile in this project.
|
22
|
+
orig_dir = Dir.pwd
|
23
|
+
|
24
|
+
loop do
|
25
|
+
break if File.exist?('Moonfile.rb')
|
26
|
+
|
27
|
+
if Dir.pwd == '/'
|
28
|
+
warn 'No Moonfile.rb found, are you in a project? Maybe you need to '\
|
29
|
+
'create one with `moonshot new <app_name>`?'
|
30
|
+
raise 'No Moonfile found'
|
31
|
+
end
|
32
|
+
|
33
|
+
Dir.chdir('..')
|
34
|
+
end
|
35
|
+
|
36
|
+
moonfile_dir = Dir.pwd
|
37
|
+
Dir.chdir(orig_dir)
|
38
|
+
|
39
|
+
# Load any plugins and CLI extensions relative to the Moonfile
|
40
|
+
if File.directory?(File.join(moonfile_dir, 'moonshot'))
|
41
|
+
load_plugins(moonfile_dir)
|
42
|
+
load_cli_extensions(moonfile_dir)
|
43
|
+
end
|
44
|
+
|
45
|
+
Object.include(Moonshot::ArtifactRepository)
|
46
|
+
Object.include(Moonshot::BuildMechanism)
|
47
|
+
Object.include(Moonshot::DeploymentMechanism)
|
48
|
+
load(File.join(moonfile_dir, 'Moonfile.rb'))
|
49
|
+
|
50
|
+
Moonshot.config.project_root = moonfile_dir
|
51
|
+
|
52
|
+
load_commands
|
53
|
+
|
54
|
+
# Determine what command is being run, which should be the first argument.
|
55
|
+
command = ARGV.shift
|
56
|
+
if %w(--help -h help).include?(command) || command.nil?
|
57
|
+
usage
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
# Dispatch to that command, by executing it's parser, then
|
62
|
+
# comparing ARGV to the execute methods arity.
|
63
|
+
unless @commands.key?(command)
|
64
|
+
usage
|
65
|
+
raise "Command not found '#{command}'"
|
66
|
+
end
|
67
|
+
|
68
|
+
handler = @commands[command].new
|
69
|
+
handler.parser.parse!
|
70
|
+
|
71
|
+
unless ARGV.size == handler.method(:execute).arity
|
72
|
+
warn handler.parser.help
|
73
|
+
raise "Invalid command line for '#{command}'."
|
74
|
+
end
|
75
|
+
|
76
|
+
handler.execute(*ARGV)
|
77
|
+
end
|
78
|
+
|
79
|
+
def load_plugins(moonfile_dir)
|
80
|
+
plugins_path = File.join(moonfile_dir, 'moonshot', 'plugins', '**', '*.rb')
|
81
|
+
Dir.glob(plugins_path).each do |file|
|
82
|
+
load(file)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def load_cli_extensions(moonfile_dir)
|
87
|
+
cli_extensions_path = File.join(moonfile_dir, 'moonshot', 'cli_extensions', '**', '*.rb')
|
88
|
+
Dir.glob(cli_extensions_path).each do |file|
|
89
|
+
load(file)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def usage
|
94
|
+
warn 'Usage: moonshot [command]'
|
95
|
+
warn
|
96
|
+
warn 'Valid commands include:'
|
97
|
+
fields = []
|
98
|
+
@commands.each do |c, k|
|
99
|
+
fields << [c, k.description]
|
100
|
+
end
|
101
|
+
|
102
|
+
max_len = fields.map(&:first).map(&:size).max
|
103
|
+
|
104
|
+
fields.each do |f|
|
105
|
+
line = format(" %-#{max_len}s # %s", *f)
|
106
|
+
warn line
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def load_commands
|
111
|
+
@commands = {}
|
112
|
+
|
113
|
+
# Include all Moonshot::Command and Moonshot::SSHCommand
|
114
|
+
# derived classes as subcommands, with the description of their
|
115
|
+
# default task.
|
116
|
+
self.class.registered_commands.each do |klass|
|
117
|
+
next unless klass.instance_methods.include?(:execute)
|
118
|
+
|
119
|
+
command_name = commandify(klass)
|
120
|
+
@commands[command_name] = klass
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def commandify(klass)
|
125
|
+
word = klass.to_s.split('::').last
|
126
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
|
127
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
|
128
|
+
word.tr!('_'.freeze, '-'.freeze)
|
129
|
+
word.downcase!
|
130
|
+
word
|
131
|
+
end
|
132
|
+
|
133
|
+
def handle_early_commands
|
134
|
+
# If this is a legacy (Thor) help command, re-write it as
|
135
|
+
# OptionParser format.
|
136
|
+
if ARGV[0] == 'help'
|
137
|
+
ARGV.delete_at(0)
|
138
|
+
ARGV.push('-h')
|
139
|
+
elsif ARGV[0] == 'new'
|
140
|
+
require_relative 'commands/new'
|
141
|
+
app_name = ARGV[1]
|
142
|
+
::Moonshot::Commands::New.run!(app_name)
|
143
|
+
return true
|
144
|
+
end
|
145
|
+
|
146
|
+
# Proceed to processing commands normally.
|
147
|
+
false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|