pagoda 0.1.0

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.
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