tenantify 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +166 -0
- data/Rakefile +2 -0
- data/lib/tenantify.rb +57 -0
- data/lib/tenantify/configuration.rb +25 -0
- data/lib/tenantify/middleware.rb +40 -0
- data/lib/tenantify/middleware/builder.rb +63 -0
- data/lib/tenantify/middleware/strategies.rb +57 -0
- data/lib/tenantify/middleware/strategies/default.rb +46 -0
- data/lib/tenantify/middleware/strategies/header.rb +52 -0
- data/lib/tenantify/middleware/strategies/host.rb +57 -0
- data/lib/tenantify/resource.rb +28 -0
- data/lib/tenantify/tenant.rb +44 -0
- data/lib/tenantify/version.rb +5 -0
- data/spec/integration/manual_tenant_selection_spec.rb +16 -0
- data/spec/integration/middleware/custom_strategy.rb +40 -0
- data/spec/integration/middleware/many_strategies.rb +35 -0
- data/spec/integration/middleware/one_strategy_spec.rb +36 -0
- data/spec/integration/middleware/using_resources_spec.rb +48 -0
- data/spec/integration/resources_spec.rb +16 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/tenantify/configuration_spec.rb +19 -0
- data/spec/tenantify/middleware/builder_spec.rb +66 -0
- data/spec/tenantify/middleware/strategies/default_spec.rb +17 -0
- data/spec/tenantify/middleware/strategies/header_spec.rb +27 -0
- data/spec/tenantify/middleware/strategies/host_spec.rb +32 -0
- data/spec/tenantify/middleware/strategies_spec.rb +41 -0
- data/spec/tenantify/middleware_spec.rb +37 -0
- data/spec/tenantify/resource_spec.rb +41 -0
- data/spec/tenantify/tenant_spec.rb +43 -0
- data/spec/tenantify/version_spec.rb +9 -0
- data/spec/tenantify_spec.rb +7 -0
- data/tenantify.gemspec +24 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8c5ba9f063bf0b429174f9c8e1c0ec83c7502bf8
|
4
|
+
data.tar.gz: 90a9332da5ce0b6a1ff840181ab141c45a3bf45c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fff72aa956ebf8afcd1d6129a2b6114743b0514b3d79d01036cc85c32464e239e02341d1536d1c2fa3f9b945bbd8e19e9d0655df8f02728b8b372adf1fcbec25
|
7
|
+
data.tar.gz: 9f18246005abc4e3d0580ec40b443cc4b591e1c306e4c13747a701ce3cdb16d49139dbb8cf5ff90e29fc20f4459dd805418ec25f3405f6ddd7eb24502416273f
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Jaime Cabot Campins
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
# Tenantify
|
2
|
+
|
3
|
+
This gem provides some tools to manage multitenancy on Ruby applications.
|
4
|
+
|
5
|
+
## Synopsis
|
6
|
+
|
7
|
+
Tenantify is a tool to simplify multitenancy on Ruby applications.
|
8
|
+
It stores the current tenant in a thread variable and provides:
|
9
|
+
|
10
|
+
- A Rack middleware supporting some built-in and custom strategies to find a tenant
|
11
|
+
and set it as the current one for a request.
|
12
|
+
|
13
|
+
- A Resource class to encapsulate your application resources per tenant (databases,
|
14
|
+
external services, your own ruby instances, etc)
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'tenantify'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
$ bundle
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
### The current tenant
|
31
|
+
|
32
|
+
Tenantify provides some class methods to set the current tenant for a piece of code.
|
33
|
+
|
34
|
+
To execute some code for a particular tenant:
|
35
|
+
```ruby
|
36
|
+
Tenantify.using(:the_tenant) { # some code }
|
37
|
+
```
|
38
|
+
|
39
|
+
After that the tenant is set to its previous value.
|
40
|
+
|
41
|
+
The using method returns the value returned by the block. If you want to get some data from
|
42
|
+
a database for a particular tenant:
|
43
|
+
```ruby
|
44
|
+
data = Tenantify.using :the_tenant do
|
45
|
+
get_data_from_database
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
To get the current tenant `Tenantify.current` is provided.
|
50
|
+
|
51
|
+
The `#using` method is the recommended way to run code for a particular tenant, but in some cases
|
52
|
+
may be useful to set a tenant as the current one from now on instead of just running a block of code.
|
53
|
+
For instance, in a pry session:
|
54
|
+
```ruby
|
55
|
+
[1] pry(main)> Tenantify.use! :my_tenant
|
56
|
+
[2] pry(main)> Tenantify.current
|
57
|
+
=> :my_tenant
|
58
|
+
```
|
59
|
+
|
60
|
+
Example to show `Tenantify.using` and `Tenantify.use!` behaviour:
|
61
|
+
```ruby
|
62
|
+
# No tenant is set by default
|
63
|
+
Tenantify.current # => nil
|
64
|
+
|
65
|
+
Tenantify.using :tenant_1 do
|
66
|
+
Tenantify.current # => :tenant_1
|
67
|
+
|
68
|
+
# Nested `using` blocks allowed
|
69
|
+
Tenantify.using :tenant_2 do
|
70
|
+
Tenantify.current # => :tenant_2
|
71
|
+
|
72
|
+
Tenantify.use! :tenant_3
|
73
|
+
Tenantify.current # => :tenant_3
|
74
|
+
end
|
75
|
+
|
76
|
+
# When a block ends the tenant before the block is set again
|
77
|
+
# even if it has changed inside the block due a `use!` call.
|
78
|
+
Tenantify.current # => :tenant_1
|
79
|
+
end
|
80
|
+
|
81
|
+
Tenantify.use! :tenant_4
|
82
|
+
|
83
|
+
# The current tenant is stored as a thread variable. On new threads it has to be set manually.
|
84
|
+
Thread.new do
|
85
|
+
Tenantify.current # => nil
|
86
|
+
end
|
87
|
+
Tenantify.current # => :tenant_4
|
88
|
+
```
|
89
|
+
|
90
|
+
### Resources
|
91
|
+
|
92
|
+
On your multitenant application some resources may depend on the current tenant. For instance: A Sequel database,
|
93
|
+
a redis database, the host of an external service, etc.
|
94
|
+
|
95
|
+
You could handle this situation by calling `Tenantify.current` to determine the resource you need for a tenant.
|
96
|
+
To avoid having to deal with the current tenant within your app business logic a `Tenantify::Resource` class is
|
97
|
+
provided.
|
98
|
+
|
99
|
+
To build a tenantified resource you have to build a hash that maps the tenant name to the resource for that tenant.
|
100
|
+
The following example shows how to configure a redis database per tenant on the same host.
|
101
|
+
```ruby
|
102
|
+
# when initializing your application:
|
103
|
+
correspondence = {
|
104
|
+
:tenant_1 => Redis.new(:host => "localhost", :port => 6379, :db => 1),
|
105
|
+
:tenant_2 => Redis.new(:host => "localhost", :port => 6379, :db => 2)
|
106
|
+
}
|
107
|
+
|
108
|
+
redis_resource = Tenantify.resource(correspondence)
|
109
|
+
Object.const_set("Redis", redis_resource)
|
110
|
+
|
111
|
+
|
112
|
+
# at the entry point of your application
|
113
|
+
tenant_name = find_out_current_tenant
|
114
|
+
Tenantify.using(tenant_name) { app.run }
|
115
|
+
|
116
|
+
|
117
|
+
# anywhere inside your app
|
118
|
+
Redis.current # => Returns the redis instance for the current tenant
|
119
|
+
```
|
120
|
+
|
121
|
+
You can build a resource for any multitenant resource you have on your application.
|
122
|
+
|
123
|
+
### The Rack middleware
|
124
|
+
|
125
|
+
You can use Tenantify with any Ruby application you like and set the current tenant as soon as you know it,
|
126
|
+
ideally outside of the boundaries of your application business logic.
|
127
|
+
|
128
|
+
On a Rack application, this place is somewhere in the middleware stack. To handle this situation Tenantify
|
129
|
+
provides a `Tenantify::Middleware`.
|
130
|
+
|
131
|
+
There are several strategies you could use to choose the tenant to work with from the Rack environment.
|
132
|
+
Tenantify has some basic built-in strategies, but you might want to implement your custom ones to handle
|
133
|
+
more specific situations.
|
134
|
+
|
135
|
+
The built-in strategies are:
|
136
|
+
|
137
|
+
* The `:header` strategy expects the tenant name to be sent in a request header.
|
138
|
+
* The `:host` strategy expects a hash with tenants as keys, and arrays of hosts as values.
|
139
|
+
* The `:default` strategy always returns the same tenant.
|
140
|
+
|
141
|
+
You can configure Tenantify to use one or more strategies.
|
142
|
+
|
143
|
+
The following example configures Tenantify to:
|
144
|
+
|
145
|
+
* Check the "X-Tenant" header to look for a tenant.
|
146
|
+
* If the header does not exist, select a tenant associated to the current host.
|
147
|
+
* If no tenant provided for the current host, use de tenant: `:main_tenant`
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
# when initializing your application
|
151
|
+
hosts_per_tenant = {
|
152
|
+
:tenant_1 => ["www.host_a.com", "www.host_b.com"],
|
153
|
+
:tenant_2 => ["www.host_c.com"]
|
154
|
+
}
|
155
|
+
|
156
|
+
Tenantify.configure do |config|
|
157
|
+
config.strategy :header, :name => "X-Tenant"
|
158
|
+
config.strategy :hosts, hosts_per_tenant
|
159
|
+
config.strategy :default, :tenant => :main_tenant
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
# on your config.ru
|
164
|
+
use Tenantify::Middleware
|
165
|
+
run MyRackApplication
|
166
|
+
```
|
data/Rakefile
ADDED
data/lib/tenantify.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require "tenantify/version"
|
2
|
+
|
3
|
+
require "tenantify/configuration"
|
4
|
+
require "tenantify/tenant"
|
5
|
+
require "tenantify/resource"
|
6
|
+
require "tenantify/middleware"
|
7
|
+
|
8
|
+
module Tenantify
|
9
|
+
# Tenantify configuration
|
10
|
+
#
|
11
|
+
# @return [Configuration] the current configuration
|
12
|
+
def self.configuration
|
13
|
+
@configuration ||= Configuration.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# A helper to configure Tenantify
|
17
|
+
#
|
18
|
+
# @yield [configuration] Configures tenantify
|
19
|
+
def self.configure
|
20
|
+
yield configuration
|
21
|
+
end
|
22
|
+
|
23
|
+
# An alias to {Tenant::using}
|
24
|
+
#
|
25
|
+
# @example Run some code on a particular tenant
|
26
|
+
# Tenantify.using :a_tenant do
|
27
|
+
# # some code...
|
28
|
+
# end
|
29
|
+
# @see Tenant.using
|
30
|
+
def self.using tenant, &block
|
31
|
+
Tenant.using(tenant, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
# An alias to {Tenant::use!}
|
35
|
+
#
|
36
|
+
# @example Change the current tenant
|
37
|
+
# Tenanfify.use! :a_tenant
|
38
|
+
# # using :a_tenant from now on
|
39
|
+
# @see Tenant.use!
|
40
|
+
def self.use! tenant
|
41
|
+
Tenant.use!(tenant)
|
42
|
+
end
|
43
|
+
|
44
|
+
# An alias to {Tenant::current}
|
45
|
+
#
|
46
|
+
# @see Tenant.current
|
47
|
+
def self.current
|
48
|
+
Tenant.current
|
49
|
+
end
|
50
|
+
|
51
|
+
# An alias to {Resource::new}
|
52
|
+
#
|
53
|
+
# @see Resource
|
54
|
+
def self.resource correspondence
|
55
|
+
Resource.new(correspondence)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Tenantify
|
2
|
+
# It stores a configuration for {Tenantify::Middleware}.
|
3
|
+
class Configuration
|
4
|
+
# All configured strategies in order of priority.
|
5
|
+
#
|
6
|
+
# @return [Array<strategy_config>] a collection of strategy configurations.
|
7
|
+
attr_reader :strategies
|
8
|
+
|
9
|
+
# Constructor.
|
10
|
+
def initialize
|
11
|
+
@strategies = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Adds a new strategy for the Tenantify middleware. The order the strategies
|
15
|
+
# are added is the priority order they have to match the tenant.
|
16
|
+
#
|
17
|
+
# @param [Symbol, Class] the name of a known strategy or a custom strategy
|
18
|
+
# class.
|
19
|
+
# @param [Hash] strategy configuration.
|
20
|
+
# @return [Array<strategy_config>] a collection of strategy configurations.
|
21
|
+
def strategy name_or_class, strategy_config = {}
|
22
|
+
strategies << [name_or_class, strategy_config]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'tenantify/tenant'
|
2
|
+
require 'tenantify/middleware/builder'
|
3
|
+
|
4
|
+
module Tenantify
|
5
|
+
# Rack middleware responsible for setting the tenant during the http request.
|
6
|
+
#
|
7
|
+
# This middleware builds a set of strategies from the given configuration, and
|
8
|
+
# sets the tenant returned from those strategies.
|
9
|
+
class Middleware
|
10
|
+
# Constructor.
|
11
|
+
#
|
12
|
+
# @param [#call] the Rack application
|
13
|
+
# @param [Tenantify::Configuration] the Rack application
|
14
|
+
def initialize app, config = Tenantify.configuration
|
15
|
+
@app = app
|
16
|
+
@config = config
|
17
|
+
end
|
18
|
+
|
19
|
+
# Calls the rack middleware.
|
20
|
+
#
|
21
|
+
# @param [rack_environment] the Rack environment
|
22
|
+
# @return [rack_response] the Rack response
|
23
|
+
def call env
|
24
|
+
tenant = strategies.tenant_for(env)
|
25
|
+
|
26
|
+
Tenant.using(tenant) { app.call(env) }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :app, :config
|
32
|
+
|
33
|
+
def strategies
|
34
|
+
@strategies ||= begin
|
35
|
+
builder = Builder.new(config)
|
36
|
+
builder.call
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'tenantify/middleware/strategies'
|
2
|
+
|
3
|
+
strategies_pattern = File.join(File.dirname(__FILE__), "strategies/*.rb")
|
4
|
+
Dir[strategies_pattern].each { |strategy_file| require strategy_file }
|
5
|
+
|
6
|
+
module Tenantify
|
7
|
+
class Middleware
|
8
|
+
# This class builds all the strategies and injects them into a
|
9
|
+
# Strategies object.
|
10
|
+
class Builder
|
11
|
+
# Invalid strategy specification
|
12
|
+
UnknownStrategyError = Class.new(StandardError)
|
13
|
+
|
14
|
+
# Known strategies. They can be specified with a symbol.
|
15
|
+
KNOWN_STRATEGIES = {
|
16
|
+
:header => Strategies::Header,
|
17
|
+
:host => Strategies::Host,
|
18
|
+
:default => Strategies::Default
|
19
|
+
}
|
20
|
+
|
21
|
+
# @return [Tenantify::Configuration] given configuration.
|
22
|
+
attr_reader :config
|
23
|
+
|
24
|
+
# Constructor.
|
25
|
+
#
|
26
|
+
# @param [Tenantify::Configuration] the tenantify configuration.
|
27
|
+
# @param [Hash] a correspondence between strategy names and classes.
|
28
|
+
def initialize config, known_strategies: KNOWN_STRATEGIES
|
29
|
+
@config = config
|
30
|
+
@known_strategies = known_strategies
|
31
|
+
end
|
32
|
+
|
33
|
+
# Builds the Strategies object.
|
34
|
+
#
|
35
|
+
# @return [Strategies] the strategies object.
|
36
|
+
def call
|
37
|
+
Strategies.new(strategies)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :known_strategies
|
43
|
+
|
44
|
+
def strategies
|
45
|
+
strategies_config.map do |(name_or_class, strategy_config)|
|
46
|
+
strategy_class_for(name_or_class).new(strategy_config)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def strategy_class_for name_or_class
|
51
|
+
case name_or_class
|
52
|
+
when Class then name_or_class
|
53
|
+
when Symbol, String then known_strategies.fetch(name_or_class.to_sym)
|
54
|
+
else raise UnknownStrategyError.new(name_or_class.inspect)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def strategies_config
|
59
|
+
config.strategies
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Tenantify
|
2
|
+
class Middleware
|
3
|
+
# Responsible for finding the tenant for the given env.
|
4
|
+
#
|
5
|
+
# It iterates the strategies given to the constructor until it finds
|
6
|
+
# one that returns a tenant.
|
7
|
+
#
|
8
|
+
# == Default strategy
|
9
|
+
# When there is no matching strategy for the current environment
|
10
|
+
# a NotMatchingStrategyError is raised. To avoid this behaviour and
|
11
|
+
# use a particular tenant by default a Default strategy is provided.
|
12
|
+
#
|
13
|
+
# @example Configuring a tenant by default:
|
14
|
+
# Tenantify.configure do |config|
|
15
|
+
# # your strategies
|
16
|
+
#
|
17
|
+
# config.strategy :default, :tenant => :my_default_tenant
|
18
|
+
# end
|
19
|
+
class Strategies
|
20
|
+
# None strategy found a valid tenant for the given environment
|
21
|
+
NotMatchingStrategyError = Class.new(StandardError)
|
22
|
+
|
23
|
+
# Constructor. It receives all strategies in order of precedence.
|
24
|
+
#
|
25
|
+
# @param [<#tenant_for>] enumerable of strategies
|
26
|
+
def initialize strategies
|
27
|
+
@strategies = strategies
|
28
|
+
end
|
29
|
+
|
30
|
+
# Find a tenant for the current env.
|
31
|
+
#
|
32
|
+
# @param [rack_environment] current env.
|
33
|
+
# @return [Symbol] returns the matching tenant.
|
34
|
+
def tenant_for env
|
35
|
+
find_tenant_for(env) or raise_error(env)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :strategies
|
41
|
+
|
42
|
+
def find_tenant_for env
|
43
|
+
lazy_strategies.
|
44
|
+
map { |strategy| strategy.tenant_for(env) }.
|
45
|
+
find { |tenant| tenant }
|
46
|
+
end
|
47
|
+
|
48
|
+
def lazy_strategies
|
49
|
+
@lazy_strategies ||= strategies.lazy
|
50
|
+
end
|
51
|
+
|
52
|
+
def raise_error env
|
53
|
+
raise NotMatchingStrategyError.new(env.inspect)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|