tbm 0.1.2 → 0.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/TBM/meta.rb ADDED
@@ -0,0 +1,6 @@
1
+ # The namespace for this application/gem.
2
+ module TBM
3
+ APP_NAME = "Tunnel Boring Machine"
4
+ VERSION = "0.2.0.rc1"
5
+ RELEASE_DATE = '2012-12-30'
6
+ end
data/lib/TBM/target.rb ADDED
@@ -0,0 +1,54 @@
1
+ module TBM
2
+
3
+ # A target defines a tunnel or set of tunnels that can be created by invoking one or more names assigned to the target.
4
+ #
5
+ # It has a name, a host, a user, and one or more tunnels well as an optional list of aliases.
6
+ class Target
7
+
8
+ attr_reader :name, :host, :username, :tunnels
9
+
10
+ def initialize( name, host, username )
11
+ @name = name
12
+ @host = host
13
+ @username = username
14
+ @tunnels = []
15
+ @aliases = []
16
+ end
17
+
18
+ # Adds a tunnel to the target.
19
+ #
20
+ # @param [Tunnel] tunnel the tunnel to add
21
+ def add_tunnel( tunnel )
22
+ @tunnels << tunnel
23
+ end
24
+
25
+ # Adds an alias to the list of recognized aliases supported by the target.
26
+ #
27
+ # @param [String] name the alias to add
28
+ def add_alias( name )
29
+ @aliases << name
30
+ end
31
+
32
+ def has_name?( name )
33
+ ( @name == name ) || ( @aliases.include? name )
34
+ end
35
+
36
+ def each_tunnel( &block )
37
+ @tunnels.each { |tunnel| yield tunnel }
38
+ end
39
+
40
+ def to_s
41
+ if @aliases.empty?
42
+ @name
43
+ else
44
+ "#{@name} (#{@aliases.join(', ')})"
45
+ end
46
+ end
47
+
48
+ def has_tunnels?
49
+ !@tunnels.empty?
50
+ end
51
+
52
+ end
53
+
54
+ end
data/lib/TBM/tunnel.rb ADDED
@@ -0,0 +1,26 @@
1
+ module TBM
2
+
3
+ # Represents a particular tunnel, possibly one of many for a given target.
4
+ class Tunnel
5
+ attr_reader :port
6
+
7
+ def initialize( port, options = {} )
8
+ @port = port
9
+ @options = options
10
+ end
11
+
12
+ def remote_port
13
+ @options[:remote_port] || port
14
+ end
15
+
16
+ def remote_host_addr
17
+ @options[:remote_host] || 'localhost'
18
+ end
19
+
20
+ def remote_host
21
+ @options[:remote_host]
22
+ end
23
+
24
+ end
25
+
26
+ end
data/lib/tbm.rb CHANGED
@@ -1,4 +1,7 @@
1
- require 'tunnel/meta'
2
- require 'tunnel/cli'
3
- require 'tunnel/config'
4
- require 'tunnel/target'
1
+ require 'TBM/meta'
2
+ require 'TBM/cli'
3
+ require 'TBM/config'
4
+ require 'TBM/config_parser'
5
+ require 'TBM/machine'
6
+ require 'TBM/target'
7
+ require 'TBM/tunnel'
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,165 @@
1
+ require 'tbm'
2
+
3
+ include TBM
4
+
5
+ describe CommandLineInterface do
6
+
7
+ let( :config ) { double( TBM::Config ) }
8
+ subject { CommandLineInterface.new config }
9
+
10
+ before do
11
+ TBM::ConfigParser.stub( :parse ) { config }
12
+ config.stub( :valid? ) { config_valid }
13
+ config.stub( :errors ) { config_errors }
14
+ stub_messages
15
+ end
16
+
17
+ context "without valid config" do
18
+ let(:config_valid) { false }
19
+ let(:config_errors) { ["Invalid Config"] }
20
+ it "should print config errors" do
21
+ CommandLineInterface.parse_and_run
22
+ @messages.should include_match(/Cannot parse config/)
23
+ @messages.should include_match(/Invalid Config/)
24
+ end
25
+ end
26
+
27
+ context "with no parameters" do
28
+ let(:config_valid) { true }
29
+
30
+ before do
31
+ ARGV.clear
32
+ config.stub(:each_target).and_yield( 'alpha' ).and_yield( 'beta' )
33
+ end
34
+
35
+ it "should print syntax and targets" do
36
+ CommandLineInterface.parse_and_run
37
+ @messages.should include_match( /SYNTAX/ )
38
+ @messages.should include_match( /alpha/ )
39
+ @messages.should include_match( /beta/ )
40
+ end
41
+ end
42
+
43
+ context "with a single parameter" do
44
+ let(:config_valid) { true }
45
+
46
+ before do
47
+ ARGV.clear.push( 'target-name' )
48
+ config.stub(:get_target).with('target-name') { target }
49
+ end
50
+
51
+ context "matching a config target" do
52
+ let(:target) { double Target }
53
+ let(:thost) { 'target-host.example.com' }
54
+ let(:tuser) { 'username' }
55
+ let(:machine) { double( Machine ) }
56
+
57
+ before do
58
+ target.stub(:host) { thost }
59
+ target.stub(:username) { tuser }
60
+ end
61
+
62
+ it "should start Tunnel Boring Machine" do
63
+ Machine.stub(:new) { machine }
64
+ machine.should_receive(:bore)
65
+ CommandLineInterface.parse_and_run
66
+ end
67
+ end
68
+
69
+ context "not matching a config target" do
70
+ let(:target) { nil }
71
+
72
+ before do
73
+ config.stub(:each_target).and_yield( 'another-target' )
74
+ end
75
+
76
+ it "should say 'Cannot find target'" do
77
+ CommandLineInterface.parse_and_run
78
+ @messages.should include_match( /Cannot find target/ )
79
+ end
80
+
81
+ it "should print target list" do
82
+ CommandLineInterface.parse_and_run
83
+ @messages.should include_match( /another-target/ )
84
+ end
85
+ end
86
+ end
87
+
88
+ context "with multiple parameters" do
89
+ let(:config_valid) { true }
90
+
91
+ before do
92
+ ARGV.clear.push( 'alpha', 'beta' )
93
+ config.stub(:get_target).with('alpha') { alpha }
94
+ config.stub(:get_target).with('beta') { beta }
95
+ end
96
+
97
+ context "matching configured targets with same host and user" do
98
+ let(:alpha) { Target.new( 'alpha', 'host', 'username' ) }
99
+ let(:beta) { Target.new( 'beta', 'host', 'username' ) }
100
+ let(:machine) { double( Machine ) }
101
+
102
+ it "should start boring machine" do
103
+ Machine.stub(:new) { machine }
104
+ machine.should_receive(:bore)
105
+ CommandLineInterface.parse_and_run
106
+ end
107
+ end
108
+
109
+ context "matching configured targets with different hosts" do
110
+ let(:alpha) { double Target }
111
+ let(:beta) { double Target }
112
+
113
+ before do
114
+ alpha.stub(:host) { 'host1' }
115
+ alpha.stub(:username) { 'username' }
116
+ beta.stub(:host) { 'host2' }
117
+ beta.stub(:username) { 'username' }
118
+ end
119
+
120
+ it "should say 'Can't combine targets'" do
121
+ CommandLineInterface.parse_and_run
122
+ @messages.should include_match(/Can't combine targets/)
123
+ end
124
+ end
125
+
126
+ context "matching configured targets with different usernames" do
127
+ let(:alpha) { double Target }
128
+ let(:beta) { double Target }
129
+
130
+ before do
131
+ alpha.stub(:host) { 'host' }
132
+ alpha.stub(:username) { 'username1' }
133
+ beta.stub(:host) { 'host' }
134
+ beta.stub(:username) { 'username2' }
135
+ end
136
+
137
+ it "should say 'Can't combine targets'" do
138
+ CommandLineInterface.parse_and_run
139
+ @messages.should include_match(/Can't combine targets/)
140
+ end
141
+ end
142
+
143
+ context "not all matching configured targets" do
144
+ let(:alpha) { nil }
145
+ let(:beta) { nil }
146
+
147
+ before do
148
+ config.stub(:each_target).and_yield( 'gamma' ).and_yield( 'delta' )
149
+ end
150
+
151
+ it "should say 'Cannot find target'" do
152
+ CommandLineInterface.parse_and_run
153
+ @messages.should include_match(/Cannot find target/)
154
+ end
155
+
156
+ it "should print target list" do
157
+ CommandLineInterface.parse_and_run
158
+ @messages.should include_match( /gamma/ )
159
+ @messages.should include_match( /delta/ )
160
+ end
161
+ end
162
+ end
163
+
164
+
165
+ end
@@ -0,0 +1,348 @@
1
+ require 'TBM/config_parser'
2
+ require 'TBM/config'
3
+ require 'TBM/target'
4
+ require 'rspec'
5
+
6
+ include TBM
7
+
8
+ describe ConfigParser do
9
+ let( :path ) { File.expand_path( "~/.tbm" ) }
10
+ subject { ConfigParser.parse }
11
+
12
+ context "w/o config file" do
13
+ before do
14
+ stub_messages
15
+ File.stub( :file? ).with( path ) { false }
16
+ end
17
+
18
+ it { should_not be_valid }
19
+ specify { subject.errors.should include_match /No configuration file found/ }
20
+ end
21
+
22
+ context "w/ config file" do
23
+ before do
24
+ File.stub( :file? ).with( path ) { true }
25
+ YAML.should_receive( :load_file ).with( path ) { config }
26
+ end
27
+
28
+ context "containing no config" do
29
+ let(:config) { Hash.new }
30
+ it { should_not be_valid }
31
+ specify { subject.errors.should include_match(/No gateways/) }
32
+ end
33
+
34
+ context "containing an array" do
35
+ let(:config) { Array.new }
36
+ it { should_not be_valid }
37
+ specify { subject.errors.should include("Cannot parse TBM configuration of type: Array") }
38
+ end
39
+
40
+ context "containing a Hash" do
41
+ let(:config) { { gateway => targets } }
42
+
43
+ context "keyed by a gateway string" do
44
+ let(:gateway) { "gateway" }
45
+ let(:targets) { { 'web' => 80 } }
46
+ it { should be_valid }
47
+
48
+ before do
49
+ Etc.stub(:getlogin) { 'local-username' }
50
+ end
51
+
52
+ it "should contain a host with the specified gateway name" do
53
+ subject.get_target('web').host.should eql('gateway')
54
+ end
55
+
56
+ it "should contain a host with the local username" do
57
+ subject.get_target('web').username.should eql('local-username')
58
+ end
59
+ end
60
+
61
+ context "keyed by a username@gateway string" do
62
+ let(:gateway) { "remote-username@gateway.example.com" }
63
+ let(:targets) { { 'web' => 80 } }
64
+ it { should be_valid }
65
+ it "should contain a host with the specified gateway name" do
66
+ subject.get_target('web').host.should eql('gateway.example.com')
67
+ end
68
+ it "should contain a host with the specified username" do
69
+ subject.get_target('web').username.should eql('remote-username')
70
+ end
71
+ end
72
+
73
+ context "keyed by a non-String" do
74
+ let(:gateway) { Array.new }
75
+ let(:targets) { Hash.new }
76
+ specify { subject.errors.should include( "Cannot parse gateway name: [] (Array)" ) }
77
+ end
78
+
79
+ context "with target hash of tunnels" do
80
+ let(:gateway) { 'user@host' }
81
+ let(:targets) { { 'target-name' => target } }
82
+
83
+ context "with nil tunnel" do
84
+ let(:target) { nil }
85
+ specify { subject.errors.should include_match(/No target config/) }
86
+ end
87
+
88
+ context "with target config of 8080" do
89
+ let(:target) { 8080 }
90
+ it "should forward port 8080" do
91
+ subject.should be_valid
92
+ subject.get_target('target-name').should have_tunnel( :port => 8080, :remote_port => 8080, :remote_host => nil )
93
+ end
94
+ end
95
+
96
+ context "with target config of 0" do
97
+ let(:target) { 0 }
98
+ specify { subject.errors.should include( "Invalid port number: 0" ) }
99
+ end
100
+
101
+ context "with target config of '8443'" do
102
+ let(:target) { "8443" }
103
+ it "should forward port 8443" do
104
+ subject.should be_valid
105
+ target = subject.get_target('target-name').should have_tunnel( :port => 8443, :remote_port => 8443, :remote_host => nil )
106
+ end
107
+ end
108
+
109
+ context "with target config of '77777'" do
110
+ let(:target) { "77777" }
111
+ specify { subject.errors.should include( "Invalid port number: 77777" ) }
112
+ end
113
+
114
+ context "with target config of '8080:80'" do
115
+ let(:target) { "8080:80" }
116
+ it "should map port 8080 to 80" do
117
+ subject.should be_valid
118
+ target = subject.get_target('target-name').should have_tunnel( :port => 8080, :remote_port => 80, :remote_host => nil )
119
+ end
120
+ end
121
+
122
+ context "with target config of '8080:prod:80'" do
123
+ let(:target) { "8080:prod:80" }
124
+ it "should map port 8080 to 80 on prod" do
125
+ subject.should be_valid
126
+ target = subject.get_target('target-name').should have_tunnel( :port => 8080, :remote_port => 80, :remote_host => 'prod' )
127
+ end
128
+ end
129
+
130
+ context "with target config of 'staging:8080'" do
131
+ let(:target) { "staging:8080" }
132
+ it "should forward port 8080 to staging" do
133
+ puts subject.errors
134
+ subject.should be_valid
135
+ target = subject.get_target('target-name').should have_tunnel( :port => 8080, :remote_port => 8080, :remote_host => 'staging' )
136
+ end
137
+ end
138
+
139
+ context "with target config of [8080,8443]" do
140
+ let(:target) { [8080,8443] }
141
+ it "should forward ports 8080 and 8443" do
142
+ puts subject.errors
143
+ subject.should be_valid
144
+ target = subject.get_target('target-name').should have_tunnels( [ { :port => 8080 }, { :port => 8443 } ] )
145
+ end
146
+ end
147
+
148
+ context "with target config of [3000,8080:80]" do
149
+ let(:target) { [3000,"8080:80"] }
150
+ it "should forward port 3000 and map 8080 to 80" do
151
+ puts subject.errors
152
+ subject.should be_valid
153
+ target = subject.get_target('target-name').should have_tunnels( [ { :port => 3000, :remote_port => 3000 }, { :port => 8080, :remote_port => 80 } ] )
154
+ end
155
+ end
156
+
157
+ context "with Hash target config" do
158
+ let(:target) { Hash.new }
159
+
160
+ context "containing nothing" do
161
+ specify { subject.errors.should include_match(/no tunnels/) }
162
+ end
163
+
164
+ it "should forward port from 'tunnel' key" do
165
+ target['tunnel'] = 3000
166
+ subject.get_target('target-name').should have_tunnel( :port => 3000 )
167
+ end
168
+
169
+ it "should add alias from 'alias' key" do
170
+ target['alias']='aka'
171
+ subject.get_target('target-name').should have_name('aka')
172
+ end
173
+
174
+ it "should treat any other key as a remote host" do
175
+ target['staging']=[1111,"2222:3333"]
176
+ target['prod']=3306
177
+ subject.get_target('target-name').should have_tunnels( [
178
+ { :port => 1111, :remote_port => 1111, :remote_host => 'staging' },
179
+ { :port => 2222, :remote_port => 3333, :remote_host => 'staging' },
180
+ { :port => 3306, :remote_port => 3306, :remote_host => 'prod' },
181
+ ])
182
+ end
183
+ end
184
+ end
185
+
186
+ context "with a non-Hash value" do
187
+ let(:gateway) { 'user@host' }
188
+ let(:targets) { "Targets" }
189
+ specify { subject.errors.should include( "Cannot parse targets, expected Hash, received: String" ) }
190
+ end
191
+
192
+ end
193
+
194
+ # target-details is a string (connection), array (array of connection) or hash (target-hash)
195
+ # connection is an integer or string, matching any of: <port> or <localport>:<remoteport> or <localport>:host:<remoteport>
196
+ # target-hash contains: alias, forward, remote-host
197
+ # alias is string or array of strings containing aliases for the tunnel name.
198
+ # forward contains a connection or array of connections
199
+ # remote-host maps to a remote connection or array of remote connections
200
+ # remote connection is an integer or a string in the following formats: <port>, <local-port>:<remote-port>
201
+
202
+ # context "and no forward" do
203
+ # it { should_not be_valid }
204
+ # specify { subject.errors.should include_match(/no forward/) }
205
+ # end
206
+
207
+ # context "and a forward" do
208
+ # let(:target) { { 'host' => 'host', 'username' => 'username', 'forward' => forward } }
209
+ # let(:targetmock) { double(Tunnel::Target) }
210
+
211
+ # before do
212
+ # Tunnel::Target.stub(:new) { targetmock }
213
+ # targetmock.stub( :host ) { 'host' }
214
+ # end
215
+
216
+ # context "of 8080" do
217
+ # let(:forward) { 8080 }
218
+ # it "should forward port 8080 on localhost" do
219
+ # targetmock.should_receive( :forward_port ).with( 8080, nil )
220
+ # subject.should be_valid
221
+ # end
222
+ # end
223
+
224
+ # context "of [ 8000, 8443 ]" do
225
+ # let(:forward) { [8000,8443] }
226
+ # it "should forward ports 8080 and 8443" do
227
+ # targetmock.should_receive( :forward_port ).with( 8000, nil )
228
+ # targetmock.should_receive( :forward_port ).with( 8443, nil )
229
+ # subject.should be_valid
230
+ # end
231
+ # end
232
+
233
+ # context "of { alpha => 3000, beta => [ 8080, 8443 ] } ]" do
234
+ # let(:forward) { { 'alpha' => 3000, 'beta' => [ 8080, 8443 ] } }
235
+ # it "should forward port 3000 to alpha" do
236
+ # targetmock.should_receive( :forward_port ).with( 3000, 'alpha' )
237
+ # targetmock.stub( :forward_port ).with( anything(), 'beta' )
238
+ # subject.should be_valid
239
+ # end
240
+ # it "should forward ports 8080, 8443 to alpha" do
241
+ # targetmock.should_receive( :forward_port ).with( 8080, 'beta' )
242
+ # targetmock.should_receive( :forward_port ).with( 8443, 'beta' )
243
+ # targetmock.stub( :forward_port ).with( anything(), 'alpha' )
244
+ # subject.should be_valid
245
+ # end
246
+ # end
247
+
248
+ # context "of 8000.5" do
249
+ # let(:forward) { 8000.5 }
250
+ # it { should_not be_valid }
251
+ # specify { subject.errors.should include_match(/Not sure how to handle forward .*: Float/) }
252
+ # end
253
+
254
+ # context "of -8080" do
255
+ # let(:forward) { -8080 }
256
+ # it { should_not be_valid }
257
+ # specify { subject.errors.should include_match(/Invalid port/) }
258
+ # end
259
+
260
+ # context "of 'blueberry'" do
261
+ # let(:forward) { 'blueberry' }
262
+ # it { should_not be_valid }
263
+ # specify { subject.errors.should include_match(/Not sure how to handle forward .*: String/) }
264
+ # end
265
+
266
+ # context "of 'blueberry'" do
267
+ # let(:forward) { [ 80.80, -443, 'blueberry' ] }
268
+ # it { should_not be_valid }
269
+ # specify { subject.errors.should include_match(/Invalid port/) }
270
+ # end
271
+
272
+ # end
273
+
274
+ # context "and an alias" do
275
+ # let(:target) { { 'host' => 'host', 'username' => 'username', 'forward' => 8080 } }
276
+ # let(:targetmock) { double(Tunnel::Target) }
277
+
278
+ # before do
279
+ # Tunnel::Target.stub(:new) { targetmock }
280
+ # targetmock.stub(:forward_port)
281
+ # end
282
+
283
+ # it "should treat string as single alias" do
284
+ # target['alias']= 'mr-smith'
285
+ # targetmock.should_receive( :alias ).with( 'mr-smith' )
286
+ # subject.should be_valid
287
+ # end
288
+
289
+ # it "should treat an array as a series of aliases" do
290
+ # target['alias']= 'mr-smith'
291
+ # targetmock.should_receive( :alias ).with( 'mr-smith' )
292
+ # subject.should be_valid
293
+ # end
294
+
295
+ # it "should warn with any other content" do
296
+ # target['alias'] = Hash.new
297
+ # subject.should_not be_valid
298
+ # subject.errors.should include_match( /Cannot parse alias/ )
299
+ # end
300
+ # end
301
+ # end
302
+
303
+ # end
304
+
305
+ # context "containing a hash of five targets" do
306
+ # let(:config) { { 'alpha' => { 'host' => 'host', 'forward' => 3001 }, 'beta' => { 'host' => 'host', 'forward' => 3002 }, 'gamma' => { 'host' => 'host', 'forward' => 3003 }, 'delta' => { 'host' => 'host', 'forward' => 3004 },
307
+ # 'omega' => { 'host' => 'host', 'forward' => 3005 } } }
308
+ # it "should return all five targets specified" do
309
+ # subject.should be_valid
310
+ # subject.get_target( 'alpha' ).should be_instance_of(Tunnel::Target)
311
+ # subject.get_target( 'beta' ).should be_instance_of(Tunnel::Target)
312
+ # subject.get_target( 'gamma' ).should be_instance_of(Tunnel::Target)
313
+ # subject.get_target( 'delta' ).should be_instance_of(Tunnel::Target)
314
+ # subject.get_target( 'omega' ).should be_instance_of(Tunnel::Target)
315
+ # end
316
+ # end
317
+
318
+ end
319
+ end
320
+
321
+ RSpec::Matchers.define :have_tunnel do |expected|
322
+ match do |actual|
323
+ actual.should_not be_nil
324
+ actual.tunnels.size.should eql(1)
325
+ tunnel = actual.tunnels[0]
326
+ expected.each do |k,v|
327
+ tunnel.send(k).should eql(v)
328
+ end
329
+ end
330
+ description do
331
+ properties = expected.map{ |k,v| "#{k}=#{v}" }.join( ', ' )
332
+ "have a tunnel with #{properties}"
333
+ end
334
+ end
335
+
336
+ RSpec::Matchers.define :have_tunnels do |expected|
337
+ match do |actual|
338
+ actual.should_not be_nil
339
+ actual.tunnels.size.should eql(expected.size)
340
+ (0...expected.size).each do |index|
341
+ tunnel = actual.tunnels[index]
342
+ properties = expected[index]
343
+ properties.each do |k,v|
344
+ tunnel.send(k).should eql(v)
345
+ end
346
+ end
347
+ end
348
+ end