moonshot 0.7.7 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/bin/moonshot +11 -0
  3. data/lib/default/Moonfile.rb +0 -0
  4. data/lib/moonshot.rb +33 -6
  5. data/lib/moonshot/always_use_default_source.rb +17 -0
  6. data/lib/moonshot/artifact_repository/s3_bucket_via_github_releases.rb +140 -3
  7. data/lib/moonshot/ask_user_source.rb +38 -0
  8. data/lib/moonshot/build_mechanism/github_release.rb +1 -1
  9. data/lib/moonshot/build_mechanism/script.rb +1 -1
  10. data/lib/moonshot/command.rb +64 -0
  11. data/lib/moonshot/command_line.rb +150 -0
  12. data/lib/moonshot/commands/build.rb +12 -0
  13. data/lib/moonshot/commands/console.rb +19 -0
  14. data/lib/moonshot/commands/create.rb +37 -0
  15. data/lib/moonshot/commands/delete.rb +12 -0
  16. data/lib/moonshot/commands/deploy.rb +12 -0
  17. data/lib/moonshot/commands/doctor.rb +12 -0
  18. data/lib/moonshot/commands/list.rb +16 -0
  19. data/lib/moonshot/commands/new.rb +99 -0
  20. data/lib/moonshot/commands/parameter_arguments.rb +27 -0
  21. data/lib/moonshot/commands/push.rb +12 -0
  22. data/lib/moonshot/commands/ssh.rb +12 -0
  23. data/lib/moonshot/commands/status.rb +12 -0
  24. data/lib/moonshot/commands/update.rb +29 -0
  25. data/lib/moonshot/commands/version.rb +12 -0
  26. data/lib/moonshot/config.rb +0 -0
  27. data/lib/moonshot/controller.rb +106 -42
  28. data/lib/moonshot/controller_config.rb +31 -13
  29. data/lib/moonshot/deployment_mechanism/code_deploy.rb +17 -7
  30. data/lib/moonshot/json_stack_template.rb +17 -0
  31. data/lib/moonshot/parameter_collection.rb +50 -0
  32. data/lib/moonshot/parent_stack_parameter_loader.rb +51 -0
  33. data/lib/moonshot/resources.rb +3 -3
  34. data/lib/moonshot/resources_helper.rb +2 -2
  35. data/lib/moonshot/ssh_command.rb +31 -0
  36. data/lib/moonshot/ssh_config.rb +1 -1
  37. data/lib/moonshot/stack.rb +66 -77
  38. data/lib/moonshot/stack_list_printer.rb +21 -0
  39. data/lib/moonshot/stack_lister.rb +16 -6
  40. data/lib/moonshot/stack_parameter.rb +64 -0
  41. data/lib/moonshot/stack_parameter_printer.rb +3 -49
  42. data/lib/moonshot/stack_template.rb +13 -25
  43. data/lib/moonshot/task.rb +10 -0
  44. data/lib/moonshot/tools/asg_rollout.rb +1 -1
  45. data/lib/moonshot/tools/asg_rollout/instance_health.rb +1 -1
  46. data/lib/moonshot/tools/asg_rollout_config.rb +1 -1
  47. data/lib/moonshot/yaml_stack_template.rb +17 -0
  48. metadata +51 -9
  49. data/lib/moonshot/cli.rb +0 -220
  50. data/lib/moonshot/environment_parser.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a022cf1a54f893264b6ee7487303d79b8c29fb44
4
- data.tar.gz: 7425456a02ec0451c431c6dc1863616fae9200e5
3
+ metadata.gz: 19e43423a8173b2f19405834ed09736d99a39b51
4
+ data.tar.gz: 4b344bc4416972f25320e35510e381aae44bfe7b
5
5
  SHA512:
6
- metadata.gz: 8857003d9882c8fa70b559ba182887af90c5bd0b6cf2a62bc60e6bd4109c4e26e1f4ab6fd74bbc5764286cb592a1f70347659f0dfc79b8e13df104cc8356bc40
7
- data.tar.gz: 36b9bb90372c8ae2bacf1253061d243c2333f57095fb49f2741de6d2bab602bfaed19652b2bcb86a22df67dc8e45af1ea583b0a760105dc8474c40a570dfa538
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
- module ArtifactRepository # rubocop:disable Documentation
8
+ class << self
9
+ attr_writer :config
8
10
  end
9
- module BuildMechanism # rubocop:disable Documentation
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 # rubocop:disable Documentation
21
+ module DeploymentMechanism
12
22
  end
13
- module Plugins # rubocop:disable Documentation
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
- sh_out("hub release download #{version}")
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 Thor::Error, 'Release declined.' unless yes?(q)
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 Thor::Error, 'Build command did not produce output file!'
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