tbm 0.1.2 → 0.2.0.rc1

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