herokugarden 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,101 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run all specs"
5
+ Spec::Rake::SpecTask.new('spec') do |t|
6
+ t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
7
+ t.spec_files = FileList['spec/*_spec.rb']
8
+ end
9
+
10
+ desc "Print specdocs"
11
+ Spec::Rake::SpecTask.new(:doc) do |t|
12
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
13
+ t.spec_files = FileList['spec/*_spec.rb']
14
+ end
15
+
16
+ desc "Generate RCov code coverage report"
17
+ Spec::Rake::SpecTask.new('rcov') do |t|
18
+ t.spec_files = FileList['spec/*_spec.rb']
19
+ t.rcov = true
20
+ t.rcov_opts = ['--exclude', 'examples']
21
+ end
22
+
23
+ task :default => :spec
24
+
25
+ ######################################################
26
+
27
+ require 'rake'
28
+ require 'rake/testtask'
29
+ require 'rake/clean'
30
+ require 'rake/gempackagetask'
31
+ require 'rake/rdoctask'
32
+ require 'fileutils'
33
+ include FileUtils
34
+ require 'lib/herokugarden'
35
+
36
+ version = HerokuGarden::Client.version
37
+ name = "herokugarden"
38
+
39
+ spec = Gem::Specification.new do |s|
40
+ s.name = name
41
+ s.version = version
42
+ s.summary = "Client library and CLI to deploy Rails apps on Heroku Garden."
43
+ s.description = "Client library and command-line tool to manage and deploy Rails apps on Heroku Garden."
44
+ s.author = "Adam Wiggins"
45
+ s.email = "feedback@heroku.com"
46
+ s.homepage = "http://herokugarden.com/"
47
+ s.executables = [ "herokugarden" ]
48
+ s.default_executable = "herokugarden"
49
+ s.rubyforge_project = "herokugarden"
50
+ s.post_install_message = <<EOF
51
+
52
+ ---> Installing herokugarden v#{version}
53
+ ---> Migrate local checkouts using the git:transition command:
54
+
55
+ cd myapp/
56
+ herokugarden git:transition
57
+
58
+ EOF
59
+
60
+ s.platform = Gem::Platform::RUBY
61
+ s.has_rdoc = true
62
+
63
+ s.files = %w(Rakefile) +
64
+ Dir.glob("{bin,lib,spec}/**/*")
65
+
66
+ s.require_path = "lib"
67
+ s.bindir = "bin"
68
+
69
+ s.add_dependency('rest-client', '>=0.5')
70
+ end
71
+
72
+ Rake::GemPackageTask.new(spec) do |p|
73
+ p.need_tar = true if RUBY_PLATFORM !~ /mswin/
74
+ end
75
+
76
+ task :install => [ :test, :package ] do
77
+ sh %{sudo gem install pkg/#{name}-#{version}.gem}
78
+ end
79
+
80
+ task :uninstall => [ :clean ] do
81
+ sh %{sudo gem uninstall #{name}}
82
+ end
83
+
84
+ Rake::TestTask.new do |t|
85
+ t.libs << "spec"
86
+ t.test_files = FileList['spec/*_spec.rb']
87
+ t.verbose = true
88
+ end
89
+
90
+ Rake::RDocTask.new do |t|
91
+ t.rdoc_dir = 'rdoc'
92
+ t.title = "Heroku API"
93
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
94
+ t.options << '--charset' << 'utf-8'
95
+ t.rdoc_files.include('README')
96
+ t.rdoc_files.include('REST')
97
+ t.rdoc_files.include('lib/heroku/*.rb')
98
+ end
99
+
100
+ CLEAN.include [ 'build/*', '**/*.o', '**/*.so', '**/*.a', 'lib/*-*', '**/*.log', 'pkg', 'lib/*.bundle', '*.gem', '.config' ]
101
+
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'herokugarden'
6
+ require 'herokugarden/command_line'
7
+
8
+ usage = <<EOTXT
9
+ === Heroku Garden Commands
10
+ list - list your apps
11
+ create [<name>] - create a new app
12
+ info <app> - show app info, like web url and git repo
13
+ update <app> - update the app
14
+ --name <newname>
15
+ --public (true|false)
16
+ --mode (production|development)
17
+ sharing <app> - manage collaborators
18
+ --add <email>
19
+ --remove <email>
20
+ --access (edit|view)
21
+ rake <app> <command> - remotely execute a rake command
22
+ destroy <app> - destroy the app permanently
23
+ keys - manage your user's ssh public keys for git access
24
+ --add [<path to keyfile>]
25
+ --remove <keyname or all>
26
+
27
+ Example story:
28
+ herokugarden create myapp
29
+ git clone git@herokugarden.com:myapp.git
30
+ cd myapp
31
+ (...make edits...)
32
+ git add .
33
+ git commit -m "changes"
34
+ git push
35
+ herokugarden update myapp --public true --mode production
36
+ EOTXT
37
+
38
+ command = ARGV.shift.strip.gsub(/:/, '_') rescue ""
39
+ if command.length == 0
40
+ puts usage
41
+ exit 1
42
+ end
43
+
44
+ cli = HerokuGarden::CommandLine.new
45
+ unless cli.methods.include? command
46
+ puts "no such method as #{command}, run without arguments for usage"
47
+ exit 2
48
+ end
49
+
50
+ cli.execute(command, ARGV)
51
+
@@ -0,0 +1,5 @@
1
+ module HerokuGarden; end
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/herokugarden')
4
+
5
+ require 'client'
@@ -0,0 +1,153 @@
1
+ require 'rubygems'
2
+ require 'rexml/document'
3
+ require 'rest_client'
4
+ require 'uri'
5
+
6
+ # A Ruby class to call the Heroku REST API. You might use this if you want to
7
+ # manage your Heroku apps from within a Ruby program, such as Capistrano.
8
+ #
9
+ # Example:
10
+ #
11
+ # require 'heroku'
12
+ # heroku = HerokuGarden::Client.new('me@example.com', 'mypass')
13
+ # heroku.create('myapp')
14
+ #
15
+ class HerokuGarden::Client
16
+ def self.version
17
+ '0.4.2'
18
+ end
19
+
20
+ attr_accessor :host, :user, :password
21
+
22
+ def initialize(user, password, host='herokugarden.com')
23
+ @user = user
24
+ @password = password
25
+ @host = host
26
+ end
27
+
28
+ # Show a list of apps which you are a collaborator on.
29
+ def list
30
+ doc = xml(get('/apps'))
31
+ doc.elements.to_a("//apps/app/name").map { |a| a.text }
32
+ end
33
+
34
+ # Show info such as mode, custom domain, and collaborators on an app.
35
+ def info(name, saferoute=false)
36
+ doc = xml(get("/#{'safe' if saferoute}apps/#{name}"))
37
+ attrs = { :collaborators => list_collaborators(name) }
38
+ doc.elements.to_a('//app/*').inject(attrs) do |hash, element|
39
+ hash[element.name.to_sym] = element.text; hash
40
+ end
41
+ end
42
+
43
+ # Create a new app, with an optional name.
44
+ def create(name=nil, options={})
45
+ params = {}
46
+ params['app[name]'] = name if name
47
+ xml(post('/apps', params)).elements["//app/name"].text
48
+ end
49
+
50
+ # Update an app. Available attributes:
51
+ # :name => rename the app (changes http and git urls)
52
+ # :public => true | false
53
+ # :mode => production | development
54
+ def update(name, attributes)
55
+ put("/apps/#{name}", :app => attributes)
56
+ end
57
+
58
+ # Destroy the app permanently.
59
+ def destroy(name)
60
+ delete("/apps/#{name}")
61
+ end
62
+
63
+ # Get a list of collaborators on the app, returns an array of hashes each of
64
+ # which contain :email and :access (=> edit | view) elements.
65
+ def list_collaborators(app_name)
66
+ doc = xml(get("/apps/#{app_name}/collaborators"))
67
+ doc.elements.to_a("//collaborators/collaborator").map do |a|
68
+ { :email => a.elements['email'].text, :access => a.elements['access'].text }
69
+ end
70
+ end
71
+
72
+ # Invite a person by email address to collaborate on the app. Optional
73
+ # third parameter can be edit or view.
74
+ def add_collaborator(app_name, email, access='view')
75
+ xml(post("/apps/#{app_name}/collaborators", { 'collaborator[email]' => email, 'collaborator[access]' => access }))
76
+ end
77
+
78
+ # Change an existing collaborator.
79
+ def update_collaborator(app_name, email, access)
80
+ put("/apps/#{app_name}/collaborators/#{escape(email)}", { 'collaborator[access]' => access })
81
+ end
82
+
83
+ # Remove a collaborator.
84
+ def remove_collaborator(app_name, email)
85
+ delete("/apps/#{app_name}/collaborators/#{escape(email)}")
86
+ end
87
+
88
+ # Get the list of ssh public keys for the current user.
89
+ def keys
90
+ doc = xml get('/user/keys')
91
+ doc.elements.to_a('//keys/key').map do |key|
92
+ key.elements['contents'].text
93
+ end
94
+ end
95
+
96
+ # Add an ssh public key to the current user.
97
+ def add_key(key)
98
+ post("/user/keys", key, { 'Content-Type' => 'text/ssh-authkey' })
99
+ end
100
+
101
+ # Remove an existing ssh public key from the current user.
102
+ def remove_key(key)
103
+ delete("/user/keys/#{escape(key)}")
104
+ end
105
+
106
+ # Clear all keys on the current user.
107
+ def remove_all_keys
108
+ delete("/user/keys")
109
+ end
110
+
111
+ # Run a rake command on the Heroku app.
112
+ def rake(app_name, cmd)
113
+ post("/apps/#{app_name}/rake", cmd)
114
+ end
115
+
116
+ ##################
117
+
118
+ def resource(uri)
119
+ RestClient::Resource.new("http://#{host}", user, password)[uri]
120
+ end
121
+
122
+ def get(uri, extra_headers={}) # :nodoc:
123
+ resource(uri).get(heroku_headers.merge(extra_headers))
124
+ end
125
+
126
+ def post(uri, payload="", extra_headers={}) # :nodoc:
127
+ resource(uri).post(payload, heroku_headers.merge(extra_headers))
128
+ end
129
+
130
+ def put(uri, payload, extra_headers={}) # :nodoc:
131
+ resource(uri).put(payload, heroku_headers.merge(extra_headers))
132
+ end
133
+
134
+ def delete(uri) # :nodoc:
135
+ resource(uri).delete
136
+ end
137
+
138
+ def heroku_headers # :nodoc:
139
+ {
140
+ 'X-Heroku-API-Version' => '2',
141
+ 'User-Agent' => "herokugarden-gem/#{self.class.version}",
142
+ }
143
+ end
144
+
145
+ def xml(raw) # :nodoc:
146
+ REXML::Document.new(raw)
147
+ end
148
+
149
+ def escape(value) # :nodoc:
150
+ escaped = URI.escape(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
151
+ escaped.gsub('.', '%2E') # not covered by the previous URI.escape
152
+ end
153
+ end
@@ -0,0 +1,450 @@
1
+ require 'fileutils'
2
+
3
+ # This wraps the Heroku::Client class with higher-level actions suitable for
4
+ # use from the command line.
5
+ class HerokuGarden::CommandLine
6
+ class CommandFailed < RuntimeError; end
7
+
8
+ def execute(command, args)
9
+ send(command, args)
10
+ rescue RestClient::Unauthorized
11
+ display "Authentication failure"
12
+ rescue RestClient::ResourceNotFound
13
+ display "Resource not found. (Did you mistype the app name?)"
14
+ rescue RestClient::RequestFailed => e
15
+ display extract_error(e.response)
16
+ rescue HerokuGarden::CommandLine::CommandFailed => e
17
+ display e.message
18
+ end
19
+
20
+ def list(args)
21
+ list = heroku.list
22
+ if list.size > 0
23
+ display list.join("\n")
24
+ else
25
+ display "You have no apps."
26
+ end
27
+ end
28
+
29
+ def info(args)
30
+ name = args.shift.downcase.strip rescue ""
31
+ if name.length == 0 or name.slice(0, 1) == '-'
32
+ display "Usage: herokugarden info <app>"
33
+ else
34
+ attrs = heroku.info(name)
35
+ display "=== #{attrs[:name]}"
36
+ display "Web URL: http://#{attrs[:name]}.#{heroku.host}/"
37
+ display "Domain name: http://#{attrs[:domain_name]}/" if attrs[:domain_name]
38
+ display "Git Repo: git@#{heroku.host}:#{attrs[:name]}.git"
39
+ display "Mode: #{ attrs[:production] == 'true' ? 'production' : 'development' }"
40
+ display "Public: #{ attrs[:'share-public'] == 'true' ? 'true' : 'false' }"
41
+
42
+ first = true
43
+ lead = "Collaborators:"
44
+ attrs[:collaborators].each do |collaborator|
45
+ display "#{first ? lead : ' ' * lead.length} #{collaborator[:email]} (#{collaborator[:access]})"
46
+ first = false
47
+ end
48
+ end
49
+ end
50
+
51
+ def create(args)
52
+ name = args.shift.downcase.strip rescue nil
53
+ name = heroku.create(name, {})
54
+ display "Created http://#{name}.#{heroku.host}/ | git@#{heroku.host}:#{name}.git"
55
+ end
56
+
57
+ def update(args)
58
+ name = args.shift.downcase.strip rescue ""
59
+ raise CommandFailed, "Invalid app name" if name.length == 0 or name.slice(0, 1) == '-'
60
+
61
+ attributes = {}
62
+ extract_option(args, '--name') do |new_name|
63
+ attributes[:name] = new_name
64
+ end
65
+ extract_option(args, '--public', %w( true false )) do |public|
66
+ attributes[:share_public] = (public == 'true')
67
+ end
68
+ extract_option(args, '--mode', %w( production development )) do |mode|
69
+ attributes[:production] = (mode == 'production')
70
+ end
71
+ raise CommandFailed, "Nothing to update" if attributes.empty?
72
+ heroku.update(name, attributes)
73
+
74
+ app_name = attributes[:name] || name
75
+ display "http://#{app_name}.#{heroku.host}/ updated"
76
+ end
77
+
78
+ def clone(args)
79
+ name = args.shift.downcase.strip rescue ""
80
+ if name.length == 0 or name.slice(0, 1) == '-'
81
+ display "Usage: herokugarden clone <app>"
82
+ display "(this command is deprecated in favor of using the git repo url directly)"
83
+ else
84
+ cmd = "git clone #{git_repo_for(name)}"
85
+ display cmd
86
+ system cmd
87
+ end
88
+ end
89
+
90
+ def destroy(args)
91
+ name = args.shift.strip.downcase rescue ""
92
+ if name.length == 0 or name.slice(0, 1) == '-'
93
+ display "Usage: herokugarden destroy <app>"
94
+ else
95
+ heroku.destroy(name)
96
+ display "Destroyed #{name}"
97
+ end
98
+ end
99
+
100
+ def sharing(args)
101
+ name = args.shift.strip.downcase rescue ""
102
+ if name.length == 0 or name.slice(0, 1) == '-'
103
+ display "Usage: herokugarden sharing <app>"
104
+ else
105
+ access = extract_option(args, '--access', %w( edit view )) || 'view'
106
+ extract_option(args, '--add') do |email|
107
+ return add_collaborator(name, email, access)
108
+ end
109
+ extract_option(args, '--update') do |email|
110
+ return update_collaborator(name, email, access)
111
+ end
112
+ extract_option(args, '--remove') do |email|
113
+ return remove_collaborator(name, email)
114
+ end
115
+ return list_collaborators(name)
116
+ end
117
+ end
118
+
119
+ def collaborators(args)
120
+ sharing(args)
121
+ end
122
+
123
+ def add_collaborator(name, email, access)
124
+ display heroku.add_collaborator(name, email, access)
125
+ end
126
+
127
+ def update_collaborator(name, email, access)
128
+ heroku.update_collaborator(name, email, access)
129
+ display "Collaborator updated"
130
+ end
131
+
132
+ def remove_collaborator(name, email)
133
+ heroku.remove_collaborator(name, email)
134
+ display "Collaborator removed"
135
+ end
136
+
137
+ def list_collaborators(name)
138
+ list = heroku.list_collaborators(name)
139
+ display list.map { |c| "#{c[:email]} (#{c[:access]})" }.join("\n")
140
+ end
141
+
142
+ def git_repo_for(name)
143
+ "git@#{heroku.host}:#{name}.git"
144
+ end
145
+
146
+ def rake(args)
147
+ app_name = args.shift.strip.downcase rescue ""
148
+ cmd = args.join(' ')
149
+ if app_name.length == 0 or cmd.length == 0
150
+ display "Usage: herokugarden rake <app> <command>"
151
+ else
152
+ puts heroku.rake(app_name, cmd)
153
+ end
154
+ end
155
+
156
+ def git_transition(args)
157
+ unless File.exists?(Dir.pwd + '/.git')
158
+ raise HerokuGarden::CommandLine::CommandFailed, "The current directory is not a git repository.\nBe sure to cd into the app's directory and try again."
159
+ end
160
+
161
+ gitconfig = File.read(Dir.pwd + '/.git/config')
162
+ if gitconfig.match(/url = git@herokugarden\.com:(.+)\.git/)
163
+ raise HerokuGarden::CommandLine::CommandFailed, "This git repo for #{app_name} is already up to date and pointing at herokugarden.com."
164
+ end
165
+ if app_name_match = gitconfig.match(/url = git@heroku\.com:(.+)\.git/)
166
+ app_name = app_name_match[1]
167
+ begin
168
+ attempt_git_transition(gitconfig, app_name, 'heroku.com')
169
+ rescue RestClient::ResourceNotFound
170
+ begin
171
+ attempt_git_transition(gitconfig, app_name, 'herokugarden.com')
172
+ rescue RestClient::ResourceNotFound
173
+ raise HerokuGarden::CommandLine::CommandFailed, "The current directory does not contain a known Heroku app."
174
+ end
175
+ end
176
+ else
177
+ raise HerokuGarden::CommandLine::CommandFailed, "The current directory does not contain a known Heroku app."
178
+ end
179
+ end
180
+
181
+ def attempt_git_transition(gitconfig, app_name, domain)
182
+ heroku.host = domain
183
+ if [nil, 'true'].include?(heroku.info(app_name, true)[:stack2])
184
+ raise HerokuGarden::CommandLine::CommandFailed, "#{app_name} is running on the Heroku production platform.\nThis git repo URL is valid and does not need to be changed."
185
+ else
186
+ File.open(Dir.pwd + '/.git/config', 'w') do |f|
187
+ f.write gitconfig.gsub("url = git@heroku.com:#{app_name}.git", "url = git@herokugarden.com:#{app_name}.git")
188
+ end
189
+ display "This git repo for #{app_name} has been updated to the new herokugarden.com URL."
190
+ end
191
+ end
192
+
193
+ ############
194
+ attr_accessor :credentials
195
+
196
+ def heroku # :nodoc:
197
+ @heroku ||= init_heroku
198
+ end
199
+
200
+ def init_heroku # :nodoc:
201
+ HerokuGarden::Client.new(user, password, ENV['HEROKU_HOST'] || 'herokugarden.com')
202
+ end
203
+
204
+ def user # :nodoc:
205
+ get_credentials
206
+ @credentials[0]
207
+ end
208
+
209
+ def password # :nodoc:
210
+ get_credentials
211
+ @credentials[1]
212
+ end
213
+
214
+ def credentials_file
215
+ "#{home_directory}/.heroku/herokugarden_credentials"
216
+ end
217
+
218
+ def get_credentials # :nodoc:
219
+ return if @credentials
220
+ unless @credentials = read_credentials
221
+ @credentials = ask_for_credentials
222
+ save_credentials
223
+ end
224
+ @credentials
225
+ end
226
+
227
+ def read_credentials
228
+ if File.exists? credentials_file
229
+ return File.read(credentials_file).split("\n")
230
+ end
231
+ end
232
+
233
+ def echo_off
234
+ system "stty -echo"
235
+ end
236
+
237
+ def echo_on
238
+ system "stty echo"
239
+ end
240
+
241
+ def ask_for_credentials
242
+ puts "Enter your Heroku credentials."
243
+
244
+ print "Email: "
245
+ user = gets.strip
246
+
247
+ print "Password: "
248
+ password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
249
+
250
+ [ user, password ]
251
+ end
252
+
253
+ def ask_for_password_on_windows
254
+ require "Win32API"
255
+ char = nil
256
+ password = ''
257
+
258
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
259
+ break if char == 10 || char == 13 # received carriage return or newline
260
+ if char == 127 || char == 8 # backspace and delete
261
+ password.slice!(-1, 1)
262
+ else
263
+ password << char.chr
264
+ end
265
+ end
266
+ puts
267
+ return password
268
+ end
269
+
270
+ def ask_for_password
271
+ echo_off
272
+ password = gets.strip
273
+ puts
274
+ echo_on
275
+ return password
276
+ end
277
+
278
+ def save_credentials
279
+ begin
280
+ write_credentials
281
+ add_key
282
+ rescue RestClient::Unauthorized => e
283
+ delete_credentials
284
+ raise e unless retry_login?
285
+
286
+ display "\nAuthentication failed"
287
+ @credentials = ask_for_credentials
288
+ @heroku = init_heroku
289
+ retry
290
+ rescue Exception => e
291
+ delete_credentials
292
+ raise e
293
+ end
294
+ end
295
+
296
+ def retry_login?
297
+ @login_attempts ||= 0
298
+ @login_attempts += 1
299
+ @login_attempts < 3
300
+ end
301
+
302
+ def write_credentials
303
+ FileUtils.mkdir_p(File.dirname(credentials_file))
304
+ File.open(credentials_file, 'w') do |f|
305
+ f.puts self.credentials
306
+ end
307
+ set_credentials_permissions
308
+ end
309
+
310
+ def set_credentials_permissions
311
+ FileUtils.chmod 0700, File.dirname(credentials_file)
312
+ FileUtils.chmod 0600, credentials_file
313
+ end
314
+
315
+ def delete_credentials
316
+ FileUtils.rm_f(credentials_file)
317
+ end
318
+
319
+ def extract_option(args, options, valid_values=nil)
320
+ values = options.is_a?(Array) ? options : [options]
321
+ return unless opt_index = args.select { |a| values.include? a }.first
322
+ opt_value = args[args.index(opt_index) + 1] rescue nil
323
+
324
+ # remove option from args
325
+ args.delete(opt_index)
326
+ args.delete(opt_value)
327
+
328
+ if valid_values
329
+ opt_value = opt_value.downcase if opt_value
330
+ raise CommandFailed, "Invalid value '#{opt_value}' for option #{values.last}" unless valid_values.include?(opt_value)
331
+ end
332
+
333
+ block_given? ? yield(opt_value) : opt_value
334
+ end
335
+
336
+ def keys(*args)
337
+ args = args.first # something weird with ruby argument passing
338
+
339
+ if args.empty?
340
+ list_keys
341
+ return
342
+ end
343
+
344
+ extract_option(args, '--add') do |keyfile|
345
+ add_key(keyfile)
346
+ return
347
+ end
348
+ extract_option(args, '--remove') do |arg|
349
+ remove_key(arg)
350
+ return
351
+ end
352
+
353
+ display "Usage: heroku keys [--add or --remove]"
354
+ end
355
+
356
+ def list_keys
357
+ keys = heroku.keys
358
+ if keys.empty?
359
+ display "No keys for #{user}"
360
+ else
361
+ display "=== #{keys.size} key#{keys.size > 1 ? 's' : ''} for #{user}"
362
+ keys.each do |key|
363
+ display key
364
+ end
365
+ end
366
+ end
367
+
368
+ def add_key(keyfile=nil)
369
+ keyfile ||= find_key
370
+ key = File.read(keyfile)
371
+
372
+ display "Uploading ssh public key #{keyfile}"
373
+ heroku.add_key(key)
374
+ end
375
+
376
+ def remove_key(arg)
377
+ if arg == 'all'
378
+ heroku.remove_all_keys
379
+ display "All keys removed."
380
+ else
381
+ heroku.remove_key(arg)
382
+ display "Key #{arg} removed."
383
+ end
384
+ end
385
+
386
+ def find_key
387
+ %w(rsa dsa).each do |key_type|
388
+ keyfile = "#{home_directory}/.ssh/id_#{key_type}.pub"
389
+ return keyfile if File.exists? keyfile
390
+ end
391
+ raise CommandFailed, "No ssh public key found in #{home_directory}/.ssh/id_[rd]sa.pub. You may want to specify the full path to the keyfile."
392
+ end
393
+
394
+ # vvv Deprecated
395
+ def upload_authkey(*args)
396
+ extract_key!
397
+ display "Uploading ssh public key"
398
+ display "(upload_authkey is deprecated, please use \"heroku keys --add\" instead)"
399
+ heroku.add_key(authkey)
400
+ end
401
+
402
+ def extract_key!
403
+ return unless key_path = extract_option(ARGV, ['-k', '--key'])
404
+ raise "Please inform the full path for your ssh public key" if File.directory?(key_path)
405
+ raise "Could not read ssh public key in #{key_path}" unless @ssh_key = authkey_read(key_path)
406
+ end
407
+
408
+ def authkey_type(key_type)
409
+ authkey_read("#{home_directory}/.ssh/id_#{key_type}.pub")
410
+ end
411
+
412
+ def authkey_read(filename)
413
+ File.read(filename) if File.exists?(filename)
414
+ end
415
+
416
+ def authkey
417
+ return @ssh_key if @ssh_key
418
+ %w( rsa dsa ).each do |key_type|
419
+ key = authkey_type(key_type)
420
+ return key if key
421
+ end
422
+ raise "Your ssh public key was not found. Make sure you have a rsa or dsa key in #{home_directory}/.ssh, or specify the full path to the keyfile."
423
+ end
424
+ # ^^^ Deprecated
425
+
426
+ def display(msg)
427
+ puts msg
428
+ end
429
+
430
+ def home_directory
431
+ running_on_windows? ? ENV['USERPROFILE'] : ENV['HOME']
432
+ end
433
+
434
+ def extract_error(response)
435
+ return "Not found" if response.code.to_i == 404
436
+
437
+ msg = parse_error_xml(response.body) rescue ''
438
+ msg = 'Internal server error' if msg.empty?
439
+ msg
440
+ end
441
+
442
+ def parse_error_xml(body)
443
+ xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
444
+ xml_errors.map { |a| a.text }.join(" / ")
445
+ end
446
+
447
+ def running_on_windows?
448
+ RUBY_PLATFORM =~ /mswin32/
449
+ end
450
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'fileutils'
4
+
5
+ require File.dirname(__FILE__) + '/../lib/herokugarden'
6
+ require 'command_line'
7
+
8
+ class Module
9
+ def redefine_const(name, value)
10
+ __send__(:remove_const, name) if const_defined?(name)
11
+ const_set(name, value)
12
+ end
13
+ end
@@ -0,0 +1,157 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe HerokuGarden::Client do
4
+ before do
5
+ @client = HerokuGarden::Client.new(nil, nil)
6
+ @resource = mock('heroku rest resource')
7
+ end
8
+
9
+ it "list -> get a list of this user's apps" do
10
+ @client.should_receive(:resource).with('/apps').and_return(@resource)
11
+ @resource.should_receive(:get).and_return <<EOXML
12
+ <?xml version="1.0" encoding="UTF-8"?>
13
+ <apps type="array">
14
+ <app><name>myapp1</name></app>
15
+ <app><name>myapp2</name></app>
16
+ </apps>
17
+ EOXML
18
+ @client.list.should == %w(myapp1 myapp2)
19
+ end
20
+
21
+ it "info -> get app attributes" do
22
+ @client.should_receive(:resource).with('/apps/myapp').and_return(@resource)
23
+ @resource.should_receive(:get).and_return <<EOXML
24
+ <?xml version='1.0' encoding='UTF-8'?>
25
+ <app>
26
+ <blessed type='boolean'>true</blessed>
27
+ <created-at type='datetime'>2008-07-08T17:21:50-07:00</created-at>
28
+ <id type='integer'>49134</id>
29
+ <name>testgems</name>
30
+ <production type='boolean'>true</production>
31
+ <share-public type='boolean'>true</share-public>
32
+ <domain_name/>
33
+ </app>
34
+ EOXML
35
+ @client.stub!(:list_collaborators).and_return([:jon, :mike])
36
+ @client.info('myapp').should == { :blessed => 'true', :'created-at' => '2008-07-08T17:21:50-07:00', :id => '49134', :name => 'testgems', :production => 'true', :'share-public' => 'true', :domain_name => nil, :collaborators => [:jon, :mike] }
37
+ end
38
+
39
+ it "create -> create a new blank app" do
40
+ @client.should_receive(:resource).with('/apps').and_return(@resource)
41
+ @resource.should_receive(:post).and_return <<EOXML
42
+ <?xml version="1.0" encoding="UTF-8"?>
43
+ <app><name>untitled-123</name></app>
44
+ EOXML
45
+ @client.create.should == "untitled-123"
46
+ end
47
+
48
+ it "create(name) -> create a new blank app with a specified name" do
49
+ @client.should_receive(:resource).with('/apps').and_return(@resource)
50
+ @resource.should_receive(:post).with({ 'app[name]' => 'newapp' }, @client.heroku_headers).and_return <<EOXML
51
+ <?xml version="1.0" encoding="UTF-8"?>
52
+ <app><name>newapp</name></app>
53
+ EOXML
54
+ @client.create("newapp").should == "newapp"
55
+ end
56
+
57
+ it "update(name, attributes) -> updates existing apps" do
58
+ @client.should_receive(:resource).with('/apps/myapp').and_return(@resource)
59
+ @resource.should_receive(:put).with({ :app => { :mode => 'production', :public => true } }, anything)
60
+ @client.update("myapp", :mode => 'production', :public => true)
61
+ end
62
+
63
+ it "destroy(name) -> destroy the named app" do
64
+ @client.should_receive(:resource).with('/apps/destroyme').and_return(@resource)
65
+ @resource.should_receive(:delete)
66
+ @client.destroy("destroyme")
67
+ end
68
+
69
+ it "rake(app_name, cmd) -> run a rake command on the app" do
70
+ @client.should_receive(:resource).with('/apps/myapp/rake').and_return(@resource)
71
+ @resource.should_receive(:post).with('db:migrate', @client.heroku_headers)
72
+ @client.rake('myapp', 'db:migrate')
73
+ end
74
+
75
+ describe "collaborators" do
76
+ it "list(app_name) -> list app collaborators" do
77
+ @client.should_receive(:resource).with('/apps/myapp/collaborators').and_return(@resource)
78
+ @resource.should_receive(:get).and_return <<EOXML
79
+ <?xml version="1.0" encoding="UTF-8"?>
80
+ <collaborators type="array">
81
+ <collaborator><email>joe@example.com</email><access>edit</access></collaborator>
82
+ <collaborator><email>jon@example.com</email><access>view</access></collaborator>
83
+ </collaborators>
84
+ EOXML
85
+ @client.list_collaborators('myapp').should == [
86
+ { :email => 'joe@example.com', :access => 'edit' },
87
+ { :email => 'jon@example.com', :access => 'view' }
88
+ ]
89
+ end
90
+
91
+ it "add_collaborator(app_name, email, access) -> adds collaborator to app" do
92
+ @client.should_receive(:resource).with('/apps/myapp/collaborators').and_return(@resource)
93
+ @resource.should_receive(:post).with({ 'collaborator[email]' => 'joe@example.com', 'collaborator[access]' => 'edit'}, anything)
94
+ @client.add_collaborator('myapp', 'joe@example.com', 'edit')
95
+ end
96
+
97
+ it "update_collaborator(app_name, email, access) -> updates existing collaborator record" do
98
+ @client.should_receive(:resource).with('/apps/myapp/collaborators/joe%40example%2Ecom').and_return(@resource)
99
+ @resource.should_receive(:put).with({ 'collaborator[access]' => 'view'}, anything)
100
+ @client.update_collaborator('myapp', 'joe@example.com', 'view')
101
+ end
102
+
103
+ it "remove_collaborator(app_name, email, access) -> removes collaborator from app" do
104
+ @client.should_receive(:resource).with('/apps/myapp/collaborators/joe%40example%2Ecom').and_return(@resource)
105
+ @resource.should_receive(:delete)
106
+ @client.remove_collaborator('myapp', 'joe@example.com')
107
+ end
108
+ end
109
+
110
+ describe "ssh keys" do
111
+ it "fetches a list of the user's current keys" do
112
+ @client.should_receive(:resource).with('/user/keys').and_return(@resource)
113
+ @resource.should_receive(:get).and_return <<EOXML
114
+ <?xml version="1.0" encoding="UTF-8"?>
115
+ <keys type="array">
116
+ <key>
117
+ <contents>ssh-dss thekey== joe@workstation</contents>
118
+ </key>
119
+ </keys>
120
+ EOXML
121
+ @client.keys.should == [ "ssh-dss thekey== joe@workstation" ]
122
+ end
123
+
124
+ it "add_key(key) -> add an ssh key (e.g., the contents of id_rsa.pub) to the user" do
125
+ @client.should_receive(:resource).with('/user/keys').and_return(@resource)
126
+ @client.stub!(:heroku_headers).and_return({})
127
+ @resource.should_receive(:post).with('a key', 'Content-Type' => 'text/ssh-authkey')
128
+ @client.add_key('a key')
129
+ end
130
+
131
+ it "remove_key(key) -> remove an ssh key by name (user@box)" do
132
+ @client.should_receive(:resource).with('/user/keys/joe%40workstation').and_return(@resource)
133
+ @resource.should_receive(:delete)
134
+ @client.remove_key('joe@workstation')
135
+ end
136
+
137
+ it "remove_all_keys -> removes all ssh keys for the user" do
138
+ @client.should_receive(:resource).with('/user/keys').and_return(@resource)
139
+ @resource.should_receive(:delete)
140
+ @client.remove_all_keys
141
+ end
142
+ end
143
+
144
+ describe "internal" do
145
+ it "creates a RestClient resource for making calls" do
146
+ @client.stub!(:host).and_return('heroku.com')
147
+ @client.stub!(:user).and_return('joe@example.com')
148
+ @client.stub!(:password).and_return('secret')
149
+
150
+ res = @client.resource('/xyz')
151
+
152
+ res.url.should == 'http://heroku.com/xyz'
153
+ res.user.should == 'joe@example.com'
154
+ res.password.should == 'secret'
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,334 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe HerokuGarden::CommandLine do
4
+ before do
5
+ @cli = HerokuGarden::CommandLine.new
6
+ @cli.stub!(:display)
7
+ @cli.stub!(:print)
8
+ @cli.stub!(:ask_for_credentials).and_raise("ask_for_credentials should not be called by specs")
9
+ end
10
+
11
+ describe "credentials" do
12
+ it "reads credentials from the credentials file" do
13
+ sandbox = "/tmp/cli_spec_#{Process.pid}"
14
+ File.open(sandbox, "w") { |f| f.write "user\npass\n" }
15
+ @cli.stub!(:credentials_file).and_return(sandbox)
16
+ @cli.read_credentials.should == %w(user pass)
17
+ end
18
+
19
+ it "takes the user from the first line and the password from the second line" do
20
+ @cli.stub!(:read_credentials).and_return(%w(user pass))
21
+ @cli.user.should == 'user'
22
+ @cli.password.should == 'pass'
23
+ end
24
+
25
+ it "asks for credentials when the file doesn't exist" do
26
+ sandbox = "/tmp/cli_spec_#{Process.pid}"
27
+ FileUtils.rm_rf(sandbox)
28
+ @cli.stub!(:credentials_file).and_return(sandbox)
29
+ @cli.should_receive(:ask_for_credentials).and_return([ 'u', 'p'])
30
+ @cli.should_receive(:save_credentials)
31
+ @cli.get_credentials.should == [ 'u', 'p' ]
32
+ end
33
+
34
+ it "writes the credentials to a file" do
35
+ sandbox = "/tmp/cli_spec_#{Process.pid}"
36
+ FileUtils.rm_rf(sandbox)
37
+ @cli.stub!(:credentials_file).and_return(sandbox)
38
+ @cli.stub!(:credentials).and_return(['one', 'two'])
39
+ @cli.should_receive(:set_credentials_permissions)
40
+ @cli.write_credentials
41
+ File.read(sandbox).should == "one\ntwo\n"
42
+ end
43
+
44
+ it "sets ~/.heroku/credentials to be readable only by the user" do
45
+ sandbox = "/tmp/cli_spec_#{Process.pid}"
46
+ FileUtils.rm_rf(sandbox)
47
+ FileUtils.mkdir_p(sandbox)
48
+ fname = "#{sandbox}/file"
49
+ system "touch #{fname}"
50
+ @cli.stub!(:credentials_file).and_return(fname)
51
+ @cli.set_credentials_permissions
52
+ File.stat(sandbox).mode.should == 040700
53
+ File.stat(fname).mode.should == 0100600
54
+ end
55
+
56
+ it "writes credentials and uploads authkey when credentials are saved" do
57
+ @cli.stub!(:credentials)
58
+ @cli.should_receive(:write_credentials)
59
+ @cli.should_receive(:add_key)
60
+ @cli.save_credentials
61
+ end
62
+
63
+ it "save_credentials deletes the credentials when the upload authkey is unauthorized" do
64
+ @cli.stub!(:write_credentials)
65
+ @cli.stub!(:retry_login?).and_return(false)
66
+ @cli.should_receive(:add_key).and_raise(RestClient::Unauthorized)
67
+ @cli.should_receive(:delete_credentials)
68
+ lambda { @cli.save_credentials }.should raise_error(RestClient::Unauthorized)
69
+ end
70
+
71
+ it "save_credentials deletes the credentials when there's no authkey" do
72
+ @cli.stub!(:write_credentials)
73
+ @cli.should_receive(:add_key).and_raise(RuntimeError)
74
+ @cli.should_receive(:delete_credentials)
75
+ lambda { @cli.save_credentials }.should raise_error
76
+ end
77
+
78
+ it "save_credentials deletes the credentials when the authkey is weak" do
79
+ @cli.stub!(:write_credentials)
80
+ @cli.should_receive(:add_key).and_raise(RestClient::RequestFailed)
81
+ @cli.should_receive(:delete_credentials)
82
+ lambda { @cli.save_credentials }.should raise_error
83
+ end
84
+
85
+ it "asks for login again when not authorized, for three times" do
86
+ @cli.stub!(:write_credentials)
87
+ @cli.stub!(:delete_credentials)
88
+ @cli.stub!(:add_key).and_raise(RestClient::Unauthorized)
89
+ @cli.should_receive(:ask_for_credentials).exactly(4).times
90
+ lambda { @cli.save_credentials }.should raise_error(RestClient::Unauthorized)
91
+ end
92
+
93
+ it "deletes the credentials file" do
94
+ FileUtils.should_receive(:rm_f).with(@cli.credentials_file)
95
+ @cli.delete_credentials
96
+ end
97
+ end
98
+
99
+ describe "key management" do
100
+ before do
101
+ @cli.instance_variable_set('@credentials', %w(user pass))
102
+ end
103
+
104
+ it "finds the user's ssh key in ~/ssh/id_rsa.pub" do
105
+ @cli.stub!(:home_directory).and_return('/home/joe')
106
+ File.should_receive(:exists?).with('/home/joe/.ssh/id_rsa.pub').and_return(true)
107
+ @cli.find_key.should == '/home/joe/.ssh/id_rsa.pub'
108
+ end
109
+
110
+ it "finds the user's ssh key in ~/ssh/id_dsa.pub" do
111
+ @cli.stub!(:home_directory).and_return('/home/joe')
112
+ File.should_receive(:exists?).with('/home/joe/.ssh/id_rsa.pub').and_return(false)
113
+ File.should_receive(:exists?).with('/home/joe/.ssh/id_dsa.pub').and_return(true)
114
+ @cli.find_key.should == '/home/joe/.ssh/id_dsa.pub'
115
+ end
116
+
117
+ it "raises an exception if neither id_rsa or id_dsa were found" do
118
+ @cli.stub!(:home_directory).and_return('/home/joe')
119
+ File.stub!(:exists?).and_return(false)
120
+ lambda { @cli.find_key }.should raise_error(HerokuGarden::CommandLine::CommandFailed)
121
+ end
122
+
123
+ it "adds a key from the default locations if no key filename is supplied" do
124
+ @cli.should_receive(:find_key).and_return('/home/joe/.ssh/id_rsa.pub')
125
+ File.should_receive(:read).with('/home/joe/.ssh/id_rsa.pub').and_return('ssh-rsa xyz')
126
+ @cli.heroku.should_receive(:add_key).with('ssh-rsa xyz')
127
+ @cli.add_key
128
+ end
129
+
130
+ it "adds a key from a specified keyfile path" do
131
+ @cli.should_not_receive(:find_key)
132
+ File.should_receive(:read).with('/my/key.pub').and_return('ssh-rsa xyz')
133
+ @cli.heroku.should_receive(:add_key).with('ssh-rsa xyz')
134
+ @cli.add_key('/my/key.pub')
135
+ end
136
+ end
137
+
138
+ describe "deprecated key management" do
139
+ it "gets pub keys from the user's home directory" do
140
+ @cli.should_receive(:home_directory).and_return('/Users/joe')
141
+ File.should_receive(:exists?).with('/Users/joe/.ssh/id_xyz.pub').and_return(true)
142
+ File.should_receive(:read).with('/Users/joe/.ssh/id_xyz.pub').and_return('ssh-xyz somehexkey')
143
+ @cli.authkey_type('xyz').should == 'ssh-xyz somehexkey'
144
+ end
145
+
146
+ it "gets the rsa key" do
147
+ @cli.stub!(:authkey_type).with('rsa').and_return('ssh-rsa somehexkey')
148
+ @cli.authkey.should == 'ssh-rsa somehexkey'
149
+ end
150
+
151
+ it "gets the dsa key when there's no rsa" do
152
+ @cli.stub!(:authkey_type).with('rsa').and_return(nil)
153
+ @cli.stub!(:authkey_type).with('dsa').and_return('ssh-dsa somehexkey')
154
+ @cli.authkey.should == 'ssh-dsa somehexkey'
155
+ end
156
+
157
+ it "raises a friendly error message when no key is found" do
158
+ @cli.stub!(:authkey_type).with('rsa').and_return(nil)
159
+ @cli.stub!(:authkey_type).with('dsa').and_return(nil)
160
+ lambda { @cli.authkey }.should raise_error
161
+ end
162
+
163
+ it "accepts a custom key via the -k parameter" do
164
+ Object.redefine_const(:ARGV, ['-k', '/Users/joe/sshkeys/mykey.pub'])
165
+ @cli.should_receive(:authkey_read).with('/Users/joe/sshkeys/mykey.pub').and_return('ssh-rsa somehexkey')
166
+ @cli.extract_key!
167
+ @cli.authkey.should == 'ssh-rsa somehexkey'
168
+ end
169
+
170
+ it "extracts options from ARGV" do
171
+ Object.redefine_const(:ARGV, %w( a b --something value c d ))
172
+ @cli.extract_option(ARGV, '--something').should == 'value'
173
+ ARGV.should == %w( a b c d )
174
+ end
175
+
176
+ it "rejects options outside valid values for ARGV" do
177
+ Object.redefine_const(:ARGV, %w( -boolean_option t ))
178
+ lambda { @cli.extract_argv_option('-boolean_option', %w( true false )) }.should raise_error
179
+ end
180
+
181
+ it "uploads the ssh authkey (deprecated in favor of add_key)" do
182
+ @cli.should_receive(:extract_key!)
183
+ @cli.should_receive(:authkey).and_return('my key')
184
+ heroku = mock("heroku client")
185
+ @cli.should_receive(:init_heroku).and_return(heroku)
186
+ heroku.should_receive(:add_key).with('my key')
187
+ @cli.upload_authkey
188
+ end
189
+
190
+ it "gets the home directory from HOME when running on *nix" do
191
+ ENV.should_receive(:[]).with('HOME').and_return(@home)
192
+ @cli.stub!(:running_on_windows?).and_return(false)
193
+ @cli.home_directory.should == @home
194
+ end
195
+
196
+ it "gets the home directory from USERPROFILE when running on windows" do
197
+ ENV.should_receive(:[]).with('USERPROFILE').and_return(@home)
198
+ @cli.stub!(:running_on_windows?).and_return(true)
199
+ @cli.home_directory.should == @home
200
+ end
201
+
202
+ it "detects it's running on windows" do
203
+ Object.redefine_const(:RUBY_PLATFORM, 'i386-mswin32')
204
+ @cli.should be_running_on_windows
205
+ end
206
+
207
+ it "doesn't consider cygwin as windows" do
208
+ Object.redefine_const(:RUBY_PLATFORM, 'i386-cygwin')
209
+ @cli.should_not be_running_on_windows
210
+ end
211
+ end
212
+
213
+ describe "execute" do
214
+ it "executes an action" do
215
+ @cli.should_receive(:my_action).with(%w(arg1 arg2))
216
+ @cli.execute('my_action', %w(arg1 arg2))
217
+ end
218
+
219
+ it "catches unauthorized errors" do
220
+ @cli.should_receive(:my_action).and_raise(RestClient::Unauthorized)
221
+ @cli.should_receive(:display).with('Authentication failure')
222
+ @cli.execute('my_action', 'args')
223
+ end
224
+
225
+ it "parses rails-format error xml" do
226
+ @cli.parse_error_xml('<errors><error>Error 1</error><error>Error 2</error></errors>').should == 'Error 1 / Error 2'
227
+ end
228
+
229
+ it "does not catch general exceptions, those are shown to the user as normal" do
230
+ @cli.should_receive(:my_action).and_raise(RuntimeError)
231
+ lambda { @cli.execute('my_action', 'args') }.should raise_error(RuntimeError)
232
+ end
233
+ end
234
+
235
+ describe "app actions" do
236
+ before do
237
+ @cli.instance_variable_set('@credentials', %w(user pass))
238
+ end
239
+
240
+ it "shows app info" do
241
+ @cli.heroku.should_receive(:info).with('myapp').and_return({ :name => 'myapp', :collaborators => [] })
242
+ @cli.heroku.stub!(:domain).and_return('heroku.com')
243
+ @cli.should_receive(:display).with('=== myapp')
244
+ @cli.should_receive(:display).with('Web URL: http://myapp.herokugarden.com/')
245
+ @cli.info([ 'myapp' ])
246
+ end
247
+
248
+ it "creates without a name" do
249
+ @cli.heroku.should_receive(:create).with(nil, {}).and_return("untitled-123")
250
+ @cli.create([])
251
+ end
252
+
253
+ it "creates with a name" do
254
+ @cli.heroku.should_receive(:create).with('myapp', {}).and_return("myapp")
255
+ @cli.create([ 'myapp' ])
256
+ end
257
+
258
+ it "updates app" do
259
+ @cli.heroku.should_receive(:update).with('myapp', { :name => 'myapp2', :share_public => true, :production => true })
260
+ @cli.update([ 'myapp', '--name', 'myapp2', '--public', 'true', '--mode', 'production' ])
261
+ end
262
+
263
+ it "clones the app (deprecated in favor of straight git clone)" do
264
+ @cli.should_receive(:system).with('git clone git@herokugarden.com:myapp.git')
265
+ @cli.clone([ 'myapp' ])
266
+ end
267
+ end
268
+
269
+ describe "collaborators" do
270
+ before do
271
+ @cli.instance_variable_set('@credentials', %w(user pass))
272
+ end
273
+
274
+ it "list collaborators when there's just the app name" do
275
+ @cli.heroku.should_receive(:list_collaborators).and_return([])
276
+ @cli.collaborators(['myapp'])
277
+ end
278
+
279
+ it "add collaborators with default access to view only" do
280
+ @cli.heroku.should_receive(:add_collaborator).with('myapp', 'joe@example.com', 'view')
281
+ @cli.collaborators(['myapp', '--add', 'joe@example.com'])
282
+ end
283
+
284
+ it "add collaborators with edit access" do
285
+ @cli.heroku.should_receive(:add_collaborator).with('myapp', 'joe@example.com', 'edit')
286
+ @cli.collaborators(['myapp', '--add', 'joe@example.com', '--access', 'edit'])
287
+ end
288
+
289
+ it "updates collaborators" do
290
+ @cli.heroku.should_receive(:update_collaborator).with('myapp', 'joe@example.com', 'view')
291
+ @cli.collaborators(['myapp', '--update', 'joe@example.com', '--access', 'view'])
292
+ end
293
+
294
+ it "removes collaborators" do
295
+ @cli.heroku.should_receive(:remove_collaborator).with('myapp', 'joe@example.com')
296
+ @cli.collaborators(['myapp', '--remove', 'joe@example.com'])
297
+ end
298
+ end
299
+
300
+ describe "git transition" do
301
+ before do
302
+ @cli.instance_variable_set('@credentials', %w(user pass))
303
+ Dir.stub!(:pwd).and_return('/apps/myapp')
304
+ end
305
+
306
+ it "requires a git repo" do
307
+ File.stub!(:exists?).with('/apps/myapp/.git').and_return(false)
308
+ lambda { @cli.git_transition([]) }.should raise_error(HerokuGarden::CommandLine::CommandFailed)
309
+ end
310
+
311
+ it "requires a remote entry with git@heroku.com:appname.git" do
312
+ File.stub!(:exists?).with('/apps/myapp/.git').and_return(true)
313
+ File.stub!(:read).with('/apps/myapp/.git/config').and_return('config without app')
314
+ lambda { @cli.git_transition([]) }.should raise_error(HerokuGarden::CommandLine::CommandFailed)
315
+ end
316
+
317
+ it "doesn't update stack2 apps" do
318
+ File.stub!(:exists?).with('/apps/myapp/.git').and_return(true)
319
+ File.stub!(:read).with('/apps/myapp/.git/config').and_return("[remote \"heroku\"]\n\turl = git@heroku.com:myapp.git")
320
+ @cli.heroku.should_receive(:info).with('myapp', true).and_return({ :stack2 => 'true' })
321
+ lambda { @cli.git_transition([]) }.should raise_error(HerokuGarden::CommandLine::CommandFailed)
322
+ end
323
+
324
+ it "updates the gitconfig" do
325
+ File.stub!(:exists?).with('/apps/myapp/.git').and_return(true)
326
+ File.stub!(:read).with('/apps/myapp/.git/config').and_return("[remote \"heroku\"]\n\turl = git@heroku.com:myapp.git")
327
+ @cli.heroku.should_receive(:info).with('myapp', true).and_return({ :stack2 => 'false' })
328
+ gitconfig = mock('git config')
329
+ File.should_receive(:open).with('/apps/myapp/.git/config', 'w').and_yield(gitconfig)
330
+ gitconfig.should_receive(:write).with("[remote \"heroku\"]\n\turl = git@herokugarden.com:myapp.git")
331
+ @cli.git_transition([])
332
+ end
333
+ end
334
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: herokugarden
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.2
5
+ platform: ruby
6
+ authors:
7
+ - Adam Wiggins
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-14 00:00:00 -08:00
13
+ default_executable: herokugarden
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rest-client
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.5"
24
+ version:
25
+ description: Client library and command-line tool to manage and deploy Rails apps on Heroku Garden.
26
+ email: feedback@heroku.com
27
+ executables:
28
+ - herokugarden
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - Rakefile
35
+ - bin/herokugarden
36
+ - lib/herokugarden
37
+ - lib/herokugarden/client.rb
38
+ - lib/herokugarden/command_line.rb
39
+ - lib/herokugarden.rb
40
+ - spec/base.rb
41
+ - spec/client_spec.rb
42
+ - spec/command_line_spec.rb
43
+ has_rdoc: true
44
+ homepage: http://herokugarden.com/
45
+ post_install_message: |+
46
+
47
+ ---> Installing herokugarden v0.4.2
48
+ ---> Migrate local checkouts using the git:transition command:
49
+
50
+ cd myapp/
51
+ herokugarden git:transition
52
+
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project: herokugarden
72
+ rubygems_version: 1.2.0
73
+ signing_key:
74
+ specification_version: 2
75
+ summary: Client library and CLI to deploy Rails apps on Heroku Garden.
76
+ test_files: []
77
+