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.
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