tunnel-cf-plugin 0.2.2

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