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,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__)
7
+ load File.expand_path('../../../bin/ey', __FILE__)
@@ -0,0 +1,115 @@
1
+ module EY
2
+ class << self
3
+ def define_git_repo(name, &setup)
4
+ git_repo_setup[name] ||= setup
5
+ end
6
+
7
+ def refresh_git_repo(name)
8
+ git_repo_dir_cache.delete name
9
+ end
10
+
11
+ def git_repo_dir(name)
12
+ return git_repo_dir_cache[name] if git_repo_dir_cache.has_key?(name)
13
+ raise ArgumentError, "No definition for git repo #{name}" unless git_repo_setup[name]
14
+
15
+ git_dir = TMPDIR.join("engineyard_test_repo_#{Time.now.tv_sec}_#{Time.now.tv_usec}_#{$$}")
16
+ git_dir.mkpath
17
+ Dir.chdir(git_dir) do
18
+ system("git init -q")
19
+ system('git config user.email ey@spec.test')
20
+ system('git config user.name "EY Specs"')
21
+ system("git remote add testremote user@git.host:path/to/repo.git")
22
+ git_repo_setup[name].call(git_dir)
23
+ end
24
+ git_repo_dir_cache[name] = git_dir
25
+ end
26
+
27
+ def chdir_to_repo(repo_name)
28
+ @_original_wd ||= []
29
+ @_original_wd << Dir.getwd
30
+ Dir.chdir(git_repo_dir(repo_name))
31
+ end
32
+
33
+ def chdir_return
34
+ Dir.chdir(@_original_wd.pop) if @_original_wd && @_original_wd.any?
35
+ end
36
+
37
+ def fixture_recipes_tgz
38
+ File.expand_path('../fixture_recipes.tgz', __FILE__)
39
+ end
40
+
41
+ def link_recipes_tgz(git_dir)
42
+ system("ln -s #{fixture_recipes_tgz} #{git_dir.join('recipes.tgz')}")
43
+ end
44
+
45
+ protected
46
+
47
+ def git_repo_setup
48
+ @git_repo_setup ||= {}
49
+ end
50
+
51
+ def git_repo_dir_cache
52
+ @git_repo_dir_cache ||= {}
53
+ end
54
+ end
55
+
56
+ define_git_repo("default") do |git_dir|
57
+ system("echo 'source :gemcutter' > Gemfile")
58
+ system("git add Gemfile")
59
+ system("git commit -m 'initial commit' >/dev/null 2>&1")
60
+ end
61
+
62
+ define_git_repo('deploy test') do
63
+ # we'll have one commit on master
64
+ system("echo 'source :gemcutter' > Gemfile")
65
+ system("git add Gemfile")
66
+ system("git commit -m 'initial commit' >/dev/null 2>&1")
67
+
68
+ # and a tag
69
+ system("git tag -a -m 'version one' v1")
70
+
71
+ # and we need a non-master branch
72
+ system("git checkout -b current-branch >/dev/null 2>&1")
73
+ end
74
+
75
+ define_git_repo('+cookbooks') do |git_dir|
76
+ git_dir.join("cookbooks").mkdir
77
+ git_dir.join("cookbooks/file").open("w") {|f| f << "boo" }
78
+ end
79
+
80
+ define_git_repo('+recipes') do |git_dir|
81
+ link_recipes_tgz(git_dir)
82
+ end
83
+
84
+ define_git_repo "only cookbooks, no remotes" do |git_dir|
85
+ `git --git-dir "#{git_dir}/.git" remote`.split("\n").each do |remote|
86
+ `git --git-dir "#{git_dir}/.git" remote rm #{remote}`
87
+ end
88
+
89
+ git_dir.join("cookbooks").mkdir
90
+ File.open(git_dir.join("cookbooks/file"), "w"){|f| f << "stuff" }
91
+ end
92
+
93
+ define_git_repo "only cookbooks, unrelated remotes" do |git_dir|
94
+ `git --git-dir "#{git_dir}/.git" remote`.split("\n").each do |remote|
95
+ `git --git-dir "#{git_dir}/.git" remote rm #{remote}`
96
+ end
97
+
98
+ `git remote add origin polly@pirate.example.com:wanna/cracker.git`
99
+
100
+ git_dir.join("cookbooks").mkdir
101
+ File.open(git_dir.join("cookbooks/file"), "w"){|f| f << "rawk" }
102
+ end
103
+
104
+ define_git_repo('dup test') do
105
+ system("git remote add dup git://github.com/engineyard/dup.git")
106
+ end
107
+
108
+ define_git_repo("not actually a git repo") do |git_dir|
109
+ # in case we screw up and are not in a freshly-generated test
110
+ # git repository, don't blow away the thing we're developing
111
+ system("rm -rf .git") if `git remote -v`.include?("path/to/repo.git")
112
+ git_dir.join("cookbooks").mkdir
113
+ link_recipes_tgz(git_dir)
114
+ end
115
+ end
@@ -0,0 +1,330 @@
1
+ require 'engineyard/cli'
2
+
3
+ require 'realweb'
4
+ require 'open4'
5
+ require 'stringio'
6
+
7
+ module SpecHelpers
8
+ module Given
9
+ def given(name)
10
+ include_examples name
11
+ end
12
+ end
13
+
14
+ module IntegrationHelpers
15
+ def run_ey(command_options, ey_options={})
16
+
17
+ if respond_to?(:extra_ey_options) # needed for ssh tests
18
+ ey_options.merge!(extra_ey_options)
19
+ return ey(command_to_run(command_options), ey_options)
20
+ end
21
+
22
+ if ey_options[:expect_failure]
23
+ fast_failing_ey(command_to_run(command_options))
24
+ else
25
+ fast_ey(command_to_run(command_options))
26
+ end
27
+ end
28
+
29
+ def make_scenario(opts)
30
+ # since nil will silently turn to empty string when interpolated,
31
+ # and there's a lot of string matching involved in integration
32
+ # testing, it would be nice to have early notification of typos.
33
+ scenario = Hash.new { |h,k| raise "Tried to get key #{k.inspect}, but it's missing!" }
34
+ scenario.merge!(opts)
35
+ end
36
+ end
37
+
38
+ module GitRepoHelpers
39
+ def define_git_repo(name, &setup)
40
+ # EY's ivars don't get cleared between examples, so we can keep
41
+ # a git repo around longer (and thus make our tests faster)
42
+ EY.define_git_repo(name, &setup)
43
+ end
44
+
45
+ def use_git_repo(repo_name)
46
+ before(:all) do
47
+ EY.chdir_to_repo(repo_name)
48
+ end
49
+
50
+ after(:all) do
51
+ EY.chdir_return
52
+ end
53
+ end
54
+ end
55
+
56
+ class UnexpectedExit < StandardError
57
+ def initialize(stdout, stderr)
58
+ super "Exited with an unexpected exit code\n---STDOUT---\n#{stdout}\n---STDERR---\n#{stderr}\n"
59
+ end
60
+ end
61
+ NonzeroExitStatus = Class.new(UnexpectedExit)
62
+ ZeroExitStatus = Class.new(UnexpectedExit)
63
+
64
+ def ey_api
65
+ @api ||= EY::CloudClient.new(:token => 'asdf')
66
+ end
67
+
68
+ def ensure_eyrc
69
+ begin
70
+ unless (data = read_eyrc) and data['api_token']
71
+ raise ".eyrc has no token, specs will stall waiting for stdin authentication input"
72
+ end
73
+ rescue Errno::ENOENT => e
74
+ raise ".eyrc must be written before calling run_ey or specs will stall waiting for stdin authentication input"
75
+ end
76
+ end
77
+
78
+ def fast_ey(args, options = {})
79
+
80
+ ensure_eyrc
81
+
82
+ begin
83
+ debug = options[:debug] ? 'true' : nil
84
+ err, out = StringIO.new, StringIO.new
85
+ capture_stderr_into(err) do
86
+ capture_stdout_into(out) do
87
+ with_env('DEBUG' => debug, 'PRINT_CMD' => 'true') do
88
+ EY::CLI.start(args)
89
+ end
90
+ end
91
+ end
92
+ ensure
93
+ @err, @out = err.string, out.string
94
+ @raw_ssh_commands, @ssh_commands = extract_ssh_commands(@out)
95
+ end
96
+ end
97
+
98
+ def fast_failing_ey(*args)
99
+ begin
100
+ fast_ey(*args)
101
+ raise ZeroExitStatus.new(@out, @err)
102
+ rescue SystemExit => exit_status
103
+ # SystemExit typically indicates a bogus command, which we
104
+ # here in expected-to-fail land are entirely happy with.
105
+ nil
106
+ rescue Thor::Error, EY::Error, EY::CloudClient::Error => e
107
+ nil
108
+ end
109
+ end
110
+
111
+ def capture_stderr_into(stream)
112
+ $stderr = stream
113
+ yield
114
+ ensure
115
+ $stderr = STDERR
116
+ end
117
+
118
+ def capture_stdout_into(stream)
119
+ $stdout = stream
120
+ yield
121
+ ensure
122
+ $stdout = STDOUT
123
+ end
124
+
125
+ def ey(args = [], options = {}, &block)
126
+ if respond_to?(:extra_ey_options) # needed for ssh tests
127
+ options.merge!(extra_ey_options)
128
+ end
129
+
130
+ hide_err = options.has_key?(:hide_err) ? options[:hide_err] : options[:expect_failure]
131
+
132
+ path_prepends = options[:prepend_to_path]
133
+
134
+ ey_env = {
135
+ 'DEBUG' => ENV['DEBUG'],
136
+ 'PRINT_CMD' => 'true',
137
+ 'EYRC' => ENV['EYRC'],
138
+ 'CLOUD_URL' => ENV['CLOUD_URL'],
139
+ }
140
+
141
+ if options.has_key?(:debug)
142
+ ey_env['DEBUG'] = options[:debug] ? "true" : nil
143
+ end
144
+
145
+ if path_prepends
146
+ tempdir = TMPDIR.join("ey_test_cmds_#{Time.now.tv_sec}#{Time.now.tv_usec}_#{$$}")
147
+ tempdir.mkpath
148
+ path_prepends.each do |name, contents|
149
+ tempdir.join(name).open('w') do |f|
150
+ f.write(contents)
151
+ f.chmod(0755)
152
+ end
153
+ end
154
+
155
+ ey_env['PATH'] = "#{tempdir}:#{ENV['PATH']}"
156
+ end
157
+
158
+ eybin = File.expand_path('../bundled_ey', __FILE__)
159
+
160
+ with_env(ey_env) do
161
+ exit_status = Open4::open4("#{eybin} #{Escape.shell_command(args)}") do |pid, stdin, stdout, stderr|
162
+ block.call(stdin) if block
163
+ stdin.close
164
+ @out = stdout.read
165
+ @err = stderr.read
166
+ end
167
+
168
+ if !exit_status.success? && !options[:expect_failure]
169
+ raise NonzeroExitStatus.new(@out, @err)
170
+ elsif exit_status.success? && options[:expect_failure]
171
+ raise ZeroExitStatus.new(@out, @err)
172
+ end
173
+ end
174
+
175
+ @raw_ssh_commands, @ssh_commands = extract_ssh_commands(@out)
176
+
177
+ puts @err unless @err.empty? || hide_err
178
+ @out
179
+ end
180
+
181
+ def extract_ssh_commands(output)
182
+ raw_ssh_commands = [@out,@err].join("\n").split(/\n/).find_all do |line|
183
+ line =~ /^bash -lc/ || line =~ /^ssh/ || line =~ /^scp/
184
+ end
185
+
186
+ ssh_commands = raw_ssh_commands.map do |cmd|
187
+ # Strip off everything up to and including user@host, leaving
188
+ # just the command that the remote system would run
189
+ #
190
+ # XXX: this is a really icky icky.
191
+ # engineyard gem was written as if shelling out to run serverside
192
+ # and running an ssh command will always be the same. This is a nasty
193
+ # hack to get it working with Net::SSH for now so we can repair 1.9.2.
194
+ ssh_prefix_removed = cmd.gsub(/^bash -lc /, '').gsub(/^ssh .*?\w+@\S*\s*/, '')
195
+
196
+ # Its arguments have been double-escaped: one layer is to get
197
+ # them through our local shell and into ssh, and the other
198
+ # layer is to get them through the remote system's shell.
199
+ #
200
+ # Strip off one layer by running it through the shell.
201
+ just_the_remote_command = ssh_prefix_removed.gsub(/>\s*\/dev\/null.*$/, '')
202
+ `echo #{just_the_remote_command}`.strip
203
+ end
204
+
205
+ [raw_ssh_commands, ssh_commands]
206
+ end
207
+
208
+ DEPRECATED_SCENARIOS = {
209
+ "empty" => "User Name",
210
+ "one app without environment" => "App Without Env",
211
+ "one app, one environment, not linked" => "Unlinked App",
212
+ "two apps" => "Two Apps",
213
+ "one app, one environment" => "Linked App",
214
+ "Stuck Deployment" => "Stuck Deployment",
215
+ "two accounts, two apps, two environments, ambiguous" => "Multiple Ambiguous Accounts",
216
+ "one app, one environment, no instances" => "Linked App Not Running",
217
+ "one app, one environment, app master red" => "Linked App Red Master",
218
+ "one app, many environments" => "One App Many Envs",
219
+ "one app, many similarly-named environments" => "One App Similarly Named Envs",
220
+ "two apps, same git uri" => "Two Apps Same Git URI",
221
+ }
222
+
223
+ def api_scenario(old_name)
224
+ clean_eyrc # switching scenarios, always clean up
225
+ name = DEPRECATED_SCENARIOS[old_name]
226
+ @scenario = EY::CloudClient::Test::Scenario[name]
227
+ @scenario_email = @scenario.email
228
+ @scenario_password = @scenario.password
229
+ @scenario_api_token = @scenario.api_token
230
+ @scenario
231
+ end
232
+
233
+ def login_scenario(scenario_name)
234
+ scen = api_scenario(scenario_name)
235
+ write_eyrc('api_token' => scenario_api_token)
236
+ scen
237
+ end
238
+
239
+ def scenario_email
240
+ @scenario_email
241
+ end
242
+
243
+ def scenario_password
244
+ @scenario_password
245
+ end
246
+
247
+ def scenario_api_token
248
+ @scenario_api_token
249
+ end
250
+
251
+ def clean_tmpdir
252
+ TMPDIR.rmtree if TMPDIR.exist?
253
+ end
254
+
255
+ def read_yaml(file)
256
+ contents = File.read(File.expand_path(file))
257
+ YAML.load(contents)
258
+ rescue Exception => e
259
+ raise "#{e}\n#{contents}"
260
+ end
261
+
262
+ def write_yaml(data, file)
263
+ File.open(file, "w"){|f| YAML.dump(data, f) }
264
+ end
265
+
266
+ def clean_eyrc
267
+ ENV['EYRC'] = File.join('/tmp','eyrc')
268
+ if ENV['EYRC'] && File.exist?(ENV['EYRC'])
269
+ File.unlink(ENV['EYRC'])
270
+ end
271
+ end
272
+
273
+ def read_eyrc
274
+ read_yaml(ENV['EYRC'])
275
+ end
276
+
277
+ def write_eyrc(data)
278
+ write_yaml(data, ENV['EYRC'])
279
+ end
280
+
281
+ def ey_yml
282
+ EY::Config.pathname
283
+ end
284
+
285
+ def clean_ey_yml
286
+ ey_yml.unlink if ey_yml && ey_yml.exist?
287
+ FileUtils.rm_r 'config' if FileTest.exist?('config')
288
+ end
289
+
290
+ def read_ey_yml
291
+ read_yaml(EY::Config.pathname)
292
+ end
293
+
294
+ def write_ey_yml(data)
295
+ write_yaml(data, EY::Config.pathname_for_write)
296
+ end
297
+
298
+ def expect_config(*keys)
299
+ root = keys.unshift('defaults') unless %w[defaults environments].include?(keys.first)
300
+ config = read_ey_yml
301
+ value = keys.inject(config) { |conf, key| conf[key] }
302
+ expect(value)
303
+ end
304
+
305
+ def exist
306
+ be_exist
307
+ end
308
+
309
+ def with_env(new_env_vars)
310
+ raise ArgumentError, "with_env takes a block" unless block_given?
311
+ old_env_vars = {}
312
+ new_env_vars.each do |k, v|
313
+ if ENV.has_key?(k)
314
+ old_env_vars[k] = ENV[k]
315
+ end
316
+ ENV[k] = v if v
317
+ end
318
+
319
+ retval = yield
320
+
321
+ new_env_vars.keys.each do |k|
322
+ if old_env_vars.has_key?(k)
323
+ ENV[k] = old_env_vars[k]
324
+ else
325
+ ENV.delete(k)
326
+ end
327
+ end
328
+ retval
329
+ end
330
+ end