crazy-yard 3.2.2

Sign up to get free protection for your applications and to get access to all the features.
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