pagoda 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.bundle/config ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_DISABLE_SHARED_GEMS: "1"
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pagoda-*.*.*.gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in pagoda.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,39 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pagoda (0.0.1)
5
+ crack
6
+ iniparse
7
+ json_pure
8
+ rest-client
9
+
10
+ GEM
11
+ remote: http://rubygems.org/
12
+ specs:
13
+ addressable (2.2.5)
14
+ crack (0.1.8)
15
+ diff-lcs (1.1.2)
16
+ iniparse (1.1.4)
17
+ json_pure (1.5.1)
18
+ mime-types (1.16)
19
+ rest-client (1.6.1)
20
+ mime-types (>= 1.16)
21
+ rspec (2.5.0)
22
+ rspec-core (~> 2.5.0)
23
+ rspec-expectations (~> 2.5.0)
24
+ rspec-mocks (~> 2.5.0)
25
+ rspec-core (2.5.1)
26
+ rspec-expectations (2.5.0)
27
+ diff-lcs (~> 1.1.2)
28
+ rspec-mocks (2.5.0)
29
+ webmock (1.6.2)
30
+ addressable (>= 2.2.2)
31
+ crack (>= 0.1.7)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ pagoda!
38
+ rspec
39
+ webmock
data/README ADDED
@@ -0,0 +1,3 @@
1
+ Released under the MIT license. http://github.com/pagoda/pagoda-client
2
+
3
+ Special thanks to heroku. We borrowed the initial skeleton from the heroku client gem: http://github.com/heroku/heroku
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ require "rspec/core/rake_task"
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ desc "Run all specs"
7
+ RSpec::Core::RakeTask.new('spec') do |t|
8
+ t.rspec_opts = ['--colour --format progress']
9
+ end
10
+
11
+ task :default => :spec
data/bin/pagoda ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'pagoda/client'
7
+ require 'pagoda/command'
8
+
9
+ args = ARGV.dup
10
+ ARGV.clear
11
+ command = args.shift.strip rescue 'help'
12
+
13
+ Pagoda::Command.run(command, args)
@@ -0,0 +1,221 @@
1
+ require 'pagoda/version'
2
+ require 'rexml/document'
3
+ require 'rest_client'
4
+ require 'uri'
5
+ require 'json/pure' unless {}.respond_to?(:to_json)
6
+
7
+ class Pagoda::Client
8
+
9
+ attr_reader :user, :password
10
+
11
+ class << self
12
+ def version
13
+ Pagoda::VERSION
14
+ end
15
+
16
+ def gem_version_string
17
+ "pagoda-gem/#{version}"
18
+ end
19
+ end
20
+
21
+ def initialize(user, password)
22
+ @user = user
23
+ @password = password
24
+ end
25
+
26
+ def app_list
27
+ doc = xml(get("/apps.xml").to_s)
28
+ doc.elements['apps'].elements.to_a('//app/').inject([]) do |list, app|
29
+ list << {
30
+ :name => app.elements['name'].text,
31
+ :instances => app.elements['instances'].text,
32
+ :git_url => app.elements['git-url'].text
33
+ }
34
+ end
35
+ end
36
+
37
+ def database_exists?(app, mysql_instance)
38
+ begin
39
+ response = get("/apps/#{app}/databases/#{mysql_instance}.xml")
40
+ true
41
+ rescue RestClient::ResourceNotFound => e
42
+ false
43
+ end
44
+
45
+ end
46
+
47
+ def app_create(name, git_url)
48
+ doc = xml(post("/apps.xml", {:app => {:name => name, :git_url => git_url}}).to_s)
49
+ doc.elements.to_a('//app/*').inject({}) do |hash, element|
50
+ case element.name
51
+ when "owner"
52
+ hash[:owner] = {:username => element.elements['username'].text, :email => element.elements['email'].text}
53
+ when "collaborators"
54
+ hash[:collaborators] = element.elements.to_a('//collaborator/').inject([]) do |list, collaborator|
55
+ list << {:username => collaborator.elements['username'].text, :email => collaborator.elements['email'].text}
56
+ end
57
+ when "transactions"
58
+ hash[:transactions] = element.elements.to_a('//transaction/').inject([]) do |list, transaction|
59
+ list << {
60
+ :id => transaction.elements["id"].text,
61
+ :name => transaction.elements["name"].text,
62
+ :description => transaction.elements["description"].text,
63
+ :state => transaction.elements["state"].text,
64
+ :status => transaction.elements["status"].text
65
+ }
66
+ end
67
+ else
68
+ hash[element.name.gsub(/-/, '_').to_sym] = element.text
69
+ end
70
+ hash
71
+ end
72
+ end
73
+
74
+ def app_info(app)
75
+ doc = xml(get("/apps/#{app}.xml").to_s)
76
+ doc.elements.to_a('//app/*').inject({}) do |hash, element|
77
+ case element.name
78
+ when "owner"
79
+ hash[:owner] = {:username => element.elements['username'].text, :email => element.elements['email'].text}
80
+ when "collaborators"
81
+ hash[:collaborators] = element.elements.to_a('//collaborator/').inject([]) do |list, collaborator|
82
+ list << {:username => collaborator.elements['username'].text, :email => collaborator.elements['email'].text}
83
+ end
84
+ when "transactions"
85
+ hash[:transactions] = element.elements.to_a('//transaction/').inject([]) do |list, transaction|
86
+ list << {
87
+ :id => transaction.elements["id"].text,
88
+ :name => transaction.elements["name"].text,
89
+ :description => transaction.elements["description"].text,
90
+ :state => transaction.elements["state"].text,
91
+ :status => transaction.elements["status"].text
92
+ }
93
+ end
94
+ else
95
+ hash[element.name.gsub(/-/, '_').to_sym] = element.text
96
+ end
97
+ hash
98
+ end
99
+ end
100
+
101
+ def app_update(app, updates)
102
+ put("/apps/#{app}.xml", {:update => updates}).to_s
103
+ end
104
+
105
+ def app_destroy(app)
106
+ delete("/apps/#{app}.xml").to_s
107
+ end
108
+
109
+ def transaction_list(app)
110
+ doc = xml(get("/apps/#{app}/transactions.xml").to_s)
111
+ doc.elements['transactions'].elements.to_a('//transaction/').inject([]) do |list, transaction|
112
+ list << {
113
+ :id => transaction.elements['id'].text,
114
+ :name => transaction.elements['name'].text,
115
+ :description => transaction.elements['description'].text,
116
+ :state => transaction.elements['state'].text,
117
+ :status => transaction.elements['status'].text
118
+ }
119
+ end
120
+ end
121
+
122
+ def transaction_status(app, transaction)
123
+ doc = xml(get("/apps/#{app}/transactions/#{transaction}.xml").to_s)
124
+ doc.elements.to_a('//transaction/*').inject({}) { |hash, element| hash[element.name.gsub(/-/, '_').to_sym] = element.text; hash }
125
+ end
126
+
127
+ def rewind(app, places=1)
128
+ doc = xml(put("/apps/#{app}/rewind.xml", {:places => places}).to_s)
129
+ doc.elements.to_a('//transaction/*').inject({}) { |hash, element| hash[element.name.gsub(/-/, '_').to_sym] = element.text; hash }
130
+ end
131
+
132
+ def fast_forward(app, places=1)
133
+ doc = xml(put("/apps/#{app}/fast-forward.xml", {:places => places}).to_s)
134
+ doc.elements.to_a('//transaction/*').inject({}) { |hash, element| hash[element.name.gsub(/-/, '_').to_sym] = element.text; hash }
135
+ end
136
+
137
+ def rollback(app)
138
+ get("/apps/#{app}/rollback.xml").to_s
139
+ end
140
+
141
+ def deploy_latest(app)
142
+ doc = xml(post("/apps/#{app}/deploy.xml").to_s)
143
+ doc.elements.to_a('//transaction/*').inject({}) { |hash, element| hash[element.name.gsub(/-/, '_').to_sym] = element.text; hash }
144
+ end
145
+
146
+ def deploy(app, branch, commit)
147
+ doc = xml(post("/apps/#{app}/deploy.xml", {:deploy => {:branch => branch, :commit => commit}}).to_s)
148
+ doc.elements.to_a('//transaction/*').inject({}) { |hash, element| hash[element.name.gsub(/-/, '_').to_sym] = element.text; hash }
149
+ end
150
+
151
+ # def deploy(app)
152
+ # doc = xml(put("/apps/#{app}/deploy.xml").to_s)
153
+ # doc.elements.to_a('//transaction/*').inject({}) { |hash, element| hash[element.name.gsub(/-/, '_').to_sym] = element.text; hash }
154
+ # end
155
+
156
+ def scale_up(app, qty=1)
157
+ doc = xml(put("/apps/#{app}/scale-up.xml").to_s)
158
+ doc.elements.to_a('//transaction/*').inject({}) { |hash, element| hash[element.name.gsub(/-/, '_').to_sym] = element.text; hash }
159
+ end
160
+
161
+ def scale_down(app, qty=1)
162
+ doc = xml(put("/apps/#{app}/scale-down.xml").to_s)
163
+ doc.elements.to_a('//transaction/*').inject({}) { |hash, element| hash[element.name.gsub(/-/, '_').to_sym] = element.text; hash }
164
+ end
165
+
166
+ def app_databases(app)
167
+ doc = xml(get("/apps/#{app}/databases.xml").to_s)
168
+ doc.elements['databases'].elements.to_a('//database/').inject([]) {|list, instance| list << {:name => instance.elements['name'].text} }
169
+ end
170
+
171
+ def on_warning(&blk)
172
+ @warning_callback = blk
173
+ end
174
+
175
+ protected
176
+
177
+ def resource(uri)
178
+ RestClient.proxy = ENV['HTTP_PROXY'] || ENV['http_proxy']
179
+ if uri =~ /^https?/
180
+ RestClient::Resource.new(uri, @user, @password)
181
+ else
182
+ # RestClient::Resource.new("http://127.0.0.1:3000#{uri}", @user, @password)
183
+ RestClient::Resource.new("https://dashboard.pagodabox.com#{uri}", @user, @password)
184
+ end
185
+ end
186
+
187
+ def get(uri, extra_headers={})
188
+ process(:get, uri, extra_headers)
189
+ end
190
+
191
+ def post(uri, payload="", extra_headers={})
192
+ process(:post, uri, extra_headers, payload)
193
+ end
194
+
195
+ def put(uri, payload="", extra_headers={})
196
+ process(:put, uri, extra_headers, payload)
197
+ end
198
+
199
+ def delete(uri, extra_headers={})
200
+ process(:delete, uri, extra_headers)
201
+ end
202
+
203
+ def process(method, uri, extra_headers={}, payload=nil)
204
+ headers = pagoda_headers.merge(extra_headers)
205
+ args = [method, payload, headers].compact
206
+ response = resource(uri).send(*args)
207
+ end
208
+
209
+ def pagoda_headers
210
+ {
211
+ 'User-Agent' => self.class.gem_version_string,
212
+ 'X-Ruby-Version' => RUBY_VERSION,
213
+ 'X-Ruby-Platform' => RUBY_PLATFORM,
214
+ }
215
+ end
216
+
217
+ def xml(raw) # :nodoc:
218
+ REXML::Document.new(raw)
219
+ end
220
+
221
+ end
@@ -0,0 +1,93 @@
1
+ require 'pagoda/helpers'
2
+ require 'pagoda/tunnel_proxy'
3
+ require 'pagoda/commands/base'
4
+
5
+ Dir["#{File.dirname(__FILE__)}/commands/*.rb"].each { |c| require c }
6
+
7
+ module Pagoda
8
+ module Command
9
+ class InvalidCommand < RuntimeError; end
10
+ class CommandFailed < RuntimeError; end
11
+
12
+ extend Pagoda::Helpers
13
+
14
+ class << self
15
+
16
+ def run(command, args, retries=0)
17
+ begin
18
+ run_internal 'auth:reauthorize', args.dup if retries > 0
19
+ run_internal(command, args.dup)
20
+ rescue InvalidCommand
21
+ error "Unknown command: #{command}. Run 'pagoda help' for usage information."
22
+ rescue RestClient::Unauthorized
23
+ if retries < 3
24
+ STDERR.puts "Authentication failure"
25
+ run(command, args, retries+1)
26
+ else
27
+ error "Authentication failure"
28
+ end
29
+ rescue RestClient::ResourceNotFound => e
30
+ error extract_not_found(e.http_body)
31
+ rescue RestClient::RequestFailed => e
32
+ error extract_error(e.http_body) unless e.http_code == 402 || e.http_code == 102
33
+ rescue RestClient::RequestTimeout
34
+ error "API request timed out. Please try again, or contact support@pagodagrid.com if this issue persists."
35
+ rescue CommandFailed => e
36
+ error e.message
37
+ rescue Interrupt => e
38
+ error "\n[canceled]"
39
+ end
40
+ end
41
+
42
+ def run_internal(command, args)
43
+ klass, method = parse(command)
44
+ runner = klass.new(args)
45
+ raise InvalidCommand unless runner.respond_to?(method)
46
+ runner.send(method)
47
+ end
48
+
49
+ def parse(command)
50
+ parts = command.split(':')
51
+ case parts.size
52
+ when 1
53
+ begin
54
+ return eval("Pagoda::Command::#{command.capitalize}"), :index
55
+ rescue NameError, NoMethodError
56
+ return Pagoda::Command::App, command.to_sym
57
+ end
58
+ else
59
+ begin
60
+ const = Pagoda::Command
61
+ command = parts.pop
62
+ parts.each { |part| const = const.const_get(part.capitalize) }
63
+ return const, command.to_sym
64
+ rescue NameError
65
+ raise InvalidCommand
66
+ end
67
+ end
68
+ end
69
+
70
+ def extract_not_found(body)
71
+ body =~ /^[\w\s]+ not found$/ ? body : "Resource not found"
72
+ end
73
+
74
+ def extract_error(body)
75
+ msg = parse_error_xml(body) || parse_error_json(body) || 'Internal server error'
76
+ end
77
+
78
+ def parse_error_xml(body)
79
+ xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
80
+ msg = xml_errors.map { |a| a.text }.join(" / ")
81
+ return msg unless msg.empty?
82
+ rescue Exception
83
+ end
84
+
85
+ def parse_error_json(body)
86
+ json = JSON.parse(body.to_s)
87
+ json['error']
88
+ rescue JSON::ParserError
89
+ end
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,243 @@
1
+ module Pagoda::Command
2
+ class App < Base
3
+
4
+ def list
5
+ apps = client.app_list
6
+ if !apps.empty?
7
+ display
8
+ display "APPS"
9
+ display "//////////////////////////////////"
10
+ display
11
+ apps.each do |app|
12
+ display "- #{app[:name]}"
13
+ end
14
+ else
15
+ error ["looks like you haven't launched any apps", "type 'pagoda launch' to launch this project"]
16
+ end
17
+ display
18
+ end
19
+
20
+ def create
21
+ if app_name = app(true)
22
+ error ["This project is already launched and paired to #{app_name}.", "To unpair run 'pagoda unpair'"]
23
+ end
24
+
25
+ unless locate_app_root
26
+ error ["Unable to find git config in this directory or in any parent directory"]
27
+ end
28
+
29
+ unless clone_url = extract_git_clone_url
30
+ errors = []
31
+ errors << "It appears you are using git (fantastic)."
32
+ errors << "However we only support git repos hosted with github."
33
+ errors << "Please ensure your repo is hosted with github."
34
+ error errors
35
+ end
36
+
37
+ unless name = args.dup.shift
38
+ error "Please Specify an app name ie. 'pagoda launch awesomeapp'"
39
+ end
40
+
41
+ display
42
+ display "+> Registering #{name}"
43
+ app = client.app_create(name, clone_url)
44
+ display "+> Launching...", false
45
+ loop_transaction(name)
46
+ add_app(name, clone_url)
47
+ display "+> #{name} launched"
48
+
49
+ unless option_value(nil, "--latest")
50
+ Pagoda::Command.run_internal("app:deploy", args)
51
+ end
52
+
53
+ display "-----------------------------------------------"
54
+ display
55
+ display "LIVE URL : http://#{name}.pagodabox.com"
56
+ display "ADMIN PANEL : http://dashboard.pagodabox.com"
57
+ display
58
+ display "-----------------------------------------------"
59
+ display
60
+
61
+ end
62
+ alias :launch :create
63
+ alias :register :create
64
+
65
+ def destroy
66
+ display
67
+ if confirm ["Are you totally completely sure you want to delete #{app} forever and ever?", "THIS CANNOT BE UNDONE! (y/n)"]
68
+ display "+> Destroying #{app}"
69
+ client.app_destroy(app)
70
+ display "+> #{app} has been successfully destroyed. RIP #{app}."
71
+ remove_app(app)
72
+ end
73
+ display
74
+ end
75
+ alias :delete :destroy
76
+
77
+ def info
78
+ display
79
+ info = client.app_info(app)
80
+ display "INFO - #{info[:name]}"
81
+ display "//////////////////////////////////"
82
+ display "name : #{info[:name]}"
83
+ display "clone_url : #{info[:git_url]}"
84
+ display
85
+ display "owner"
86
+ display "username : #{info[:owner][:username]}", true, 2
87
+ display "email : #{info[:owner][:email]}", true, 2
88
+ display
89
+ display "collaborators"
90
+ if info[:collaborators].any?
91
+ info[:collaborators].each_with_index do |collaborator, index|
92
+ display "username : #{collaborator[:username]}", true, 2
93
+ display "email : #{collaborator[:email]}", true, 2
94
+ end
95
+ else
96
+ display "(none)", true, 2
97
+ end
98
+ display
99
+ end
100
+
101
+ def pair
102
+
103
+ if app_name = app(true)
104
+ error ["This project is paired to #{app_name}.", "To unpair run 'pagoda unpair'"]
105
+ end
106
+
107
+ unless locate_app_root
108
+ error ["Unable to find git config in this directory or in any parent directory"]
109
+ end
110
+
111
+ unless my_repo = extract_git_clone_url
112
+ errors = []
113
+ errors << "It appears you are using git (fantastic)."
114
+ errors << "However we only support git repos hosted with github."
115
+ errors << "Please ensure your repo is hosted with github."
116
+ error errors
117
+ end
118
+
119
+ display
120
+ display "+> Locating deployed app with matching git repo"
121
+
122
+ apps = client.app_list
123
+
124
+ matching_apps = []
125
+ apps.each do |a|
126
+ if a[:git_url] == my_repo
127
+ matching_apps.push a
128
+ end
129
+ end
130
+
131
+ if matching_apps.count > 1
132
+ if name = app(true) || args.dup.shift
133
+ assign_app = nil
134
+ matching_apps.each do |a|
135
+ assign_app = a if a[:name] == name
136
+ end
137
+ if assign_app
138
+ display "+> Pairing this repo to deployed app - #{assign_app[:name]}"
139
+ pair_with_remote(assign_app)
140
+ display "+> Repo is now paired to '#{assign_app[:name]}'"
141
+ display
142
+ else
143
+ error "#{name} is not found among your launched app list"
144
+ end
145
+ else
146
+ errors = []
147
+ errors << "Multiple matches found"
148
+ errors << ""
149
+ matching_apps.each do |match|
150
+ errors << "-> #{match[:name]}"
151
+ end
152
+ errors << ""
153
+ errors << "You have more then one app that uses this repo."
154
+ errors << "Please specify which app you would like to pair to."
155
+ errors << ""
156
+ errors << "ex: pagoda pair #{matching_apps[0][:name]}"
157
+ error errors
158
+ end
159
+ elsif matching_apps.count == 1
160
+ match = matching_apps.first
161
+ display "+> Pairing this repo to deployed app - #{match[:name]}"
162
+ pair_with_remote match
163
+ display "+> Repo is now paired to '#{match[:name]}'"
164
+ display
165
+ else
166
+ error "Current git repo doesn't match any launched app repos"
167
+ end
168
+ end
169
+
170
+ def unpair
171
+ app
172
+ display
173
+ display "+> Unpairing this repo"
174
+ remove_app(app)
175
+ display "+> Free at last!"
176
+ display
177
+ end
178
+
179
+ def deploy
180
+ app
181
+ display
182
+ branch = parse_branch
183
+ commit = parse_commit
184
+ if option_value(nil, "--latest")
185
+ client.deploy_latest(app)
186
+ display "+> deploying to latest commit point on github...", false
187
+ loop_transaction
188
+ display "+> deployed"
189
+ display
190
+ else
191
+ client.deploy(app, branch, commit)
192
+ display "+> deploying to match current branch and commit...", false
193
+ loop_transaction
194
+ display "+> deployed"
195
+ display
196
+ end
197
+ end
198
+
199
+ def rewind
200
+ app
201
+ display
202
+ transaction = client.rewind(app)
203
+ display "+> undo...", false
204
+ loop_transaction
205
+ display "+> done"
206
+ display
207
+ end
208
+ alias :rollback :rewind
209
+ alias :undo :rewind
210
+
211
+ def fast_forward
212
+ app
213
+ display
214
+ transaction = client.fast_forward(app)
215
+ display "+> redo...", false
216
+ loop_transaction
217
+ display "+> done"
218
+ display
219
+ end
220
+ alias :fastforward :fast_forward
221
+ alias :forward :fast_forward
222
+ alias :redo :fast_forward
223
+
224
+ protected
225
+
226
+ def pair_with_remote(app)
227
+ my_app_list = read_apps
228
+ current_root = locate_app_root
229
+ in_list = false
230
+ my_app_list.each do |app_str|
231
+ app_arr = app_str.split(" ")
232
+ if app[:git_url] == app_arr[1] && app[:name] == app_arr[0] || app_arr[2] == current_root
233
+ in_list = true
234
+ end
235
+ end
236
+ unless in_list
237
+ add_app app[:name]
238
+ end
239
+ end
240
+
241
+
242
+ end
243
+ end