right_sharding 1.0.3

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/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Right Sharding
2
+
3
+ Right Sharding is a Rack Middleware that makes requests aware of RightScale shards.
4
+ See: https://wookiee.rightscale.com/display/rightscale/Sharding
5
+
6
+ It currently supports RightSite and Right API requests.
7
+
8
+ Maintained by the RightScale Cornsilk team.
9
+
10
+ ## Setup
11
+
12
+ Right Sharding uses rconf to install bundler.
13
+
14
+ Install latest rconf:
15
+ $ gem install rconf
16
+
17
+ Run rconf:
18
+ $ rconf
19
+
20
+ ## Testing
21
+
22
+ ### Unit Testing
23
+
24
+ Run rspec:
25
+ $ bundle exec spec spec/
@@ -0,0 +1,53 @@
1
+ basedir = File.dirname(__FILE__)
2
+
3
+ #Require the core suite of RightSharding classes and modules
4
+ require File.join(basedir, "right_sharding", "routing")
5
+ require File.join(basedir, "right_sharding", "base_proxy")
6
+ require File.join(basedir, "right_sharding", "current_account")
7
+ require File.join(basedir, "right_sharding", "instance_api_proxy")
8
+ require File.join(basedir, "right_sharding", "shard_handler")
9
+ require File.join(basedir, "right_sharding", "user_api_proxy")
10
+ require File.join(basedir, "right_sharding", 'extensions', 'core')
11
+
12
+
13
+ module RightSharding
14
+ def self.activate(config, options = {})
15
+ # Ensure that this super-important module attribute is set before any init code attempts to use it!
16
+ RightSharding::Routing.skip_shard_routing_callback = options.delete(:skip_shard_routing_callback)
17
+
18
+ # The library uses only the CurrentAccount middleware.
19
+ current_account_only = options.delete(:current_account_only)
20
+
21
+ # Add middleware to update the global_session account based on received headers/params.
22
+ # Must come after GlobalSession.
23
+ config.middleware.insert_after ::Rack::MethodOverride, ::Rack::CurrentAccount, options
24
+
25
+ unless current_account_only
26
+ # Add the middleware to intercept the requests and reroute users to the appropriate shards
27
+ # It must come after GlobalSession and CurrentAccount; GlobalSession will be used for the
28
+ # legacy path.
29
+ config.middleware.insert_after ::Rack::CurrentAccount, ::Rack::ShardRouting, options
30
+
31
+ # Add the middleware to proxy the instance facing api to the appropriate shards
32
+ config.middleware.insert_before ::Rack::ShardRouting, ::Rack::ShardInstanceApiProxy, options
33
+
34
+ # This will proxy the specific accounts passed in the options variable.
35
+ config.middleware.insert_after(::Rack::ShardInstanceApiProxy, ::Rack::ShardUserApiProxy, options)
36
+ end
37
+ rescue NoMethodError => e
38
+ raise NoMethodError, "GlobalSession must be used first."
39
+ end
40
+ end
41
+
42
+
43
+ # HACK: monkey patch to ensure no extra headers get added
44
+ # do not allow Rest-Client default_headers to add 'application/xml'
45
+ # See: https://github.com/rest-client/rest-client/blob/v1.6.1/lib/restclient/request.rb#L279
46
+ require 'rest_client'
47
+ module RestClient
48
+ class Request
49
+ def default_headers
50
+ {}
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,204 @@
1
+ #--
2
+ # Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale, Inc. and
11
+ # the licensee.
12
+ #++
13
+
14
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
15
+
16
+ module RightSharding
17
+ module Rack
18
+ module Middleware
19
+
20
+ class BaseProxy
21
+ REST_CLIENT_DEFAULT_TIMEOUT = 1000 # max time to read a response in seconds
22
+ REST_CLIENT_DEFAULT_OPEN_TIMEOUT = 1000 # max time to open a connection in seconds
23
+ class BaseProxyYield < StandardError; end
24
+
25
+ class BaseProxyException < StandardError
26
+ attr_reader :code, :headers, :body
27
+
28
+ def initialize(code, headers, body)
29
+ @env = nil # subclass needs to set this
30
+ @code = code
31
+ @headers = headers
32
+ @body = body
33
+
34
+ super(body)
35
+ end
36
+ end
37
+
38
+ #-----------------------------------------------------------------------
39
+ # Rack Middleware
40
+ #-----------------------------------------------------------------------
41
+
42
+ def initialize(app, params = {})
43
+ @app = app
44
+
45
+ @config_object = Module.const_get(params[:config_object])
46
+ @shard_object = Module.const_get(params[:shard_object])
47
+ @sharding_object = Module.const_get(params[:sharding_object])
48
+ @logger = Module.const_get(params[:logger])
49
+ end
50
+
51
+ # Subclass needs to implement
52
+ # def call(env)
53
+
54
+ #-----------------------------------------------------------------------
55
+ # Getters
56
+ #-----------------------------------------------------------------------
57
+
58
+ def get_this_shard_id
59
+ RightSharding::ShardHandler.fetch_this_shard_id(@config_object)
60
+ end
61
+
62
+ def get_shard_from_shard_id(shard_id)
63
+ RightSharding::ShardHandler.fetch_shard(@shard_object, shard_id)
64
+ end
65
+
66
+ #-----------------------------------------------------------------------
67
+ # General Helper
68
+ #-----------------------------------------------------------------------
69
+
70
+ def proxy_request_to_the_destinated_shard(hostname, options={})
71
+ request = get_request
72
+
73
+ rest_options = {}
74
+ rest_options[:url] = "#{request.protocol}#{hostname}:#{request.port}#{request.request_uri}"
75
+ rest_options[:method] = request.method
76
+ rest_options[:headers] = request.get_headers_for_proxy
77
+ rest_options[:payload] = request.get_payload_for_proxy if request.get_payload_for_proxy
78
+ rest_options[:cookies] = request.get_cookies_for_proxy if request.get_cookies_for_proxy
79
+ rest_options[:timeout] = REST_CLIENT_DEFAULT_TIMEOUT
80
+ rest_options[:open_timeout] = REST_CLIENT_DEFAULT_OPEN_TIMEOUT
81
+
82
+ shard_proxy_logger(:info, "#{self.class}: Proxying the request to '#{hostname}' with url: #{rest_options[:url]}")
83
+
84
+ response = RestClient::Request.execute(rest_options)
85
+ if options[:proxy_global_session]
86
+ set_global_session_from_response(response)
87
+ end
88
+
89
+ [response.code, get_rack_format_response_headers(response), response.to_s]
90
+ rescue URI::InvalidURIError, SocketError, Errno::ECONNREFUSED, GlobalSession::ClientError => e
91
+ shard_proxy_logger(:error, "#{self.class}: failed: #{e.class} proxy request to url: #{rest_options[:url]}\n" + e.message + "\n " + e.backtrace.join("\n "))
92
+ [503, {}, "Unable to proxy the request to the destination '#{hostname}'. #{e.message}"]
93
+ rescue RestClient::Exception => e
94
+ shard_proxy_logger(:error, "#{self.class}: failed: #{e.class} proxy request to url: #{rest_options[:url]}\n" + e.message + "\n " + e.backtrace.join("\n "))
95
+ [e.http_code, get_rack_format_response_headers(e.response), e.response.to_s]
96
+ end
97
+
98
+ #-----------------------------------------------------------------------
99
+ # Request Helper
100
+ #-----------------------------------------------------------------------
101
+
102
+ def get_request
103
+ request = ActionController::Request.new(@env)
104
+
105
+ override_request_ssl(request)
106
+
107
+ define_get_headers_for_proxy(request)
108
+ define_get_payload_for_proxy(request)
109
+ define_get_cookies_for_proxy(request)
110
+
111
+ request
112
+ end
113
+
114
+ def override_request_ssl(request)
115
+ if (@http_proto && @http_proto == "http") || (@env['HTTP_X_FORWARDED_PROTO'] && @env['HTTP_X_FORWARDED_PROTO'].split(',')[0] == "http")
116
+ def request.ssl?; false; end
117
+ else
118
+ def request.ssl?; true; end
119
+ end
120
+ end
121
+
122
+ def define_get_headers_for_proxy(request)
123
+ def request.get_headers_for_proxy
124
+ pheaders = {}
125
+
126
+ # preserve headers
127
+ headers.each {|key, value| pheaders[$1] = value if key =~ /^HTTP_(.*)/}
128
+
129
+ # Send the 'X_FORWARDED_FOR' that we received on this server so that IP Whitelisting can check against that
130
+ # Prevent spoofing by calcuating the HMAC using a shared secret (in this case, the library_auth_key because
131
+ # we didn't want to add a new shared secret for this temporary proxy stuff)
132
+ forwarded_for_hmac = OpenSSL::HMAC.hexdigest('sha1', ::LIBRARY_AUTH_KEY, pheaders["X_FORWARDED_FOR"] || "")
133
+ pheaders["X_RS_PROXY_FORWARDED_FOR"] = "#{pheaders["X_FORWARDED_FOR"]};#{forwarded_for_hmac}"
134
+
135
+ # chain x_forwarded_for
136
+ pheaders["X_FORWARDED_FOR"] = [pheaders["X_FORWARDED_FOR"], self.ip].join(",") unless self.ip.blank?
137
+
138
+ pheaders
139
+ end
140
+ end
141
+
142
+ def define_get_payload_for_proxy(request)
143
+ def request.get_payload_for_proxy
144
+ raw_post.blank? ? nil : raw_post
145
+ end
146
+ end
147
+
148
+ def define_get_cookies_for_proxy(request)
149
+ def request.get_cookies_for_proxy
150
+ if headers["rack.request.cookie_hash"].blank?
151
+ nil
152
+ else
153
+ headers["rack.request.cookie_hash"].reject {|k, v| k !~ /rs_gbl|session_id/}
154
+ end
155
+ end
156
+ end
157
+
158
+ #-----------------------------------------------------------------------
159
+ # Response Helper
160
+ #-----------------------------------------------------------------------
161
+
162
+ def get_rack_format_response_headers(response)
163
+ response.raw_headers.inject({}) do |out, (key, value)|
164
+ out[key] = %w{ set-cookie }.include?(key.downcase) ? value : value.first
165
+ out
166
+ end
167
+ end
168
+
169
+ def set_global_session_from_response(response)
170
+ # Get the response's global session cookie if it exists
171
+ cookie_name = @env['global_session'].directory.configuration['cookie']['name']
172
+ cookie = CGI.unescape(response.cookies[cookie_name]) if response.cookies[cookie_name] # unescape expects a string
173
+ @env['global_session'] = @env['global_session'].directory.create_session(cookie)
174
+ end
175
+
176
+ #-----------------------------------------------------------------------
177
+ # URI Parsing Helper
178
+ #-----------------------------------------------------------------------
179
+
180
+ def get_api_version
181
+ request = ActionController::Request.new(@env)
182
+
183
+ if !request.parameters.blank? && !request.parameters[:api_version].blank?
184
+ api_version = request.parameters[:api_version]
185
+ elsif !@env["HTTP_X_API_VERSION"].blank?
186
+ api_version = @env["HTTP_X_API_VERSION"]
187
+ end
188
+
189
+ return (!api_version.blank? ? api_version.to_f : nil)
190
+ end
191
+
192
+ #-----------------------------------------------------------------------
193
+ # Logging Helper
194
+ #-----------------------------------------------------------------------
195
+
196
+ def shard_proxy_logger(level, message)
197
+ @logger.send(level, "#{message}")
198
+ end
199
+
200
+ end
201
+
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,68 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
2
+
3
+ module RightSharding
4
+ module Rack
5
+ module Middleware
6
+
7
+ class CurrentAccount
8
+ attr_reader :app
9
+ attr_reader :config_object, :logger
10
+ #-----------------------------------------------------------------------
11
+ # Rack Middleware
12
+ #-----------------------------------------------------------------------
13
+
14
+ def initialize(app, params = {})
15
+ @app = app
16
+
17
+ @config_object = Module.const_get(params[:config_object])
18
+ @logger = Module.const_get(params[:logger])
19
+ end
20
+
21
+ # TODO Current this replaces the account id set in env["global_session"]["account"]
22
+ # ultimately, we want this middleware to simply determine the correct account and set it
23
+ # in an env variable such as request.env['rightscale.current_account']. Once we refactor all
24
+ # the apps to use that instead, we can remove the use of global_session['account']
25
+ def call(env)
26
+ reset_global_session_account(env)
27
+ return @app.call(env)
28
+ end
29
+
30
+ # Passing a X_ACCOUNT header or account query parameter can override
31
+ # the account provided in the global_session cookie. The X_ACCOUNT header
32
+ # or account query parameter must be the id of the account, and not the href.
33
+ #
34
+ # Order of precedence:
35
+ # "X_ACCOUNT" header > "account" query parameter > global_session
36
+ #
37
+ def reset_global_session_account(env)
38
+ if env['HTTP_X_ACCOUNT']
39
+ account_id = env['HTTP_X_ACCOUNT']
40
+ from = 'header'
41
+ elsif env['QUERY_STRING']
42
+ query_params = ::Rack::Utils.parse_query(env['QUERY_STRING'])
43
+ if query_params['account']
44
+ account_id = query_params['account']
45
+ from = 'query string'
46
+ end
47
+ end
48
+
49
+ # Only reset the account_id if it exists, is numeric, and is different from what's
50
+ # currently in the global session
51
+ if account_id && account_id =~ /\A\d+\Z/ && account_id.to_i != env["global_session"]["account"]
52
+ logger.info("RightSharding::CurrentAccount: Resetting global_session account: " +
53
+ "#{env["global_session"]["account"]} -> #{account_id} (#{from})")
54
+ env["global_session"]["account"] = account_id.to_i
55
+ end
56
+ true
57
+ end
58
+
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ module Rack
65
+ unless defined?(::Rack::CurrentAccount)
66
+ CurrentAccount = ::RightSharding::Rack::Middleware::CurrentAccount
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+
2
+ # Custom core patches for Ruby 1.8.x
3
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("1.9")
4
+
5
+ # Ruby 1.8.x doesn't provide an Array#sample method, so alias it to Array#choice
6
+ class Array
7
+ alias_method :sample, :choice
8
+ end
9
+
10
+ end
@@ -0,0 +1,121 @@
1
+ #--
2
+ # Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale, Inc. and
11
+ # the licensee.
12
+ #++
13
+
14
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
15
+
16
+ module RightSharding
17
+ module Rack
18
+ module Middleware
19
+
20
+ class InstanceApiProxy < BaseProxy
21
+
22
+ class InstanceApiProxyYield < BaseProxyYield; end
23
+
24
+ class InstanceApiProxyException < BaseProxyException; end
25
+
26
+ #-----------------------------------------------------------------------
27
+ # Rack Middleware
28
+ #-----------------------------------------------------------------------
29
+
30
+ def initialize(app, params = {})
31
+ super
32
+ @http_proto = params[:http_proto]
33
+ end
34
+
35
+ def call(env)
36
+ @env = env
37
+
38
+ if is_instance_api?(1.0)
39
+ unless token = get_instance_api_token_from_uri
40
+ raise InstanceApiProxyYield, "Unable to extract the token out: '#{@env["PATH_INFO"]}'"
41
+ end
42
+
43
+ unless shard_info = get_shard_info_from_database(token)
44
+ shard_info = get_shard_info_from_right_net(token)
45
+ end
46
+
47
+ unless shard_info.blank?
48
+ if shard_info[:shard_id] != get_this_shard_id
49
+ # the account is migrated
50
+ return proxy_request_to_the_destinated_shard(shard_info[:shard_hostname])
51
+ end
52
+ else
53
+ # we are here when the account/shard cannot be found or the account is disabled in every shard, we'll pass on and let the app to deal with it
54
+ raise InstanceApiProxyYield, "Unable to find the account/shard or the account is disabled: '#{@env["PATH_INFO"]}'"
55
+ end
56
+ end
57
+
58
+ # everything is good
59
+ return @app.call(env)
60
+ rescue InstanceApiProxyYield => e
61
+ shard_proxy_logger(:error, e.message)
62
+ return @app.call(env)
63
+ rescue InstanceApiProxyException => e
64
+ shard_proxy_logger(:error, "failed: #{e.class} " + e.message + "\n " + e.backtrace.join("\n "))
65
+ return [e.code, e.headers, e.body]
66
+ end
67
+
68
+ #-----------------------------------------------------------------------
69
+ # Getters
70
+ #-----------------------------------------------------------------------
71
+
72
+ def get_shard_info_from_database(token)
73
+ RightSharding::ShardHandler.fetch_instance_api_token_shard_id_and_hostname(token)
74
+ end
75
+
76
+ def get_shard_id_from_right_net(token)
77
+ RightNetData.get_instance(:auth_token => token)
78
+ rescue RightSupport::Net::NoResult => e
79
+ raise InstanceApiProxyException.new(503, {}, "Unable to retrieve the instance from the given token")
80
+ rescue RestClient::Exception => e
81
+ raise InstanceApiProxyException.new(e.response.code, get_rack_format_response_headers(e.response), e.response.to_s)
82
+ end
83
+
84
+ def get_shard_info_from_right_net(token)
85
+ if instance_attributes = get_shard_id_from_right_net(token)
86
+ if !instance_attributes[:shard_id].blank? && instance_attributes[:shard_id].to_i != 0
87
+ if shard = get_shard_from_shard_id(instance_attributes[:shard_id])
88
+ {:shard_id => shard.id, :shard_hostname => shard.hostname}
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ #-----------------------------------------------------------------------
95
+ # URI Parsing Helper
96
+ #-----------------------------------------------------------------------
97
+
98
+ def is_instance_api?(version)
99
+ @env["PATH_INFO"] =~ /(?:^|\/)api\/inst\/ec2_instances\// && get_api_version == version
100
+ end
101
+
102
+ def get_instance_api_token_from_uri
103
+ @env["PATH_INFO"][/(?:^|\/)api\/inst\/ec2_instances\/([^\/]+)(?:\/|$)/, 1]
104
+ end
105
+
106
+ # Overrides BaseProxy, default is to return 1.0 as the version if an API
107
+ # version is not detected. (required for v3 instance api requests)
108
+ def get_api_version
109
+ super || 1.0
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+ end
116
+ end
117
+
118
+ module Rack
119
+ ShardInstanceApiProxy = ::RightSharding::Rack::Middleware::InstanceApiProxy unless defined?(::Rack::ShardInstanceApiProxy)
120
+ end
121
+
@@ -0,0 +1,298 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
2
+
3
+ module RightSharding
4
+ # Rails routing functionality embedded into RightSharding. The module contains public utility
5
+ # methods; most of the actual routing and redirection logic is tucked away inside the routing
6
+ # middleware that lives under this namespace.
7
+ #
8
+ # @see RightSharding::Rack::Middleware::Routing
9
+ module Routing
10
+
11
+ # mattr_accessor is Rails feature so it is not gonna work when we run "rake spec".
12
+ # # mattr_accessor :skip_shard_routing_callback
13
+ #
14
+ def self.skip_shard_routing_callback
15
+ @@skip_shard_routing_callback ||= nil
16
+ end
17
+ def self.skip_shard_routing_callback=(skip_shard_routing_callback)
18
+ @@skip_shard_routing_callback = skip_shard_routing_callback
19
+ end
20
+
21
+ module_function
22
+
23
+ # Determine whether shard routing and account-prefixing should be skipped for a given path and request method.
24
+ # This delegates to a callback that should be provided by whoever initializes the Routing middleware.
25
+ #
26
+ # @see RightSharding::Rack::Middleware::Routing#initialize
27
+ #
28
+ # @return [Boolean] true if shard routing and account-prefixing should be skipped; false otherwise
29
+ def skip_shard_routing?(path, request_method)
30
+ skip_shard_routing_callback.present? && skip_shard_routing_callback.call(path, request_method)
31
+ end
32
+
33
+ # Extract the current-account ID from a dashboard (right_site or embedded library) path.
34
+ #
35
+ # @return [String] a numeric string, or nil if the path does not include account info
36
+ def fetch_current_account_id_from_path(path)
37
+ path[/(?:^|\/)acct\/(\d+)(?:\/|$)/, 1]
38
+ end
39
+
40
+ # Prepend "/acct/X" to a URL path. Does not check if the path already contains this prefix;
41
+ # be careful!
42
+ #
43
+ # @param [String] original_path the path-info of the original URI, e.g. "/dashboard;index"
44
+ # @param [Integer] account_id the account ID to prepend to the path
45
+ # @return [String] the original_path with an account-id prefix
46
+ def add_account_to_path(original_path, account_id)
47
+ case original_path
48
+ when /(?:^|\/)api(?:\/|$)/
49
+ original_path.sub(/(^|\/)api(\/|$)/, '\1' + "api/acct/#{account_id}" + '\2')
50
+ else
51
+ "/acct/#{account_id}" + (original_path =~ /^\// ? "" : "/") + original_path
52
+ end
53
+ end
54
+ end
55
+
56
+ module Rack
57
+ module Middleware
58
+
59
+ class Routing
60
+
61
+ attr_reader :app
62
+ attr_reader :config_object, :sharding_object, :logger
63
+
64
+ #-----------------------------------------------------------------------
65
+ # Rack Middleware
66
+ #-----------------------------------------------------------------------
67
+
68
+ def initialize(app, params = {})
69
+ @app = app
70
+
71
+ @config_object = Module.const_get(params[:config_object])
72
+ @sharding_object = Module.const_get(params[:sharding_object])
73
+ @logger = Module.const_get(params[:logger])
74
+
75
+ @http_proto = params[:http_proto]
76
+ @skip_shard_routing_callback = params[:skip_shard_routing_callback]
77
+ end
78
+
79
+ def call(env)
80
+ # On every request, we want to initialize/clear the local cache to keep track the Account and Shard map,
81
+ # so it can reduce the hit to any cache system. The cache exists throughout the request and only that request.
82
+ ShardHandler::CurrentRequestCache.init
83
+
84
+ return app.call(env) if skip_shard_routing?(env["PATH_INFO"], env["REQUEST_METHOD"])
85
+
86
+ shard_id = RightSharding::ShardHandler.fetch_this_shard_id(config_object)
87
+
88
+ # HTTP_X_FORWARDED_HOST header may contain multiple values, pick the first one
89
+ forward_host = (env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"]).split(',')[0]
90
+
91
+ # NOTE: we use HTTP_HOST instead of SERVER_NAME because:
92
+ # * for the configuration like WEBrick server, it returns both hostname and port. (development env)
93
+ # * for others, it only returns the hostname
94
+ uri_handler = new_uri_handler(get_http_proto(env),
95
+ forward_host,
96
+ env['PATH_INFO'],
97
+ env['QUERY_STRING'])
98
+
99
+ if (account_id = fetch_current_account_id_from_path(env["PATH_INFO"]))
100
+ requested_shard = ShardHandler.fetch_shard_id_and_hostname(sharding_object, account_id.to_i)
101
+
102
+ if requested_shard.blank? || requested_shard[:shard_id].blank?
103
+ # no shard info, pass the request through
104
+ shard_routing_logger(:error, "Could not locate shard info", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
105
+ return app.call(env)
106
+ elsif requested_shard[:shard_id] != shard_id
107
+ # the requested account is in a different shard => redirect
108
+
109
+ # reset the hostname to a new hostname and everything else remains the same
110
+ reset_request_uri(uri_handler, requested_shard[:shard_hostname], env["REQUEST_METHOD"], env['PATH_INFO'], env['QUERY_STRING'])
111
+
112
+ return reroute_to_shard(uri_handler)
113
+ else
114
+ # everything good => continue to the next in the chain
115
+ return app.call(env)
116
+ end
117
+ else
118
+ ## legacy or api route
119
+ # always default to the account_id from global_session if none given in the url
120
+ if (account_id = env["global_session"]["account"])
121
+ requested_shard = ShardHandler.fetch_shard_id_and_hostname(sharding_object, account_id.to_i)
122
+
123
+ path_info = env['PATH_INFO']
124
+ # add account to the legacy route
125
+ if path_info && !path_info.empty?
126
+ # we don't rewrite the route if it is api-1.5 request
127
+ if path_info !~ /(?:^|\/)api\//
128
+ shard_routing_logger(:info, "Missing account_id in URL", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
129
+ path_info = add_account_to_path(path_info, account_id)
130
+ else
131
+ api_version = get_api_version(env, uri_handler)
132
+
133
+ if api_version
134
+ # we don't rewrite the route if it is api-1.0 instance facing request
135
+ if api_version < 1.5 && path_info !~ /(?:^|\/)api\/inst\//
136
+ shard_routing_logger(:error, "Invalid API (<1.5) Request", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
137
+ path_info = add_account_to_path(path_info, account_id)
138
+ end
139
+ else
140
+ shard_routing_logger(:error, "Missing API version", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
141
+ end
142
+ end
143
+ end
144
+
145
+ # requested the account is in a different shard => redirect
146
+ if requested_shard.blank? || requested_shard[:shard_id].blank?
147
+ # no shard info, pass the request through
148
+ shard_routing_logger(:error, "Could not locate shard info", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
149
+ return app.call(env)
150
+ elsif requested_shard[:shard_id] != shard_id
151
+ # reset the hostname and the path (added /acct/:id) and everything else remains the same
152
+ reset_request_uri(uri_handler, requested_shard[:shard_hostname], env["REQUEST_METHOD"], path_info, env['QUERY_STRING'])
153
+
154
+ return reroute_to_shard(uri_handler)
155
+ else
156
+ if uri_handler.path != path_info
157
+ # rewrite request uri by adding account into the route
158
+ reset_request_uri(uri_handler,
159
+ env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"],
160
+ env["REQUEST_METHOD"], path_info,
161
+ env['QUERY_STRING'])
162
+
163
+ return reroute_to_shard(uri_handler)
164
+ else
165
+ return app.call(env)
166
+ end
167
+ end
168
+ else
169
+ # Invalid
170
+ shard_routing_logger(:error, "Invalid Request", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
171
+ return app.call(env)
172
+ end
173
+ end
174
+ ensure
175
+ ShardHandler::CurrentRequestCache.destroy
176
+ end
177
+
178
+ private ######################################################################
179
+ def get_http_proto(env)
180
+ if @http_proto
181
+ @http_proto
182
+ elsif env['HTTPS'] == 'on'
183
+ 'https'
184
+ elsif env['HTTP_X_FORWARDED_SSL'] == 'on'
185
+ 'https'
186
+ elsif env['HTTP_X_FORWARDED_PROTO']
187
+ env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
188
+ else
189
+ 'https'
190
+ end
191
+ end
192
+
193
+
194
+ #-----------------------------------------------------------------------
195
+ # URL Parser
196
+ #-----------------------------------------------------------------------
197
+
198
+ # Call through to the equivalent module method in RightSharding::Routing.
199
+ def fetch_current_account_id_from_path(path)
200
+ RightSharding::Routing.fetch_current_account_id_from_path(path)
201
+ end
202
+
203
+ # Call through to the equivalent module method in RightSharding::Routing.
204
+ def skip_shard_routing?(path, request_method)
205
+ RightSharding::Routing.skip_shard_routing?(path, request_method)
206
+ end
207
+
208
+ def get_api_version(env, uri_handler)
209
+ api_version = nil
210
+
211
+ if env["HTTP_X_API_VERSION"]
212
+ api_version = env["HTTP_X_API_VERSION"]
213
+ elsif uri_handler.query
214
+ query_hash = CGI.parse(uri_handler.query)
215
+
216
+ api_version_key = query_hash.keys.find {|query_key| query_key =~ /api_version/i}
217
+ api_version = api_version_key ? query_hash[api_version_key][0] : nil
218
+ end
219
+
220
+ return (!api_version.blank? ? api_version.to_f : nil)
221
+ end
222
+
223
+ #-----------------------------------------------------------------------
224
+ # URI Constructor
225
+ #-----------------------------------------------------------------------
226
+
227
+ def new_uri_handler(schema, hostname_with_port, path_info, query_string)
228
+ hostname, port = hostname_with_port.split(":")
229
+
230
+ # Ensure port is a float for URI::HTTP.build to not fail
231
+ port = port.to_i unless port.nil?
232
+
233
+ uri_handler = if schema == "http"
234
+ URI::HTTP.build(:host => hostname, :port => port)
235
+ else
236
+ URI::HTTPS.build(:host => hostname, :port => port)
237
+ end
238
+
239
+ uri_handler.path = path_info || ""
240
+ # set the query string to nil if it is blank (otherwise it will leave a '?' in the url)
241
+ uri_handler.query = (query_string && !query_string.empty?) ? query_string : nil
242
+ uri_handler
243
+ end
244
+
245
+ def reset_request_uri(uri_handler, hostname_with_optional_port, request_method, path_info, query_string)
246
+ hostname, port = hostname_with_optional_port.split(":")
247
+
248
+ uri_handler.host = hostname
249
+ uri_handler.port = port unless port.nil?
250
+
251
+ if request_method.downcase == "get"
252
+ uri_handler.path = path_info || ""
253
+ # set the query string to nil if it is blank (otherwise it will leave a '?' in the url)
254
+ uri_handler.query = (query_string && !query_string.empty?) ? query_string : nil
255
+ else
256
+ uri_handler.path = ""
257
+ uri_handler.query = nil
258
+ end
259
+
260
+ uri_handler
261
+ end
262
+
263
+ # Call through to the equivalent module method in RightSharding::Routing.
264
+ def add_account_to_path(original_path, account_id)
265
+ RightSharding::Routing.add_account_to_path(original_path, account_id)
266
+ end
267
+
268
+ #-----------------------------------------------------------------------
269
+ # Redirect Helpers
270
+ #-----------------------------------------------------------------------
271
+
272
+ def reroute_to_shard(uri_handler)
273
+ location = uri_handler.to_s
274
+
275
+ [ 301, { "Location" => location }, [ redirect_message(location) ] ]
276
+ end
277
+
278
+ def redirect_message(location)
279
+ %Q(Redirecting to <a href="#{location}">#{location}</a>)
280
+ end
281
+
282
+ #-----------------------------------------------------------------------
283
+ # Logging Helpers
284
+ #-----------------------------------------------------------------------
285
+
286
+ def shard_routing_logger(type, header, uri, query_string, request_method)
287
+ logger.send(type, "RightSharding::Routing: #{header} - #{uri} - '#{query_string}' - '#{request_method}' request")
288
+ end
289
+
290
+ end
291
+
292
+ end
293
+ end
294
+ end
295
+
296
+ module Rack
297
+ ShardRouting = ::RightSharding::Rack::Middleware::Routing unless defined?(::Rack::ShardRouting)
298
+ end
@@ -0,0 +1,137 @@
1
+ #--
2
+ # Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale, Inc. and
11
+ # the licensee.
12
+ #++
13
+
14
+ module RightSharding
15
+ module ShardHandler
16
+
17
+ SHARDING_CACHE_EXPIRATION_RANGE = (30..60).to_a
18
+
19
+ #---------------------------------------------------------------------------
20
+ # THIS_SHARD_ID
21
+ #---------------------------------------------------------------------------
22
+
23
+ def self.fetch_this_shard_id(config_object)
24
+ # defer the call to the object after we are sure that it is not in cache
25
+ this_shard_id_proc = Proc.new { config_object.find_this_shard_id }
26
+
27
+ if CurrentRequestCache.is_activated?
28
+ CurrentRequestCache.find_or_save_this_shard_id(this_shard_id_proc)
29
+ else
30
+ # In this case, we are in script/console, migration, or daemon etc, we don't want to cache it in Thread.current
31
+ this_shard_id_proc.call
32
+ end
33
+ end
34
+
35
+ #---------------------------------------------------------------------------
36
+ # THIS_SHARD
37
+ #---------------------------------------------------------------------------
38
+
39
+ def self.shard_object_cache_key(shard_id)
40
+ "shard/#{shard_id}"
41
+ end
42
+
43
+ def self.fetch_shard(shard_object, shard_id)
44
+ # defer the call to the object after we are sure that it is not in cache
45
+ this_shard_proc = Proc.new {
46
+ shard_object.get_shard_object(shard_id, shard_object_cache_key(shard_id), SHARDING_CACHE_EXPIRATION_RANGE.rand.minutes)
47
+ }
48
+
49
+ if CurrentRequestCache.is_activated?
50
+ CurrentRequestCache.find_or_save_this_shard(this_shard_proc)
51
+ else
52
+ # In this case, we are in script/console, migration, or daemon etc, we don't want to cache it in Thread.current
53
+ this_shard_proc.call
54
+ end
55
+ end
56
+
57
+ def self.fetch_this_shard(config_object, shard_object)
58
+ fetch_shard(shard_object, fetch_this_shard_id(config_object))
59
+ end
60
+
61
+ #---------------------------------------------------------------------------
62
+ # SHARD information for each Account
63
+ #---------------------------------------------------------------------------
64
+
65
+ def self.shard_cache_key(account_id)
66
+ "account/#{account_id}/shard_id_and_hostname"
67
+ end
68
+
69
+ def self.fetch_shard_id_and_hostname(sharding_object, account_id)
70
+ # defer the call to the object after we are sure that it is not in cache
71
+ get_shard_id_and_hostname_proc = Proc.new {
72
+ sharding_object.get_shard_id_and_hostname(account_id, shard_cache_key(account_id), SHARDING_CACHE_EXPIRATION_RANGE.rand.minutes)
73
+ }
74
+
75
+ if CurrentRequestCache.is_activated?
76
+ CurrentRequestCache.find_or_save_shard_map(account_id, get_shard_id_and_hostname_proc)
77
+ else
78
+ # In this case, we are in script/console, migration, or daemon etc, we don't want to cache it in Thread.current
79
+ get_shard_id_and_hostname_proc.call
80
+ end
81
+ end
82
+
83
+ def self.fetch_shard_hostname(sharding_object, account_id)
84
+ requested_shard = fetch_shard_id_and_hostname(sharding_object, account_id)
85
+ requested_shard[:shard_hostname]
86
+ end
87
+
88
+ #---------------------------------------------------------------------------
89
+ # SHARD information for each InstanceApiToken
90
+ #---------------------------------------------------------------------------
91
+
92
+ def self.instance_api_token_shard_cache_key(token)
93
+ "instance_api_token/#{token}/shard_id_and_hostname"
94
+ end
95
+
96
+ def self.fetch_instance_api_token_shard_id_and_hostname(token)
97
+ # FIXME: cyclical dependency - InstanceApiToken is defined in right_site
98
+ InstanceApiToken.get_shard_id_and_hostname(token, instance_api_token_shard_cache_key(token), SHARDING_CACHE_EXPIRATION_RANGE.rand.minutes)
99
+ end
100
+
101
+ #---------------------------------------------------------------------------
102
+ # CurrentRequestCache - caching data only for this request
103
+ #---------------------------------------------------------------------------
104
+
105
+ class CurrentRequestCache
106
+ CACHE_ID = :right_sharding_current_request_cache
107
+
108
+ def self.init
109
+ Thread.current[CACHE_ID] = {}
110
+
111
+ Thread.current[CACHE_ID]["shard_map"] = {}
112
+ Thread.current[CACHE_ID]["this_shard_id"] = nil
113
+ end
114
+
115
+ def self.destroy
116
+ Thread.current[CACHE_ID] = nil
117
+ end
118
+
119
+ def self.is_activated?
120
+ !Thread.current[CACHE_ID].nil?
121
+ end
122
+
123
+ def self.find_or_save_shard_map(account_id, value_proc)
124
+ Thread.current[CACHE_ID]["shard_map"][account_id] ||= value_proc.call
125
+ end
126
+
127
+ def self.find_or_save_this_shard_id(value_proc)
128
+ Thread.current[CACHE_ID]["this_shard_id"] ||= value_proc.call
129
+ end
130
+
131
+ def self.find_or_save_this_shard(value_proc)
132
+ Thread.current[CACHE_ID]["this_shard"] ||= value_proc.call
133
+ end
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,147 @@
1
+ #--
2
+ # Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale, Inc. and
11
+ # the licensee.
12
+ #++
13
+
14
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
15
+
16
+ module RightSharding
17
+ module Rack
18
+ module Middleware
19
+
20
+ class UserApiProxy < BaseProxy
21
+
22
+ ACCOUNTS_TO_PROXY = []
23
+
24
+ class UserApiProxyYield < BaseProxyYield; end
25
+ class UserApiProxyException < BaseProxyException; end
26
+
27
+ #-----------------------------------------------------------------------
28
+ # Rack Middleware
29
+ #-----------------------------------------------------------------------
30
+
31
+ def initialize(app, params = {})
32
+ super
33
+ @http_proto = params[:http_proto]
34
+ @accounts_to_proxy = params[:accounts_to_proxy] || []
35
+ end
36
+
37
+ def call(env)
38
+ @env = env
39
+
40
+ if is_an_api_call?
41
+ account_id = get_account_id()
42
+ if is_proxiable?(account_id)
43
+ this_shard_id = RightSharding::ShardHandler.fetch_this_shard_id(@config_object)
44
+ requested_shard = RightSharding::ShardHandler.fetch_shard_id_and_hostname(@sharding_object, account_id.to_i)
45
+ requested_shard_id = requested_shard.blank? ? nil : requested_shard[:shard_id]
46
+
47
+ if !requested_shard_id.blank? && (this_shard_id != requested_shard_id)
48
+ @env['global_session.req.update'] = false
49
+ hostname = get_shard_hostname(account_id)
50
+ return proxy_request_to_the_destinated_shard(hostname, :proxy_global_session => true)
51
+ end
52
+ end
53
+ end
54
+
55
+ # everything is good
56
+ return @app.call(env)
57
+ rescue UserApiProxyYield => e
58
+ shard_proxy_logger(:error, e.message)
59
+ return @app.call(env)
60
+ rescue UserApiProxyException => e
61
+ shard_proxy_logger(:error, "failed: #{e.class} " + e.message + "\n " + e.backtrace.join("\n "))
62
+ return [e.code, e.headers, e.body]
63
+ end
64
+
65
+ # -----------------------------------------------------------------------
66
+ # General Helpers
67
+ # -----------------------------------------------------------------------
68
+
69
+ def is_proxiable?(account_id)
70
+ return true if @accounts_to_proxy.any? {|p| p == account_id}
71
+ false
72
+ end
73
+
74
+ def get_shard_hostname(account_id)
75
+ RightSharding::ShardHandler.fetch_shard_hostname(@sharding_object, account_id)
76
+ end
77
+
78
+ def get_account_id()
79
+ account_id = nil
80
+ api_version = get_api_version()
81
+ if api_version == 1.0 # It can be in url or global session, prefer url
82
+ if @env["PATH_INFO"] =~ /(?:^|\/)api\/acct\/([^\/]+)(?:\/|$)/
83
+ account_id = $1
84
+ else
85
+ if @env['global_session'] && @env['global_session']['account']
86
+ account_id = @env['global_session']['account']
87
+ end
88
+ end
89
+ elsif api_version == 1.5 # version 1.5 assumes a global account session
90
+ if @env['global_session'] && @env['global_session']['account']
91
+ account_id = @env['global_session']['account']
92
+ elsif @env['PATH_INFO'] =~ /(?:^|\/)api\/session.*$/
93
+ # We get here if it's a session request
94
+ req = ::Rack::Request.new(@env)
95
+ params = req.params
96
+ if params['account_href'] =~ /\/api\/accounts\/(\d+)/
97
+ account_id = $1
98
+ end
99
+ end
100
+ end
101
+ account_id.nil? ? nil : account_id.to_i
102
+ end
103
+
104
+ def is_an_api_call?
105
+ return true if @env['PATH_INFO'] =~ /(?:^|\/)api\/.*$/
106
+ false
107
+ end
108
+
109
+ def new_uri_handler(schema, hostname_with_port, path_info, query_string)
110
+ hostname, port = hostname_with_port.split(":")
111
+
112
+ uri_handler = if schema == "http"
113
+ URI::HTTP.build(:host => hostname, :port => port)
114
+ else
115
+ URI::HTTPS.build(:host => hostname, :port => port)
116
+ end
117
+
118
+ uri_handler.path = path_info || ""
119
+ # set the query string to nil if it is blank (otherwise it will leave a '?' in the url)
120
+ uri_handler.query = (query_string && !query_string.empty?) ? query_string : nil
121
+ uri_handler
122
+ end
123
+
124
+ def get_http_proto(env)
125
+ if @http_proto
126
+ @http_proto
127
+ elsif env['HTTPS'] == 'on'
128
+ 'https'
129
+ elsif env['HTTP_X_FORWARDED_SSL'] == 'on'
130
+ 'https'
131
+ elsif env['HTTP_X_FORWARDED_PROTO']
132
+ env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
133
+ else
134
+ 'https'
135
+ end
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+ end
142
+ end
143
+
144
+ module Rack
145
+ ShardUserApiProxy = ::RightSharding::Rack::Middleware::UserApiProxy unless defined?(::Rack::ShardUserApiProxy)
146
+ end
147
+
metadata ADDED
@@ -0,0 +1,239 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: right_sharding
3
+ version: !ruby/object:Gem::Version
4
+ hash: 17
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 3
10
+ version: 1.0.3
11
+ platform: ruby
12
+ authors:
13
+ - RightScale, Inc.
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2014-07-25 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ type: :runtime
22
+ name: global_session
23
+ version_requirements: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - <
27
+ - !ruby/object:Gem::Version
28
+ hash: 27
29
+ segments:
30
+ - 4
31
+ - 0
32
+ version: "4.0"
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ hash: 9
36
+ segments:
37
+ - 1
38
+ - 0
39
+ - 15
40
+ version: 1.0.15
41
+ prerelease: false
42
+ requirement: *id001
43
+ - !ruby/object:Gem::Dependency
44
+ type: :runtime
45
+ name: mime-types
46
+ version_requirements: &id002 !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ~>
50
+ - !ruby/object:Gem::Version
51
+ hash: 47
52
+ segments:
53
+ - 1
54
+ - 16
55
+ version: "1.16"
56
+ prerelease: false
57
+ requirement: *id002
58
+ - !ruby/object:Gem::Dependency
59
+ type: :runtime
60
+ name: rest-client
61
+ version_requirements: &id003 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ hash: 97
67
+ segments:
68
+ - 1
69
+ - 7
70
+ - 0
71
+ - 3
72
+ version: 1.7.0.3
73
+ prerelease: false
74
+ requirement: *id003
75
+ - !ruby/object:Gem::Dependency
76
+ type: :runtime
77
+ name: actionpack
78
+ version_requirements: &id004 !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - <
82
+ - !ruby/object:Gem::Version
83
+ hash: 27
84
+ segments:
85
+ - 4
86
+ - 0
87
+ version: "4.0"
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 9
91
+ segments:
92
+ - 2
93
+ - 3
94
+ - 5
95
+ version: 2.3.5
96
+ prerelease: false
97
+ requirement: *id004
98
+ - !ruby/object:Gem::Dependency
99
+ type: :runtime
100
+ name: right_support
101
+ version_requirements: &id005 !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ~>
105
+ - !ruby/object:Gem::Version
106
+ hash: 9
107
+ segments:
108
+ - 2
109
+ - 5
110
+ version: "2.5"
111
+ prerelease: false
112
+ requirement: *id005
113
+ - !ruby/object:Gem::Dependency
114
+ type: :runtime
115
+ name: simple_uuid
116
+ version_requirements: &id006 !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ~>
120
+ - !ruby/object:Gem::Version
121
+ hash: 15
122
+ segments:
123
+ - 0
124
+ - 2
125
+ version: "0.2"
126
+ prerelease: false
127
+ requirement: *id006
128
+ - !ruby/object:Gem::Dependency
129
+ type: :development
130
+ name: nokogiri
131
+ version_requirements: &id007 !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ~>
135
+ - !ruby/object:Gem::Version
136
+ hash: 3
137
+ segments:
138
+ - 1
139
+ - 5
140
+ - 0
141
+ version: 1.5.0
142
+ prerelease: false
143
+ requirement: *id007
144
+ - !ruby/object:Gem::Dependency
145
+ type: :development
146
+ name: jeweler
147
+ version_requirements: &id008 !ruby/object:Gem::Requirement
148
+ none: false
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ hash: 3
153
+ segments:
154
+ - 0
155
+ version: "0"
156
+ prerelease: false
157
+ requirement: *id008
158
+ - !ruby/object:Gem::Dependency
159
+ type: :development
160
+ name: yard
161
+ version_requirements: &id009 !ruby/object:Gem::Requirement
162
+ none: false
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ hash: 3
167
+ segments:
168
+ - 0
169
+ version: "0"
170
+ prerelease: false
171
+ requirement: *id009
172
+ - !ruby/object:Gem::Dependency
173
+ type: :development
174
+ name: redcarpet
175
+ version_requirements: &id010 !ruby/object:Gem::Requirement
176
+ none: false
177
+ requirements:
178
+ - - <
179
+ - !ruby/object:Gem::Version
180
+ hash: 7
181
+ segments:
182
+ - 3
183
+ - 0
184
+ version: "3.0"
185
+ prerelease: false
186
+ requirement: *id010
187
+ description:
188
+ email:
189
+ executables: []
190
+
191
+ extensions: []
192
+
193
+ extra_rdoc_files:
194
+ - README.md
195
+ files:
196
+ - lib/right_sharding.rb
197
+ - lib/right_sharding/base_proxy.rb
198
+ - lib/right_sharding/current_account.rb
199
+ - lib/right_sharding/extensions/core.rb
200
+ - lib/right_sharding/instance_api_proxy.rb
201
+ - lib/right_sharding/routing.rb
202
+ - lib/right_sharding/shard_handler.rb
203
+ - lib/right_sharding/user_api_proxy.rb
204
+ - README.md
205
+ homepage:
206
+ licenses: []
207
+
208
+ post_install_message:
209
+ rdoc_options: []
210
+
211
+ require_paths:
212
+ - lib
213
+ required_ruby_version: !ruby/object:Gem::Requirement
214
+ none: false
215
+ requirements:
216
+ - - ">="
217
+ - !ruby/object:Gem::Version
218
+ hash: 3
219
+ segments:
220
+ - 0
221
+ version: "0"
222
+ required_rubygems_version: !ruby/object:Gem::Requirement
223
+ none: false
224
+ requirements:
225
+ - - ">="
226
+ - !ruby/object:Gem::Version
227
+ hash: 3
228
+ segments:
229
+ - 0
230
+ version: "0"
231
+ requirements: []
232
+
233
+ rubyforge_project:
234
+ rubygems_version: 1.8.15
235
+ signing_key:
236
+ specification_version: 3
237
+ summary: ""
238
+ test_files: []
239
+