tunnel-cf-plugin 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "rake"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
@@ -0,0 +1,17 @@
1
+ redis:
2
+ redis-cli: -h ${host} -p ${port} -a ${password}
3
+
4
+ mysql:
5
+ mysql: --protocol=TCP --host=${host} --port=${port} --user=${user} --password=${password} ${name}
6
+ mysqldump: --protocol=TCP --host=${host} --port=${port} --user=${user} --password=${password} ${name} > ${ask Output file}
7
+
8
+ mongodb:
9
+ mongo: --host ${host} --port ${port} -u ${user} -p ${password} ${name}
10
+ mongodump: --host ${host} --port ${port} -u ${user} -p ${password} --db ${name}
11
+ mongorestore: --host ${host} --port ${port} -u ${user} -p ${password} --db ${name} ${ask Directory or filename to restore from}
12
+
13
+ postgresql:
14
+ psql:
15
+ command: -h ${host} -p ${port} -d ${name} -U ${user} -w
16
+ environment:
17
+ - PGPASSWORD='${password}'
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'rack', '~> 1.2.0'
4
+ gem 'caldecott', '= 0.0.3'
5
+ gem 'bundler'
6
+ gem 'em-websocket'
7
+ gem 'async_sinatra'
8
+ gem 'thin'
9
+ gem 'json'
10
+ gem 'uuidtools'
@@ -0,0 +1,48 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.2.6)
5
+ async_sinatra (0.5.0)
6
+ rack (>= 1.2.1)
7
+ sinatra (>= 1.0)
8
+ caldecott (0.0.3)
9
+ addressable (= 2.2.6)
10
+ async_sinatra (= 0.5.0)
11
+ em-http-request (= 0.3.0)
12
+ em-websocket (= 0.3.1)
13
+ json (= 1.6.1)
14
+ uuidtools (= 2.1.2)
15
+ daemons (1.1.8)
16
+ em-http-request (0.3.0)
17
+ addressable (>= 2.0.0)
18
+ escape_utils
19
+ eventmachine (>= 0.12.9)
20
+ em-websocket (0.3.1)
21
+ addressable (>= 2.1.1)
22
+ eventmachine (>= 0.12.9)
23
+ escape_utils (0.2.4)
24
+ eventmachine (0.12.10)
25
+ json (1.6.1)
26
+ rack (1.2.5)
27
+ sinatra (1.2.8)
28
+ rack (~> 1.1)
29
+ tilt (>= 1.2.2, < 2.0)
30
+ thin (1.4.1)
31
+ daemons (>= 1.0.9)
32
+ eventmachine (>= 0.12.6)
33
+ rack (>= 1.0.0)
34
+ tilt (1.3.3)
35
+ uuidtools (2.1.2)
36
+
37
+ PLATFORMS
38
+ ruby
39
+
40
+ DEPENDENCIES
41
+ async_sinatra
42
+ bundler
43
+ caldecott (= 0.0.3)
44
+ em-websocket
45
+ json
46
+ rack (~> 1.2.0)
47
+ thin
48
+ uuidtools
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright (c) 2009-2011 VMware, Inc.
3
+ $:.unshift(File.dirname(__FILE__) + '/lib')
4
+
5
+ require 'rubygems'
6
+ require 'bundler/setup'
7
+
8
+ require 'caldecott'
9
+ require 'sinatra'
10
+ require 'json'
11
+ require 'eventmachine'
12
+
13
+ port = ENV['CF_APP_PORT']
14
+ port ||= 8081
15
+
16
+ # add vcap specific stuff to Caldecott
17
+ class VcapHttpTunnel < Caldecott::Server::HttpTunnel
18
+ get '/info' do
19
+ { "version" => '0.0.4' }.to_json
20
+ end
21
+
22
+ def self.get_tunnels
23
+ super
24
+ end
25
+
26
+ get '/services' do
27
+ services_env = ENV['CF_SERVICES']
28
+ return "no services env" if services_env.nil? or services_env.empty?
29
+ services_env
30
+ end
31
+
32
+ get '/services/:service' do |service_name|
33
+ services_env = ENV['CF_SERVICES']
34
+ not_found if services_env.nil?
35
+
36
+ services = JSON.parse(services_env)
37
+ service = services.find { |s| s["name"] == service_name }
38
+ not_found if service.nil?
39
+ service["options"].to_json
40
+ end
41
+ end
42
+
43
+ VcapHttpTunnel.run!(:port => port, :auth_token => ENV["CALDECOTT_AUTH"])
@@ -0,0 +1,179 @@
1
+ require "cf/cli"
2
+ require "tunnel-cf-plugin/tunnel"
3
+
4
+ module CFTunnelPlugin
5
+ class Tunnel < CF::CLI
6
+ CLIENTS_FILE = "tunnel-clients.yml"
7
+ STOCK_CLIENTS = File.expand_path("../../../config/clients.yml", __FILE__)
8
+
9
+ desc "Create a local tunnel to a service."
10
+ group :services, :manage
11
+ input(:instance, :argument => :optional,
12
+ :from_given => find_by_name("service instance"),
13
+ :desc => "Service instance to tunnel to") { |instances|
14
+ ask("Which service instance?", :choices => instances,
15
+ :display => proc(&:name))
16
+ }
17
+ input(:client, :argument => :optional,
18
+ :desc => "Client to automatically launch") { |clients|
19
+ if clients.empty?
20
+ "none"
21
+ else
22
+ ask("Which client would you like to start?",
23
+ :choices => clients.keys.unshift("none"))
24
+ end
25
+ }
26
+ input(:port, :default => 10000, :desc => "Port to bind the tunnel to")
27
+ def tunnel
28
+ instances = client.service_instances
29
+ fail "No services available for tunneling." if instances.empty?
30
+
31
+ instance = input[:instance, instances.sort_by(&:name)]
32
+ vendor = v2? ? instance.service_plan.service.label : instance.vendor
33
+ clients = tunnel_clients[vendor] || {}
34
+ client_name = input[:client, clients]
35
+
36
+ tunnel = CFTunnel.new(client, instance)
37
+ port = tunnel.pick_port!(input[:port])
38
+
39
+ conn_info =
40
+ with_progress("Opening tunnel on port #{c(port, :name)}") do
41
+ tunnel.open!
42
+ end
43
+
44
+ if client_name == "none"
45
+ unless quiet?
46
+ line
47
+ display_tunnel_connection_info(conn_info)
48
+
49
+ line
50
+ line "Open another shell to run command-line clients or"
51
+ line "use a UI tool to connect using the displayed information."
52
+ line "Press Ctrl-C to exit..."
53
+ end
54
+
55
+ tunnel.wait_for_end
56
+ else
57
+ with_progress("Waiting for local tunnel to become available") do
58
+ tunnel.wait_for_start
59
+ end
60
+
61
+ unless start_local_prog(clients, client_name, conn_info, port)
62
+ fail "'#{client_name}' execution failed; is it in your $PATH?"
63
+ end
64
+ end
65
+ end
66
+
67
+ def tunnel_clients
68
+ return @tunnel_clients if @tunnel_clients
69
+ stock_config = YAML.load_file(STOCK_CLIENTS)
70
+ custom_config_file = File.expand_path("#{CF::CONFIG_DIR}/.cf/#{CLIENTS_FILE}")
71
+
72
+ if File.exists?(custom_config_file)
73
+ custom_config = YAML.load_file(custom_config_file)
74
+ @tunnel_clients = deep_merge(stock_config, custom_config)
75
+ else
76
+ @tunnel_clients = stock_config
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def display_tunnel_connection_info(info)
83
+ line "Service connection info:"
84
+
85
+ to_show = [nil, nil, nil] # reserved for user, pass, db name
86
+ info.keys.each do |k|
87
+ case k
88
+ when "host", "hostname", "port", "node_id"
89
+ # skip
90
+ when "user", "username"
91
+ # prefer "username" over "user"
92
+ to_show[0] = k unless to_show[0] == "username"
93
+ when "password"
94
+ to_show[1] = k
95
+ when "name"
96
+ to_show[2] = k
97
+ else
98
+ to_show << k
99
+ end
100
+ end
101
+ to_show.compact!
102
+
103
+ align_len = to_show.collect(&:size).max + 1
104
+
105
+ indented do
106
+ to_show.each do |k|
107
+ # TODO: modify the server services rest call to have explicit knowledge
108
+ # about the items to return. It should return all of them if
109
+ # the service is unknown so that we don't have to do this weird
110
+ # filtering.
111
+ line "#{k.ljust align_len}: #{b(info[k])}"
112
+ end
113
+ end
114
+
115
+ line
116
+ end
117
+
118
+ def start_local_prog(clients, command, info, port)
119
+ client = clients[File.basename(command)]
120
+
121
+ cmdline = "#{command} "
122
+
123
+ case client
124
+ when Hash
125
+ cmdline << resolve_symbols(client["command"], info, port)
126
+ client["environment"].each do |e|
127
+ if e =~ /([^=]+)=(["']?)([^"']*)\2/
128
+ ENV[$1] = resolve_symbols($3, info, port)
129
+ else
130
+ fail "Invalid environment variable: #{e}"
131
+ end
132
+ end
133
+ when String
134
+ cmdline << resolve_symbols(client, info, port)
135
+ else
136
+ raise "Unknown client info: #{client.inspect}."
137
+ end
138
+
139
+ if verbose?
140
+ line
141
+ line "Launching '#{cmdline}'"
142
+ end
143
+
144
+ system(cmdline)
145
+ end
146
+
147
+ def resolve_symbols(str, info, local_port)
148
+ str.gsub(/\$\{\s*([^\}]+)\s*\}/) do
149
+ sym = $1
150
+
151
+ case sym
152
+ when "host"
153
+ # TODO: determine proper host
154
+ "localhost"
155
+ when "port"
156
+ local_port
157
+ when "user", "username"
158
+ info["username"]
159
+ when /^ask (.+)/
160
+ ask($1)
161
+ else
162
+ info[sym] || raise("Unknown symbol in config: #{sym}")
163
+ end
164
+ end
165
+ end
166
+
167
+ def deep_merge(a, b)
168
+ merge = proc { |_, old, new|
169
+ if old.is_a?(Hash) && new.is_a?(Hash)
170
+ old.merge(new, &merge)
171
+ else
172
+ new
173
+ end
174
+ }
175
+
176
+ a.merge(b, &merge)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,308 @@
1
+ require "addressable/uri"
2
+ require "restclient"
3
+ require "uuidtools"
4
+
5
+ require "caldecott-client"
6
+
7
+
8
+ class CFTunnel
9
+ HELPER_NAME = "caldecott"
10
+ HELPER_APP = File.expand_path("../../../helper-app", __FILE__)
11
+
12
+ # bump this AND the version info reported by HELPER_APP/server.rb
13
+ # this is to keep the helper in sync with any updates here
14
+ HELPER_VERSION = "0.0.4"
15
+
16
+ def initialize(client, service, port = 10000)
17
+ @client = client
18
+ @service = service
19
+ @port = port
20
+ end
21
+
22
+ def open!
23
+ if helper
24
+ auth = helper_auth
25
+
26
+ unless helper_healthy?(auth)
27
+ delete_helper
28
+ auth = create_helper
29
+ end
30
+ else
31
+ auth = create_helper
32
+ end
33
+
34
+ bind_to_helper if @service && !helper_already_binds?
35
+
36
+ info = get_connection_info(auth)
37
+
38
+ start_tunnel(info, auth)
39
+
40
+ info
41
+ end
42
+
43
+ def wait_for_start
44
+ 10.times do |n|
45
+ begin
46
+ TCPSocket.open("localhost", @port).close
47
+ return true
48
+ rescue => e
49
+ sleep 1
50
+ end
51
+ end
52
+
53
+ raise "Could not connect to local tunnel."
54
+ end
55
+
56
+ def wait_for_end
57
+ if @local_tunnel_thread
58
+ @local_tunnel_thread.join
59
+ else
60
+ raise "Tunnel wasn't started!"
61
+ end
62
+ end
63
+
64
+ PORT_RANGE = 10
65
+ def pick_port!(port = @port)
66
+ original = port
67
+
68
+ PORT_RANGE.times do |n|
69
+ begin
70
+ TCPSocket.open("localhost", port)
71
+ port += 1
72
+ rescue
73
+ return @port = port
74
+ end
75
+ end
76
+
77
+ @port = grab_ephemeral_port
78
+ end
79
+
80
+ private
81
+
82
+ def helper
83
+ @helper ||= @client.app_by_name(HELPER_NAME)
84
+ end
85
+
86
+ def create_helper
87
+ auth = UUIDTools::UUID.random_create.to_s
88
+ push_helper(auth)
89
+ start_helper
90
+ auth
91
+ end
92
+
93
+ def helper_auth
94
+ helper.env["CALDECOTT_AUTH"]
95
+ end
96
+
97
+ def helper_healthy?(token)
98
+ return false unless helper.healthy?
99
+
100
+ begin
101
+ response = RestClient.get(
102
+ "#{helper_url}/info",
103
+ "Auth-Token" => token
104
+ )
105
+
106
+ info = JSON.parse(response)
107
+ if info["version"] == HELPER_VERSION
108
+ true
109
+ else
110
+ stop_helper
111
+ false
112
+ end
113
+ rescue RestClient::Exception
114
+ stop_helper
115
+ false
116
+ end
117
+ end
118
+
119
+ def helper_already_binds?
120
+ helper.binds? @service
121
+ end
122
+
123
+ def push_helper(token)
124
+ target_base = @client.target.sub(/^[^\.]+\./, "")
125
+
126
+ url = "#{random_helper_url}.#{target_base}"
127
+ is_v2 = @client.is_a?(CFoundry::V2::Client)
128
+
129
+ app = @client.app
130
+ app.name = HELPER_NAME
131
+ app.framework = @client.framework_by_name("sinatra")
132
+ app.runtime = @client.runtime_by_name("ruby19")
133
+ app.command = "bundle exec ruby server.rb -p $VCAP_APP_PORT"
134
+ app.total_instances = 1
135
+ app.memory = 64
136
+ app.env = { "CALDECOTT_AUTH" => token }
137
+
138
+ if is_v2
139
+ app.space = @client.current_space
140
+ else
141
+ app.services = [@service] if @service
142
+ app.url = url
143
+ end
144
+
145
+ app.create!
146
+
147
+ if is_v2
148
+ app.bind(@service) if @service
149
+ app.create_route(url)
150
+ end
151
+
152
+ begin
153
+ app.upload(HELPER_APP)
154
+ invalidate_tunnel_app_info
155
+ rescue
156
+ app.delete!
157
+ raise
158
+ end
159
+ end
160
+
161
+ def delete_helper
162
+ helper.delete!
163
+ invalidate_tunnel_app_info
164
+ end
165
+
166
+ def stop_helper
167
+ helper.stop!
168
+ invalidate_tunnel_app_info
169
+ end
170
+
171
+ TUNNEL_CHECK_LIMIT = 60
172
+ def start_helper
173
+ helper.start!
174
+
175
+ seconds = 0
176
+ until helper.healthy?
177
+ sleep 1
178
+ seconds += 1
179
+ if seconds == TUNNEL_CHECK_LIMIT
180
+ raise "Helper application failed to start."
181
+ end
182
+ end
183
+
184
+ invalidate_tunnel_app_info
185
+ end
186
+
187
+ def bind_to_helper
188
+ helper.bind(@service)
189
+ helper.restart!
190
+ end
191
+
192
+ def invalidate_tunnel_app_info
193
+ @helper_url = nil
194
+ @helper = nil
195
+ end
196
+
197
+ def helper_url
198
+ return @helper_url if @helper_url
199
+
200
+ tun_url = helper.url
201
+
202
+ ["https", "http"].each do |scheme|
203
+ url = "#{scheme}://#{tun_url}"
204
+ begin
205
+ RestClient.get(url)
206
+
207
+ # https failed
208
+ rescue Errno::ECONNREFUSED
209
+
210
+ # we expect a 404 since this request isn't auth'd
211
+ rescue RestClient::ResourceNotFound
212
+ return @helper_url = url
213
+ end
214
+ end
215
+
216
+ raise "Cannot determine URL for #{tun_url}"
217
+ end
218
+
219
+ def get_connection_info(token)
220
+ response = nil
221
+ 10.times do
222
+ begin
223
+ response =
224
+ RestClient.get(
225
+ helper_url + "/" + safe_path("services", @service.name),
226
+ "Auth-Token" => token)
227
+
228
+ break
229
+ rescue RestClient::Exception => e
230
+ sleep 1
231
+ end
232
+ end
233
+
234
+ unless response
235
+ raise "Remote tunnel helper is unaware of #{@service.name}!"
236
+ end
237
+
238
+ is_v2 = @client.is_a?(CFoundry::V2::Client)
239
+
240
+ info = JSON.parse(response)
241
+ case (is_v2 ? @service.service_plan.service.label : @service.vendor)
242
+ when "rabbitmq"
243
+ uri = Addressable::URI.parse info["url"]
244
+ info["hostname"] = uri.host
245
+ info["port"] = uri.port
246
+ info["vhost"] = uri.path[1..-1]
247
+ info["user"] = uri.user
248
+ info["password"] = uri.password
249
+ info.delete "url"
250
+
251
+ # we use "db" as the "name" for mongo
252
+ # existing "name" is junk
253
+ when "mongodb"
254
+ info["name"] = info["db"]
255
+ info.delete "db"
256
+
257
+ # our "name" is irrelevant for redis
258
+ when "redis"
259
+ info.delete "name"
260
+
261
+ when "filesystem"
262
+ raise "Tunneling is not supported for this type of service"
263
+ end
264
+
265
+ ["hostname", "port", "password"].each do |k|
266
+ raise "Could not determine #{k} for #{@service.name}" if info[k].nil?
267
+ end
268
+
269
+ info
270
+ end
271
+
272
+ def start_tunnel(conn_info, auth)
273
+ @local_tunnel_thread = Thread.new do
274
+ Caldecott::Client.start({
275
+ :local_port => @port,
276
+ :tun_url => helper_url,
277
+ :dst_host => conn_info["hostname"],
278
+ :dst_port => conn_info["port"],
279
+ :log_file => STDOUT,
280
+ :log_level => ENV["CF_TUNNEL_DEBUG"] || "ERROR",
281
+ :auth_token => auth,
282
+ :quiet => true
283
+ })
284
+ end
285
+
286
+ at_exit { @local_tunnel_thread.kill }
287
+ end
288
+
289
+ def random_helper_url
290
+ random = sprintf("%x", rand(1000000))
291
+ "caldecott-#{random}"
292
+ end
293
+
294
+ def safe_path(*segments)
295
+ segments.flatten.collect { |x|
296
+ URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
297
+ }.join("/")
298
+ end
299
+
300
+ def grab_ephemeral_port
301
+ socket = TCPServer.new("0.0.0.0", 0)
302
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
303
+ Socket.do_not_reverse_lookup = true
304
+ socket.addr[1]
305
+ ensure
306
+ socket.close
307
+ end
308
+ end
@@ -0,0 +1,3 @@
1
+ module CFTunnelPlugin
2
+ VERSION = "0.2.2".freeze
3
+ end
@@ -0,0 +1,31 @@
1
+ require "spec_helper"
2
+
3
+ describe CFTunnelPlugin::Tunnel do
4
+ describe "#tunnel_clients" do
5
+ context "when the user has a custom clients.yml in their cf directory" do
6
+ it "overrides the default client config with the user's customizations" do
7
+ stub_const('CF::CONFIG_DIR', "#{SPEC_ROOT}/fixtures/fake_home_dirs/with_custom_clients")
8
+
9
+ expect(subject.tunnel_clients["postgresql"]).to eq({
10
+ "psql" => {
11
+ "command"=>"-h ${host} -p ${port} -d ${name} -U ${user} -w",
12
+ "environment" => ["PGPASSWORD='dont_ask_password'"]
13
+ }
14
+ })
15
+ end
16
+ end
17
+
18
+ context "when the user does not have a custom clients.yml" do
19
+ it "returns the default client config" do
20
+ stub_const('CF::CONFIG_DIR', '.')
21
+
22
+ expect(subject.tunnel_clients["postgresql"]).to eq({
23
+ "psql" => {
24
+ "command"=>"-h ${host} -p ${port} -d ${name} -U ${user} -w",
25
+ "environment" => ["PGPASSWORD='${password}'"]
26
+ }
27
+ })
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ SPEC_ROOT = File.dirname(__FILE__).freeze
2
+
3
+ require "rspec"
4
+ require "cfoundry"
5
+ require "cfoundry/test_support"
6
+ require "cf"
7
+ require "cf/test_support"
8
+
9
+ require "#{SPEC_ROOT}/../lib/tunnel-cf-plugin/plugin"
10
+
11
+ RSpec.configure do |c|
12
+ c.include Fake::FakeMethods
13
+ c.include ::FakeHomeDir
14
+ end
metadata ADDED
@@ -0,0 +1,221 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tunnel-cf-plugin
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 2
10
+ version: 0.2.2
11
+ platform: ruby
12
+ authors:
13
+ - Alex Suraci
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2013-03-25 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: cfoundry
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 312055405
29
+ segments:
30
+ - 0
31
+ - 5
32
+ - 3
33
+ - rc
34
+ - 5
35
+ version: 0.5.3.rc5
36
+ - - <
37
+ - !ruby/object:Gem::Version
38
+ hash: 7
39
+ segments:
40
+ - 0
41
+ - 6
42
+ version: "0.6"
43
+ type: :runtime
44
+ version_requirements: *id001
45
+ - !ruby/object:Gem::Dependency
46
+ name: addressable
47
+ prerelease: false
48
+ requirement: &id002 !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ hash: 7
54
+ segments:
55
+ - 2
56
+ - 2
57
+ version: "2.2"
58
+ type: :runtime
59
+ version_requirements: *id002
60
+ - !ruby/object:Gem::Dependency
61
+ name: caldecott-client
62
+ prerelease: false
63
+ requirement: &id003 !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ hash: 27
69
+ segments:
70
+ - 0
71
+ - 0
72
+ - 2
73
+ version: 0.0.2
74
+ type: :runtime
75
+ version_requirements: *id003
76
+ - !ruby/object:Gem::Dependency
77
+ name: rest-client
78
+ prerelease: false
79
+ requirement: &id004 !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ~>
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 1
87
+ - 6
88
+ version: "1.6"
89
+ type: :runtime
90
+ version_requirements: *id004
91
+ - !ruby/object:Gem::Dependency
92
+ name: uuidtools
93
+ prerelease: false
94
+ requirement: &id005 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ~>
98
+ - !ruby/object:Gem::Version
99
+ hash: 1
100
+ segments:
101
+ - 2
102
+ - 1
103
+ version: "2.1"
104
+ type: :runtime
105
+ version_requirements: *id005
106
+ - !ruby/object:Gem::Dependency
107
+ name: rake
108
+ prerelease: false
109
+ requirement: &id006 !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ~>
113
+ - !ruby/object:Gem::Version
114
+ hash: 25
115
+ segments:
116
+ - 0
117
+ - 9
118
+ version: "0.9"
119
+ type: :development
120
+ version_requirements: *id006
121
+ - !ruby/object:Gem::Dependency
122
+ name: rspec
123
+ prerelease: false
124
+ requirement: &id007 !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ~>
128
+ - !ruby/object:Gem::Version
129
+ hash: 21
130
+ segments:
131
+ - 2
132
+ - 11
133
+ version: "2.11"
134
+ type: :development
135
+ version_requirements: *id007
136
+ - !ruby/object:Gem::Dependency
137
+ name: webmock
138
+ prerelease: false
139
+ requirement: &id008 !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ~>
143
+ - !ruby/object:Gem::Version
144
+ hash: 29
145
+ segments:
146
+ - 1
147
+ - 9
148
+ version: "1.9"
149
+ type: :development
150
+ version_requirements: *id008
151
+ - !ruby/object:Gem::Dependency
152
+ name: rr
153
+ prerelease: false
154
+ requirement: &id009 !ruby/object:Gem::Requirement
155
+ none: false
156
+ requirements:
157
+ - - ~>
158
+ - !ruby/object:Gem::Version
159
+ hash: 15
160
+ segments:
161
+ - 1
162
+ - 0
163
+ version: "1.0"
164
+ type: :development
165
+ version_requirements: *id009
166
+ description:
167
+ email:
168
+ - asuraci@vmware.com
169
+ executables: []
170
+
171
+ extensions: []
172
+
173
+ extra_rdoc_files: []
174
+
175
+ files:
176
+ - Rakefile
177
+ - lib/tunnel-cf-plugin/plugin.rb
178
+ - lib/tunnel-cf-plugin/tunnel.rb
179
+ - lib/tunnel-cf-plugin/version.rb
180
+ - helper-app/Gemfile
181
+ - helper-app/Gemfile.lock
182
+ - helper-app/server.rb
183
+ - config/clients.yml
184
+ - spec/plugin_spec.rb
185
+ - spec/spec_helper.rb
186
+ homepage: http://cloudfoundry.com/
187
+ licenses: []
188
+
189
+ post_install_message:
190
+ rdoc_options: []
191
+
192
+ require_paths:
193
+ - lib
194
+ required_ruby_version: !ruby/object:Gem::Requirement
195
+ none: false
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ hash: 3
200
+ segments:
201
+ - 0
202
+ version: "0"
203
+ required_rubygems_version: !ruby/object:Gem::Requirement
204
+ none: false
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ hash: 3
209
+ segments:
210
+ - 0
211
+ version: "0"
212
+ requirements: []
213
+
214
+ rubyforge_project: tunnel-cf-plugin
215
+ rubygems_version: 1.8.24
216
+ signing_key:
217
+ specification_version: 3
218
+ summary: External access to your services on Cloud Foundry via a Caldecott HTTP tunnel.
219
+ test_files:
220
+ - spec/plugin_spec.rb
221
+ - spec/spec_helper.rb