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.
@@ -0,0 +1,130 @@
1
+ require "socket"
2
+ require 'openssl'
3
+
4
+ module Pagoda
5
+ class TunnelProxy
6
+ include Pagoda::Helpers
7
+
8
+ def initialize(type, user, pass, app, instance)
9
+ @type = type
10
+ @user = user
11
+ @pass = pass
12
+ @app = app
13
+ @instance = instance
14
+ end
15
+
16
+ def start
17
+
18
+ [:INT, :TERM].each do |sig|
19
+ Signal.trap(sig) do
20
+ display "Tunnel Closed."
21
+ display "-----------------------------------------------"
22
+ display
23
+ exit
24
+ end
25
+ end
26
+
27
+ local_port = 3307
28
+ remote_host = "www.pagodabox.com"
29
+ remote_port = 3306
30
+
31
+ max_threads = 20
32
+ threads = []
33
+
34
+ chunk = 4096
35
+
36
+ #puts "start TCP server"
37
+ display "+> Opening Tunnel"
38
+ bound = false
39
+ until bound
40
+ begin
41
+ proxy_server = TCPServer.new('0.0.0.0', local_port)
42
+ bound = true
43
+ rescue Errno::EADDRINUSE
44
+ local_port += 1
45
+ end
46
+ end
47
+
48
+ display
49
+ display "Tunnel Established! Accepting connections on :"
50
+ display "-----------------------------------------------"
51
+ display
52
+ display "HOST : 127.0.0.1 (or localhost)", true, 2
53
+ display "PORT : #{local_port}", true, 2
54
+ display "USER : (found in pagodabox dashboard)", true, 2
55
+ display "PASS : (found in pagodabox dashboard)", true, 2
56
+ display
57
+ display "-----------------------------------------------"
58
+ display "(note : ctrl-c To close this tunnel)"
59
+
60
+ loop do
61
+
62
+ #puts "start a new thread for every client connection"
63
+ threads << Thread.new(proxy_server.accept) do |client_socket|
64
+
65
+ begin
66
+ # puts "client connection"
67
+ begin
68
+ server_socket = TCPSocket.new(remote_host, remote_port)
69
+ ssl_context = OpenSSL::SSL::SSLContext.new()
70
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(server_socket, ssl_context)
71
+ ssl_socket.sync_close = true
72
+ ssl_socket.connect
73
+ rescue Errno::ECONNREFUSED
74
+ # puts "connection refused"
75
+ client_socket.close
76
+ raise
77
+ end
78
+
79
+ # puts "authenticate"
80
+ if ssl_socket.readpartial(chunk) == "auth"
81
+ # puts "authentication"
82
+ ssl_socket.write "auth=#{@user}:#{@pass}:#{@app}:#{@instance}"
83
+ if ssl_socket.readpartial(chunk) == "success"
84
+ # puts "successful connection"
85
+ else
86
+ # puts "failed connection"
87
+ end
88
+ else
89
+ # puts "danger will robbinson! abort!"
90
+ end
91
+
92
+ loop do
93
+ # puts "wait for data on either socket"
94
+ (ready_sockets, dummy, dummy) = IO.select([client_socket, ssl_socket])
95
+
96
+ # puts "full duplex connection until data stream ends"
97
+ begin
98
+ ready_sockets.each do |socket|
99
+ data = socket.readpartial(chunk)
100
+ if socket == client_socket
101
+ #puts "read from client and write to server"
102
+ ssl_socket.write data
103
+ ssl_socket.flush
104
+ else
105
+ #puts "read from server and write to client."
106
+ client_socket.write data
107
+ client_socket.flush
108
+ end
109
+ end
110
+ rescue EOFError
111
+ break
112
+ end
113
+ end
114
+
115
+ rescue StandardError => error
116
+ end
117
+ client_socket.close rescue StandardError
118
+ ssl_socket.close rescue StandardError
119
+ end
120
+
121
+ #puts "clean up the dead threads, and wait until we have available threads"
122
+ threads = threads.select { |thread| thread.alive? ? true : (thread.join; false) }
123
+ while threads.size >= max_threads
124
+ sleep 1
125
+ threads = threads.select { |thread| thread.alive? ? true : (thread.join; false) }
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,3 @@
1
+ module Pagoda
2
+ VERSION = "0.1.1"
3
+ end
data/lib/pagoda.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Pagoda
2
+
3
+ end
data/pagoda.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "pagoda/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "pagoda"
7
+ s.version = Pagoda::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["lyon hill"]
10
+ s.email = ["hal@pagodabox.com"]
11
+ s.homepage = "http://www.pagodabox.com/"
12
+ s.summary = %q{Terminal client for interacting with the pagodabox}
13
+ s.description = %q{Terminal client for interacting with the pagodabox. This client does not contain full api functionality, just functionality that will enhance the workflow experience.}
14
+
15
+ s.rubyforge_project = "pagoda"
16
+
17
+ s.add_development_dependency "rspec"
18
+ s.add_development_dependency "webmock"
19
+
20
+ s.add_dependency "crack"
21
+ s.add_dependency "iniparse"
22
+ s.add_dependency "json_pure"
23
+ s.add_dependency "rest-client"
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
28
+ s.require_paths = ["lib"]
29
+ end
data/spec/base.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'webmock/rspec'
2
+
3
+ require 'pagoda/command'
4
+ require 'pagoda/commands/base'
5
+
6
+ Dir["#{File.dirname(__FILE__)}/../lib/pagoda/commands/*"].each { |c| require c }
7
+
8
+ include WebMock::API
9
+
10
+ def stub_api_request(method, path)
11
+ stub_request(method, "http://www.pagodabox.com#{path}")
12
+ end
13
+
14
+ def prepare_command(klass)
15
+ command = klass.new(['--app', 'myapp'])
16
+ command.stub!(:args).and_return([])
17
+ command.stub!(:display)
18
+ command.stub!(:pagoda).and_return(mock('pagoda client', :host => 'pagoda.com'))
19
+ command.stub!(:extract_app).and_return('myapp')
20
+ command
21
+ end
@@ -0,0 +1,255 @@
1
+ require "#{File.dirname(__FILE__)}/base"
2
+ require "#{File.dirname(__FILE__)}/../lib/pagoda/client"
3
+
4
+ describe Pagoda::Client do
5
+
6
+ before do
7
+ @client = Pagoda::Client.new(nil, nil)
8
+ end
9
+
10
+ describe "app" do
11
+
12
+ # errors come back like this:
13
+ # <?xml version="1.0" encoding="UTF-8"?>
14
+ # <errors>
15
+ # <error>Username can't be blank</error>
16
+ # <error>Email can't be blank</error>
17
+ # <error>Password can't be blank</error>
18
+ # </errors>
19
+
20
+ it "should display information" do
21
+ stub = %{
22
+ <?xml version='1.0' encoding='UTF-8'?>
23
+ <app>
24
+ <name>testapp</name>
25
+ <git-url>git@github.com:tylerflint/pagoda-pilot.git</git-url>
26
+ <owner>
27
+ <username>owner1</username>
28
+ <email>owner1@test.com</email>
29
+ </owner>
30
+ <collaborators type="array">
31
+ <collaborator>
32
+ <username>guy1</username>
33
+ <email>guy1@test.com</email>
34
+ </collaborator>
35
+ <collaborator>
36
+ <username>guy2</username>
37
+ <email>guy2@test.com</email>
38
+ </collaborator>
39
+ </collaborators>
40
+ <transactions type="array">
41
+ <transaction>
42
+ <id>1</id>
43
+ <name>app.init</name>
44
+ <description>Deploying app to the Pagoda grid</description>
45
+ <state>started</state>
46
+ <status></status>
47
+ </transaction>
48
+ </transactions>
49
+ </app>
50
+ }
51
+ stub_api_request(:get, "/apps/testapp.xml").to_return(:body => stub)
52
+ @client.app_info('testapp').should == {
53
+ :name => 'testapp',
54
+ :git_url => 'git@github.com:tylerflint/pagoda-pilot.git',
55
+ :owner => {
56
+ :username => 'owner1',
57
+ :email => 'owner1@test.com'
58
+ },
59
+ :collaborators => [
60
+ {:username => 'guy1', :email => 'guy1@test.com'},
61
+ {:username => 'guy2', :email => 'guy2@test.com'}
62
+ ],
63
+ :transactions => [
64
+ {:id => '1', :name => 'app.init', :description => 'Deploying app to the Pagoda grid', :state => 'started', :status => nil}
65
+ ]
66
+ }
67
+ end
68
+
69
+ it "lists incomplete transactions" do
70
+ stub = %{
71
+ <?xml version='1.0' encoding='UTF-8'?>
72
+ <transactions type="array">
73
+ <transaction>
74
+ <id>1</id>
75
+ <name>app.increment</name>
76
+ <description>spawn new instance of app</description>
77
+ <state>started</state>
78
+ <status></status>
79
+ </transaction>
80
+ <transaction>
81
+ <id>2</id>
82
+ <name>app.deploy</name>
83
+ <description>deploy code</description>
84
+ <state>ready</state>
85
+ <status></status>
86
+ </transaction>
87
+ </transactions>
88
+ }
89
+ stub_api_request(:get, "/apps/testapp/transactions.xml").to_return(:body => stub)
90
+ @client.transaction_list('testapp').should == [
91
+ {:id => '1', :name => 'app.increment', :description => 'spawn new instance of app', :state => 'started', :status => nil},
92
+ {:id => '2', :name => 'app.deploy', :description => 'deploy code', :state => 'ready', :status => nil}
93
+ ]
94
+ end
95
+
96
+ it "lists details of a transaction" do
97
+ stub = %{
98
+ <?xml version='1.0' encoding='UTF-8'?>
99
+ <transaction>
100
+ <id>1</id>
101
+ <name>app.increment</name>
102
+ <description>spawn new instance of app</description>
103
+ <state>started</state>
104
+ <status></status>
105
+ </transaction>
106
+ }
107
+ stub_api_request(:get, "/apps/testapp/transactions/123.xml").to_return(:body => stub)
108
+ @client.transaction_status('testapp', '123').should == {:id => '1', :name => 'app.increment', :description => 'spawn new instance of app', :state => 'started', :status => nil}
109
+ end
110
+
111
+ it "deploys" do
112
+ stub = %{
113
+ <?xml version='1.0' encoding='UTF-8'?>
114
+ <transaction>
115
+ <id>1</id>
116
+ <name>app.deploy</name>
117
+ <description>deploy new code</description>
118
+ <state>started</state>
119
+ <status></status>
120
+ </transaction>
121
+ }
122
+ stub_api_request(:put, "/apps/testapp/deploy.xml").to_return(:body => stub)
123
+ @client.deploy('testapp').should == {:id => '1', :name => 'app.deploy', :description => 'deploy new code', :state => 'started', :status => nil}
124
+ end
125
+
126
+ it "rewinds deploy list" do
127
+ stub = %{
128
+ <?xml version='1.0' encoding='UTF-8'?>
129
+ <transaction>
130
+ <id>1</id>
131
+ <name>app.traverse</name>
132
+ <description>traverse the code</description>
133
+ <state>started</state>
134
+ <status></status>
135
+ </transaction>
136
+ }
137
+ stub_api_request(:put, "/apps/testapp/rewind.xml").to_return(:body => stub)
138
+ @client.rewind('testapp', 1).should == {:id => '1', :name => 'app.traverse', :description => 'traverse the code', :state => 'started', :status => nil}
139
+ end
140
+
141
+ it "fast-forwards deploy list" do
142
+ stub = %{
143
+ <?xml version='1.0' encoding='UTF-8'?>
144
+ <transaction>
145
+ <id>1</id>
146
+ <name>app.traverse</name>
147
+ <description>traverse the code</description>
148
+ <state>started</state>
149
+ <status></status>
150
+ </transaction>
151
+ }
152
+ stub_api_request(:put, "/apps/testapp/fast-forward.xml").to_return(:body => stub)
153
+ @client.fast_forward('testapp', 1).should == {:id => '1', :name => 'app.traverse', :description => 'traverse the code', :state => 'started', :status => nil}
154
+ end
155
+
156
+ it "scales up" do
157
+ stub = %{
158
+ <?xml version='1.0' encoding='UTF-8'?>
159
+ <transaction>
160
+ <id>1</id>
161
+ <name>app.scaleup</name>
162
+ <description>scaling app</description>
163
+ <state>started</state>
164
+ <status></status>
165
+ </transaction>
166
+ }
167
+ stub_api_request(:put, "/apps/testapp/scale-up.xml").to_return(:body => stub)
168
+ @client.scale_up('testapp', 1).should == {:id => '1', :name => 'app.scaleup', :description => 'scaling app', :state => 'started', :status => nil}
169
+ end
170
+
171
+ it "scales down" do
172
+ stub = %{
173
+ <?xml version='1.0' encoding='UTF-8'?>
174
+ <transaction>
175
+ <id>1</id>
176
+ <name>app.scaledown</name>
177
+ <description>scaling app</description>
178
+ <state>started</state>
179
+ <status></status>
180
+ </transaction>
181
+ }
182
+ stub_api_request(:put, "/apps/testapp/scale-down.xml").to_return(:body => stub)
183
+ @client.scale_down('testapp', 1).should == {:id => '1', :name => 'app.scaledown', :description => 'scaling app', :state => 'started', :status => nil}
184
+ end
185
+
186
+ end
187
+
188
+ describe "user" do
189
+
190
+ it "should list all active apps" do
191
+ stub = %{
192
+ <?xml version='1.0' encoding='UTF-8'?>
193
+ <apps type="array">
194
+ <app>
195
+ <id>1</id>
196
+ <name>burt</name>
197
+ <git-url>git@github.com:tylerflint/pagoda-pilot.git</git-url>
198
+ </app>
199
+ <app>
200
+ <id>2</id>
201
+ <name>ernie</name>
202
+ <git-url>git@github.com:tylerflint/pagoda-pilot.git</git-url>
203
+ </app>
204
+ </apps>
205
+ }
206
+ stub_api_request(:get, "/apps.xml").to_return(:body => stub)
207
+ @client.app_list.should == [{:id => '1', :name => 'burt', :git_url => 'git@github.com:tylerflint/pagoda-pilot.git'}, {:id => '2', :name => 'ernie', :git_url => 'git@github.com:tylerflint/pagoda-pilot.git'}]
208
+ end
209
+
210
+ it "should create a new app" do
211
+ stub = %{
212
+ <?xml version='1.0' encoding='UTF-8'?>
213
+ <app>
214
+ <name>testapp</name>
215
+ <git-url>git@github.com:tylerflint/pagoda-pilot.git</git-url>
216
+ <owner>
217
+ <username>tylerflint</username>
218
+ <email>tylerflint@gmail.com</email>
219
+ </owner>
220
+ <collaborators>
221
+ </collaborators>
222
+ <transactions type="array">
223
+ <transaction>
224
+ <id>1</id>
225
+ <name>app.init</name>
226
+ <description>Deploying app to the Pagoda grid</description>
227
+ <state>started</state>
228
+ <status></status>
229
+ </transaction>
230
+ </transactions>
231
+ </app>
232
+ }
233
+ stub_api_request(:post, '/apps.xml').with(:body => "app[name]=testapp&app[git_url]=git%40github.com%3Atylerflint%2Fpagoda-pilot.git").to_return(:body => stub)
234
+ @client.app_create('testapp', 'git@github.com:tylerflint/pagoda-pilot.git').should == {
235
+ :name => 'testapp',
236
+ :git_url => 'git@github.com:tylerflint/pagoda-pilot.git',
237
+ :owner => {
238
+ :username => 'tylerflint',
239
+ :email => 'tylerflint@gmail.com'
240
+ },
241
+ :collaborators => [],
242
+ :transactions => [
243
+ {:id => '1', :name => 'app.init', :description => 'Deploying app to the Pagoda grid', :state => 'started', :status => nil}
244
+ ]
245
+ }
246
+ end
247
+
248
+ it "should destroy an active app" do
249
+ stub_api_request(:delete, '/apps/testapp.xml')
250
+ @client.app_destroy('testapp')
251
+ end
252
+
253
+ end
254
+
255
+ end
@@ -0,0 +1,26 @@
1
+
2
+ describe Pagoda::Command do
3
+
4
+ it "extracts error messages from response when available in XML" do
5
+ Pagoda::Command.extract_error('<errors><error>Invalid app name</error></errors>').should == ' ! Invalid app name'
6
+ end
7
+
8
+ it "shows Internal Server Error when the response doesn't contain a XML" do
9
+ Pagoda::Command.extract_error('<h1>HTTP 500</h1>').should == ' ! Internal server error'
10
+ end
11
+
12
+ it "handles a nil body in parse_error_xml" do
13
+ lambda { Pagoda::Command.parse_error_xml(nil) }.should_not raise_error
14
+ end
15
+
16
+ it "correctly resolves commands" do
17
+ class Pagoda::Command::Test; end
18
+ class Pagoda::Command::Test::Multiple; end
19
+
20
+ Pagoda::Command.parse("foo").should == [ Pagoda::Command::App, :foo ]
21
+ Pagoda::Command.parse("test").should == [ Pagoda::Command::Test, :index ]
22
+ Pagoda::Command.parse("test:foo").should == [ Pagoda::Command::Test, :foo ]
23
+ Pagoda::Command.parse("test:multiple:foo").should == [ Pagoda::Command::Test::Multiple, :foo ]
24
+ end
25
+
26
+ end
@@ -0,0 +1,57 @@
1
+ module Pagoda::Command
2
+ describe Auth do
3
+ before do
4
+ @cli = prepare_command(Auth)
5
+ @sandbox = "#{Dir.tmpdir}/cli_spec_#{Process.pid}"
6
+ File.open(@sandbox, "w") { |f| f.write "user\npass\n" }
7
+ @cli.stub!(:credentials_file).and_return(@sandbox)
8
+ @cli.stub!(:running_on_a_mac?).and_return(false)
9
+ end
10
+
11
+ after do
12
+ FileUtils.rm_rf(@sandbox)
13
+ end
14
+
15
+ it "reads credentials from the credentials file" do
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.user.should == 'user'
21
+ @cli.password.should == 'pass'
22
+ end
23
+
24
+ it "asks for credentials when the file doesn't exist" do
25
+ FileUtils.rm_rf(@sandbox)
26
+ @cli.should_receive(:ask_for_credentials).and_return([ 'u', 'p'])
27
+ @cli.should_receive(:save_credentials)
28
+ @cli.get_credentials.should == [ 'u', 'p' ]
29
+ end
30
+
31
+ it "writes the credentials to a file" do
32
+ @cli.stub!(:credentials).and_return(['one', 'two'])
33
+ @cli.should_receive(:set_credentials_permissions)
34
+ @cli.write_credentials
35
+ File.read(@sandbox).should == "one\ntwo\n"
36
+ end
37
+
38
+ it "sets ~/.pagoda/credentials to be readable only by the user" do
39
+ unless RUBY_PLATFORM =~ /mswin32|mingw32/
40
+ sandbox = "#{Dir.tmpdir}/cli_spec_#{Process.pid}"
41
+ FileUtils.rm_rf(sandbox)
42
+ FileUtils.mkdir_p(sandbox)
43
+ fname = "#{sandbox}/file"
44
+ system "touch #{fname}"
45
+ @cli.stub!(:credentials_file).and_return(fname)
46
+ @cli.set_credentials_permissions
47
+ File.stat(sandbox).mode.should == 040700
48
+ File.stat(fname).mode.should == 0100600
49
+ end
50
+ end
51
+
52
+ it "deletes the credentials file" do
53
+ FileUtils.should_receive(:rm_f).with(@cli.credentials_file)
54
+ @cli.delete_credentials
55
+ end
56
+ end
57
+ end