crazy-yard 3.2.2

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 (93) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +438 -0
  4. data/bin/ey +9 -0
  5. data/lib/engineyard.rb +9 -0
  6. data/lib/engineyard/cli.rb +816 -0
  7. data/lib/engineyard/cli/api.rb +98 -0
  8. data/lib/engineyard/cli/recipes.rb +129 -0
  9. data/lib/engineyard/cli/ui.rb +275 -0
  10. data/lib/engineyard/cli/web.rb +85 -0
  11. data/lib/engineyard/config.rb +158 -0
  12. data/lib/engineyard/deploy_config.rb +65 -0
  13. data/lib/engineyard/deploy_config/ref.rb +56 -0
  14. data/lib/engineyard/error.rb +82 -0
  15. data/lib/engineyard/eyrc.rb +59 -0
  16. data/lib/engineyard/repo.rb +105 -0
  17. data/lib/engineyard/serverside_runner.rb +159 -0
  18. data/lib/engineyard/templates.rb +6 -0
  19. data/lib/engineyard/templates/ey.yml.erb +196 -0
  20. data/lib/engineyard/templates/ey_yml.rb +119 -0
  21. data/lib/engineyard/thor.rb +215 -0
  22. data/lib/engineyard/version.rb +4 -0
  23. data/lib/vendor/thor/Gemfile +15 -0
  24. data/lib/vendor/thor/LICENSE.md +20 -0
  25. data/lib/vendor/thor/README.md +35 -0
  26. data/lib/vendor/thor/lib/thor.rb +473 -0
  27. data/lib/vendor/thor/lib/thor/actions.rb +318 -0
  28. data/lib/vendor/thor/lib/thor/actions/create_file.rb +105 -0
  29. data/lib/vendor/thor/lib/thor/actions/create_link.rb +60 -0
  30. data/lib/vendor/thor/lib/thor/actions/directory.rb +119 -0
  31. data/lib/vendor/thor/lib/thor/actions/empty_directory.rb +137 -0
  32. data/lib/vendor/thor/lib/thor/actions/file_manipulation.rb +314 -0
  33. data/lib/vendor/thor/lib/thor/actions/inject_into_file.rb +109 -0
  34. data/lib/vendor/thor/lib/thor/base.rb +652 -0
  35. data/lib/vendor/thor/lib/thor/command.rb +136 -0
  36. data/lib/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb +80 -0
  37. data/lib/vendor/thor/lib/thor/core_ext/io_binary_read.rb +12 -0
  38. data/lib/vendor/thor/lib/thor/core_ext/ordered_hash.rb +100 -0
  39. data/lib/vendor/thor/lib/thor/error.rb +28 -0
  40. data/lib/vendor/thor/lib/thor/group.rb +282 -0
  41. data/lib/vendor/thor/lib/thor/invocation.rb +172 -0
  42. data/lib/vendor/thor/lib/thor/parser.rb +4 -0
  43. data/lib/vendor/thor/lib/thor/parser/argument.rb +74 -0
  44. data/lib/vendor/thor/lib/thor/parser/arguments.rb +171 -0
  45. data/lib/vendor/thor/lib/thor/parser/option.rb +121 -0
  46. data/lib/vendor/thor/lib/thor/parser/options.rb +218 -0
  47. data/lib/vendor/thor/lib/thor/rake_compat.rb +72 -0
  48. data/lib/vendor/thor/lib/thor/runner.rb +322 -0
  49. data/lib/vendor/thor/lib/thor/shell.rb +88 -0
  50. data/lib/vendor/thor/lib/thor/shell/basic.rb +393 -0
  51. data/lib/vendor/thor/lib/thor/shell/color.rb +148 -0
  52. data/lib/vendor/thor/lib/thor/shell/html.rb +127 -0
  53. data/lib/vendor/thor/lib/thor/util.rb +270 -0
  54. data/lib/vendor/thor/lib/thor/version.rb +3 -0
  55. data/lib/vendor/thor/thor.gemspec +24 -0
  56. data/spec/engineyard/cli/api_spec.rb +50 -0
  57. data/spec/engineyard/cli_spec.rb +28 -0
  58. data/spec/engineyard/config_spec.rb +61 -0
  59. data/spec/engineyard/deploy_config_spec.rb +194 -0
  60. data/spec/engineyard/eyrc_spec.rb +76 -0
  61. data/spec/engineyard/repo_spec.rb +83 -0
  62. data/spec/engineyard_spec.rb +7 -0
  63. data/spec/ey/console_spec.rb +57 -0
  64. data/spec/ey/deploy_spec.rb +435 -0
  65. data/spec/ey/ey_spec.rb +23 -0
  66. data/spec/ey/init_spec.rb +123 -0
  67. data/spec/ey/list_environments_spec.rb +120 -0
  68. data/spec/ey/login_spec.rb +33 -0
  69. data/spec/ey/logout_spec.rb +24 -0
  70. data/spec/ey/logs_spec.rb +36 -0
  71. data/spec/ey/rebuild_spec.rb +18 -0
  72. data/spec/ey/recipes/apply_spec.rb +29 -0
  73. data/spec/ey/recipes/download_spec.rb +43 -0
  74. data/spec/ey/recipes/upload_spec.rb +99 -0
  75. data/spec/ey/rollback_spec.rb +73 -0
  76. data/spec/ey/scp_spec.rb +176 -0
  77. data/spec/ey/servers_spec.rb +209 -0
  78. data/spec/ey/ssh_spec.rb +273 -0
  79. data/spec/ey/status_spec.rb +45 -0
  80. data/spec/ey/timeout_deploy_spec.rb +18 -0
  81. data/spec/ey/web/disable_spec.rb +21 -0
  82. data/spec/ey/web/enable_spec.rb +26 -0
  83. data/spec/ey/web/restart_spec.rb +21 -0
  84. data/spec/ey/whoami_spec.rb +30 -0
  85. data/spec/spec_helper.rb +84 -0
  86. data/spec/support/bundled_ey +7 -0
  87. data/spec/support/fixture_recipes.tgz +0 -0
  88. data/spec/support/git_repos.rb +115 -0
  89. data/spec/support/helpers.rb +330 -0
  90. data/spec/support/matchers.rb +16 -0
  91. data/spec/support/ruby_ext.rb +13 -0
  92. data/spec/support/shared_behavior.rb +278 -0
  93. metadata +411 -0
@@ -0,0 +1,98 @@
1
+ require 'highline'
2
+ require 'engineyard-cloud-client'
3
+ require 'engineyard/eyrc'
4
+
5
+ module EY
6
+ class CLI
7
+ class API
8
+ USER_AGENT = "EngineYard/#{EY::VERSION}"
9
+
10
+ attr_reader :token
11
+
12
+ def initialize(endpoint, ui, token = nil)
13
+ @client = EY::CloudClient.new(endpoint: endpoint, output: ui.out, user_agent: USER_AGENT)
14
+ @ui = ui
15
+ @eyrc = EY::EYRC.load
16
+ token_from('--api-token') { token } ||
17
+ token_from('$ENGINEYARD_API_TOKEN') { ENV['ENGINEYARD_API_TOKEN'] } ||
18
+ token_from(@eyrc.path, false) { @eyrc.api_token } ||
19
+ authenticate ||
20
+ token_not_loaded
21
+ end
22
+
23
+ def respond_to?(*a)
24
+ super or @client.respond_to?(*a)
25
+ end
26
+
27
+ protected
28
+
29
+ def method_missing(meth, *args, &block)
30
+ if @client.respond_to?(meth)
31
+ with_reauthentication { @client.send(meth, *args, &block) }
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def with_reauthentication
38
+ begin
39
+ yield
40
+ rescue EY::CloudClient::InvalidCredentials
41
+ if @specified || !@ui.interactive?
42
+ # If the token is specified, we raise immediately if it is rejected.
43
+ raise EY::Error, "Authentication failed: Invalid #{@source}."
44
+ else
45
+ @ui.warn "Authentication failed: Invalid #{@source}."
46
+ authenticate
47
+ retry
48
+ end
49
+ end
50
+ end
51
+
52
+ # Get the token from the provided block, saving it if it works.
53
+ # Specified will help us know what to do if loading the token fails.
54
+ # Returns true if it gets a token.
55
+ # Returns false if there is no token.
56
+ def token_from(source, specified = true)
57
+ token = yield
58
+ if token
59
+ @client.token = token
60
+ @specified = specified
61
+ @source = "token from #{source}"
62
+ @token = token
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ # Load the token from EY Cloud if interactive and
70
+ # token wasn't explicitly specified previously.
71
+ def authenticate
72
+ if @specified
73
+ return false
74
+ end
75
+
76
+ @source = "credentials"
77
+ @specified = false
78
+
79
+ @ui.info "We need to fetch your API token; please log in."
80
+ begin
81
+ email = @ui.ask("Email: ")
82
+ passwd = @ui.ask("Password: ", true)
83
+ @token = @client.authenticate!(email, passwd)
84
+ @eyrc.api_token = @token
85
+ true
86
+ rescue EY::CloudClient::InvalidCredentials
87
+ @ui.warn "Authentication failed. Please try again."
88
+ retry
89
+ end
90
+ end
91
+
92
+ # Occurs when all avenues for getting the token are exhausted.
93
+ def token_not_loaded
94
+ raise EY::Error, "Sorry, we couldn't get your API token."
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,129 @@
1
+ require 'tempfile'
2
+
3
+ module EY
4
+ class CLI
5
+ class Recipes < EY::Thor
6
+ desc "apply [--environment ENVIRONMENT]",
7
+ "Run chef recipes uploaded by '#{banner_base} recipes upload' on the specified environment."
8
+ long_desc <<-DESC
9
+ This is similar to '#{banner_base} rebuild' except Engine Yard's main
10
+ configuration step is skipped.
11
+
12
+ The cookbook uploaded by the '#{banner_base} recipes upload' command will be run when
13
+ you run '#{banner_base} recipes apply'.
14
+ DESC
15
+
16
+ method_option :environment, type: :string, aliases: %w(-e),
17
+ required: true, default: '',
18
+ desc: "Environment in which to apply recipes"
19
+ method_option :account, type: :string, aliases: %w(-c),
20
+ required: true, default: '',
21
+ desc: "Name of the account in which the environment can be found"
22
+ def apply
23
+ environment = fetch_environment(options[:environment], options[:account])
24
+ apply_recipes(environment)
25
+ end
26
+
27
+ desc "upload [--environment ENVIRONMENT]",
28
+ "Upload custom chef recipes to specified environment so they can be applied."
29
+ long_desc <<-DESC
30
+ Make an archive of the "cookbooks/" subdirectory in your current working
31
+ directory and upload it to Engine Yard Cloud's recipe storage.
32
+
33
+ Alternatively, specify a .tgz of a cookbooks/ directory yourself as follows:
34
+
35
+ $ #{banner_base} recipes upload -f path/to/recipes.tgz
36
+
37
+ The uploaded cookbooks will be run when executing '#{banner_base} recipes apply'
38
+ and also automatically each time you update/rebuild your instances.
39
+ DESC
40
+
41
+ method_option :environment, type: :string, aliases: %w(-e),
42
+ required: true, default: '',
43
+ desc: "Environment that will receive the recipes"
44
+ method_option :account, type: :string, aliases: %w(-c),
45
+ required: true, default: '',
46
+ desc: "Name of the account in which the environment can be found"
47
+ method_option :apply, type: :boolean,
48
+ desc: "Apply the recipes immediately after they are uploaded"
49
+ method_option :file, type: :string, aliases: %w(-f),
50
+ required: true, default: '',
51
+ desc: "Specify a gzipped tar file (.tgz) for upload instead of cookbooks/ directory"
52
+ def upload
53
+ environment = fetch_environment(options[:environment], options[:account])
54
+ upload_recipes(environment, options[:file])
55
+ if options[:apply]
56
+ apply_recipes(environment)
57
+ end
58
+ end
59
+
60
+ no_tasks do
61
+ def apply_recipes(environment)
62
+ environment.run_custom_recipes
63
+ ui.info "Uploaded recipes started for #{environment.name}"
64
+ end
65
+
66
+ def upload_recipes(environment, filename)
67
+ if filename && filename != ''
68
+ environment.upload_recipes_at_path(filename)
69
+ ui.info "Recipes file #{filename} uploaded successfully for #{environment.name}"
70
+ else
71
+ path = cookbooks_dir_archive_path
72
+ environment.upload_recipes_at_path(path)
73
+ ui.info "Recipes in cookbooks/ uploaded successfully for #{environment.name}"
74
+ end
75
+ end
76
+
77
+ def cookbooks_dir_archive_path
78
+ unless FileTest.exist?("cookbooks")
79
+ raise EY::Error, "Could not find chef recipes. Please run from the root of your recipes repo."
80
+ end
81
+
82
+ recipes_file = Tempfile.new("recipes")
83
+
84
+ cmd = "tar czf '#{recipes_file.path}' cookbooks/"
85
+ if FileTest.exist?("data_bags")
86
+ cmd = cmd + " data_bags/"
87
+ end
88
+
89
+ unless system(cmd)
90
+ raise EY::Error, "Could not archive recipes.\nCommand `#{cmd}` exited with an error."
91
+ end
92
+ recipes_file.path
93
+ end
94
+ end
95
+
96
+ desc "download [--environment ENVIRONMENT]",
97
+ "Download a copy of the custom chef recipes from this environment into the current directory."
98
+ long_desc <<-DESC
99
+ The recipes will be unpacked into a directory called "cookbooks" in the
100
+ current directory. This is the opposite of 'recipes upload'.
101
+
102
+ If the cookbooks directory already exists, an error will be raised.
103
+ DESC
104
+ method_option :environment, type: :string, aliases: %w(-e),
105
+ required: true, default: '',
106
+ desc: "Environment for which to download the recipes"
107
+ method_option :account, type: :string, aliases: %w(-c),
108
+ required: true, default: '',
109
+ desc: "Name of the account in which the environment can be found"
110
+ def download
111
+ if File.exist?('cookbooks')
112
+ raise EY::Error, "Cannot download recipes, cookbooks directory already exists."
113
+ end
114
+
115
+ environment = fetch_environment(options[:environment], options[:account])
116
+
117
+ recipes = environment.download_recipes
118
+ cmd = "tar xzf '#{recipes.path}' cookbooks"
119
+
120
+ if system(cmd)
121
+ ui.info "Recipes downloaded successfully for #{environment.name}"
122
+ else
123
+ raise EY::Error, "Could not unarchive recipes.\nCommand `#{cmd}` exited with an error."
124
+ end
125
+ end
126
+
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,275 @@
1
+ require 'highline'
2
+
3
+ module EY
4
+ class CLI
5
+ class UI < Thor::Base.shell
6
+
7
+ class Tee
8
+ def initialize(*ios)
9
+ @ios = ios
10
+ end
11
+
12
+ def <<(str)
13
+ @ios.each { |io| io << str }
14
+ self
15
+ end
16
+ end
17
+
18
+ class Prompter
19
+ def self.add_answer(arg)
20
+ @answers ||= []
21
+ @answers << arg
22
+ end
23
+
24
+ def self.questions
25
+ @questions
26
+ end
27
+
28
+ def self.enable_mock!
29
+ @questions = []
30
+ @answers = []
31
+ @mock = true
32
+ end
33
+
34
+ def self.highline
35
+ @highline ||= HighLine.new($stdin)
36
+ end
37
+
38
+ def self.interactive?
39
+ @mock || ($stdout && $stdout.tty?)
40
+ end
41
+
42
+ def self.ask(question, password = false, default = nil)
43
+ if @mock
44
+ @questions ||= []
45
+ @questions << question
46
+ answer = @answers.shift
47
+ (answer == '' && default) ? default : answer
48
+ else
49
+ timeout_if_not_interactive do
50
+ highline.ask(question) do |q|
51
+ q.echo = "*" if password
52
+ q.default = default if default
53
+ end.to_s
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.agree(question, default)
59
+ if @mock
60
+ @questions ||= []
61
+ @questions << question
62
+ answer = @answers.shift
63
+ answer == '' ? default : %w[y yes].include?(answer)
64
+ else
65
+ timeout_if_not_interactive do
66
+ answer = highline.agree(question) {|q| q.default = default ? 'Y/n' : 'N/y' }
67
+ case answer
68
+ when 'Y/n' then true
69
+ when 'N/y' then false
70
+ else answer
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.timeout_if_not_interactive(&block)
77
+ if interactive?
78
+ block.call
79
+ else
80
+ Timeout.timeout(2, &block)
81
+ end
82
+ end
83
+ end
84
+
85
+ def error(name, message = nil)
86
+ $stdout = $stderr
87
+ say_with_status(name, message, :red)
88
+ ensure
89
+ $stdout = STDOUT
90
+ end
91
+
92
+ def warn(name, message = nil)
93
+ say_with_status(name, message, :yellow)
94
+ end
95
+
96
+ def info(message, color = nil)
97
+ return if quiet?
98
+ say_with_status(message, nil, color)
99
+ end
100
+
101
+ def debug(name, message = nil)
102
+ if ENV["DEBUG"]
103
+ name = name.inspect unless name.nil? or name.is_a?(String)
104
+ message = message.inspect unless message.nil? or message.is_a?(String)
105
+ say_with_status(name, message, :blue)
106
+ end
107
+ end
108
+
109
+ def say_with_status(name, message=nil, color=nil)
110
+ if message
111
+ say_status name, message, color
112
+ elsif name
113
+ say name, color
114
+ end
115
+ end
116
+
117
+ def interactive?
118
+ Prompter.interactive?
119
+ end
120
+
121
+ def agree(message, default)
122
+ Prompter.agree(message, default)
123
+ end
124
+
125
+ def ask(message, password = false, default = nil)
126
+ Prompter.ask(message, password, default)
127
+ rescue EOFError
128
+ return ''
129
+ end
130
+
131
+ def mute_if(bool, &block)
132
+ bool ? mute(&block) : yield
133
+ end
134
+
135
+ def server_tuples(servers, username=nil)
136
+ user = username && "#{username}@"
137
+
138
+ servers.map do |server|
139
+ host = "#{user}#{server.hostname}"
140
+ [host, server.amazon_id, server.role, server.name]
141
+ end
142
+ end
143
+ private :server_tuples
144
+
145
+ def print_hostnames(servers, username=nil)
146
+ server_tuples(servers, username).each do |server_tuple|
147
+ puts server_tuple.first
148
+ end
149
+ end
150
+
151
+ def print_simple_servers(servers, username=nil)
152
+ server_tuples(servers, username).each do |server_tuple|
153
+ puts server_tuple.join("\t")
154
+ end
155
+ end
156
+
157
+ def print_servers(servers, name, username=nil)
158
+ tuples = server_tuples(servers, username)
159
+ count = tuples.size
160
+ puts "# #{count} server#{count == 1 ? '' : 's'} on #{name}"
161
+
162
+ host_width = tuples.map {|s| s[0].length }.max
163
+ host_format = "%-#{host_width}s" # "%-10s" left align
164
+
165
+ role_width = tuples.map {|s| s[2].length }.max
166
+ role_format = "%-#{role_width}s" # "%-10s" left align
167
+
168
+ tuples.each do |server_tuple|
169
+ puts "#{host_format}\t%s\t#{role_format}\t%s" % server_tuple
170
+ end
171
+ end
172
+
173
+ def print_simple_envs(envs)
174
+ puts envs.map{|env| env.name }.uniq.sort
175
+ end
176
+
177
+ def print_envs(apps, default_env_name = nil)
178
+ apps.sort_by {|app| "#{app.account.name}/#{app.name}" }.each do |app|
179
+ puts "#{app.account.name}/#{app.name}"
180
+ if app.environments.any?
181
+ app.environments.sort_by {|env| env.name }.each do |env|
182
+ icount = env.instances_count
183
+ iname = case icount
184
+ when 0 then "(stopped)"
185
+ when 1 then "1 instance"
186
+ else "#{icount} instances"
187
+ end
188
+
189
+ name = env.name == default_env_name ? "#{env.name} (default)" : env.name
190
+ framework_env = env.framework_env && "[#{env.framework_env.center(12)}]"
191
+
192
+ puts " #{name.ljust(30)} #{framework_env} #{iname}"
193
+ end
194
+ else
195
+ puts " (No environments)"
196
+ end
197
+
198
+ puts ""
199
+ end
200
+ end
201
+
202
+ def deployment_status(deployment)
203
+ unless quiet?
204
+ say "# Status of last deployment of #{deployment.app_environment.hierarchy_name}:"
205
+ say "#"
206
+ show_deployment(deployment)
207
+ say "#"
208
+ end
209
+ deployment_result(deployment)
210
+ end
211
+
212
+ def show_deployment(dep)
213
+ return if quiet?
214
+ output = []
215
+ output << ["Account", dep.app.account.name]
216
+ output << ["Application", dep.app.name]
217
+ output << ["Environment", dep.environment.name]
218
+ output << ["Input Ref", dep.ref]
219
+ output << ["Resolved Ref", dep.resolved_ref]
220
+ output << ["Commit", dep.commit || '(not resolved)']
221
+ output << ["Migrate", dep.migrate]
222
+ output << ["Migrate command", dep.migrate_command] if dep.migrate
223
+ output << ["Deployed by", dep.deployed_by]
224
+ output << ["Started at", dep.created_at] if dep.created_at
225
+ output << ["Finished at", dep.finished_at] if dep.finished_at
226
+
227
+ output.each do |att, val|
228
+ puts "#\t%-16s %s" % ["#{att}:", val.to_s]
229
+ end
230
+ end
231
+
232
+ def deployment_result(dep)
233
+ if dep.successful?
234
+ say 'Deployment was successful.', :green
235
+ elsif dep.finished_at.nil?
236
+ say 'Deployment is not finished.', :yellow
237
+ else
238
+ say 'Deployment failed.', :red
239
+ end
240
+ end
241
+
242
+ def print_exception(e)
243
+ if e.message.empty? || (e.message == e.class.to_s)
244
+ message = nil
245
+ else
246
+ message = e.message
247
+ end
248
+
249
+ if ENV["DEBUG"]
250
+ error(e.class, message)
251
+ e.backtrace.each{|l| say(" "*3 + l) }
252
+ else
253
+ error(message || e.class.to_s)
254
+ end
255
+ end
256
+
257
+ def print_help(table)
258
+ print_table(table, ident: 2, truncate: true, colwidth: 20)
259
+ end
260
+
261
+ def set_color(string, color, bold=false)
262
+ ($stdout.tty? || ENV['THOR_SHELL']) ? super : string
263
+ end
264
+
265
+ def err
266
+ $stderr
267
+ end
268
+
269
+ def out
270
+ $stdout
271
+ end
272
+
273
+ end
274
+ end
275
+ end