Skip to content

Commit

Permalink
Merge branch 'release/v0.0.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
jcabotc committed Dec 24, 2015
2 parents 1c647af + 6b94aaa commit 5f00b6c
Show file tree
Hide file tree
Showing 27 changed files with 1,102 additions and 77 deletions.
189 changes: 112 additions & 77 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
# Tenantify

This gem provides some tools to manage multitenancy on Rack applications
This gem provides some tools to manage multitenancy on Ruby applications.

## Synopsis

Tenantify is a tool to simplify multitenancy on Ruby applications.
It stores the current tenant in a thread variable and provides:

- A Rack middleware supporting some built-in and custom strategies to find a tenant
and set it as the current one for a request.

- A Resource class to encapsulate your application resources per tenant (databases,
external services, your own ruby instances, etc)

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'tenantify', :git => 'https://bitbucket.org/qustodian/tenantify.git'
gem 'tenantify'
```

And then execute:
Expand All @@ -16,116 +27,140 @@ And then execute:

## Usage

### Configuration
### The current tenant

To configure Tenantify for your application:
Tenantify provides some class methods to set the current tenant for a piece of code.

To execute some code for a particular tenant:
```ruby
Tenantify.configure do |config|
config.configuration_path = "my/custom/configuration.yml"
config.environment = ENV["RACK_ENV"]
end
Tenantify.using(:the_tenant) { # some code }
```

### Middleware

The first step is to configure your Rack application to use the middleware. You can do so adding the following line to your `config.ru`:
After that the tenant is set to its previous value.

The using method returns the value returned by the block. If you want to get some data from
a database for a particular tenant:
```ruby
# ... your code
use Tenantify::Middleware
data = Tenantify.using :the_tenant do
get_data_from_database
end
```

run YourRackApplication
To get the current tenant `Tenantify.current` is provided.

The `#using` method is the recommended way to run code for a particular tenant, but in some cases
may be useful to set a tenant as the current one from now on instead of just running a block of code.
For instance, in a pry session:
```ruby
[1] pry(main)> Tenantify.use! :my_tenant
[2] pry(main)> Tenantify.current
=> :my_tenant
```

The middleware sets the current tenant based on 2 sources (in order of priority):
Example to show `Tenantify.using` and `Tenantify.use!` behaviour:
```ruby
# No tenant is set by default
Tenantify.current # => nil

1. The value of the header `X-Tenant` in the Rack environment
Tenantify.using :tenant_1 do
Tenantify.current # => :tenant_1

2. The tenant associated to the domain of the request in the configuration file (`config/tenants.yml` by default)
# Nested `using` blocks allowed
Tenantify.using :tenant_2 do
Tenantify.current # => :tenant_2

### The configuration file
Tenantify.use! :tenant_3
Tenantify.current # => :tenant_3
end

The configuration file is needed if you expect to use the domain-tenant functionality.
By default it expects the tenants configuration to be in `config/tenants.yml`. If you want to provide your custom configuration file, you can inject the configuration yourself:
# When a block ends the tenant before the block is set again
# even if it has changed inside the block due a `use!` call.
Tenantify.current # => :tenant_1
end

```ruby
Tenantify.configure do |config|
config.configuration_path = "my/custom/configuration.yml"
Tenantify.use! :tenant_4

# The current tenant is stored as a thread variable. On new threads it has to be set manually.
Thread.new do
Tenantify.current # => nil
end
Tenantify.current # => :tenant_4
```

The file must have the following keys:
### Resources

```yaml
tenant_name_1:
hosts:
- www.first_host_for_tenant_1.com
- www.second_host_for_tenant_1.com
On your multitenant application some resources may depend on the current tenant. For instance: A Sequel database,
a redis database, the host of an external service, etc.

tenant_name_2:
hosts:
- www.host_for_tenant_2.com
You could handle this situation by calling `Tenantify.current` to determine the resource you need for a tenant.
To avoid having to deal with the current tenant within your app business logic a `Tenantify::Resource` class is
provided.

# more tenants configuration
```
To build a tenantified resource you have to build a hash that maps the tenant name to the resource for that tenant.
The following example shows how to configure a redis database per tenant on the same host.
```ruby
# when initializing your application:
correspondence = {
:tenant_1 => Redis.new(:host => "localhost", :port => 6379, :db => 1),
:tenant_2 => Redis.new(:host => "localhost", :port => 6379, :db => 2)
}

If you are working on multiple environments and do you want a tenants configuration per environment set the environment in the configuration block, and nest the tenants configuration under each environment in your configuration file:
redis_resource = Tenantify.resource(correspondence)
Object.const_set("Redis", redis_resource)

```ruby
Tenantify.configure do |config|
config.environment = ENV["RACK_ENV"]
end

# at the entry point of your application
tenant_name = find_out_current_tenant
Tenantify.using(tenant_name) { app.run }


# anywhere inside your app
Redis.current # => Returns the redis instance for the current tenant
```

The file must have the following keys:
You can build a resource for any multitenant resource you have on your application.

```yaml
test:
single_tenant:
hosts:
- localhost
- www.example.com
### The Rack middleware

production:
tenant_name_1:
hosts:
- www.first_host_for_tenant_1.com
- www.second_host_for_tenant_1.com
You can use Tenantify with any Ruby application you like and set the current tenant as soon as you know it,
ideally outside of the boundaries of your application business logic.

tenant_name_2:
hosts:
- www.host_for_tenant_2.com
On a Rack application, this place is somewhere in the middleware stack. To handle this situation Tenantify
provides a `Tenantify::Middleware`.

# more environments configuration
```
There are several strategies you could use to choose the tenant to work with from the Rack environment.
Tenantify has some basic built-in strategies, but you might want to implement your custom ones to handle
more specific situations.

### The managers
The built-in strategies are:

Managers are instances of `Tenantify::Manager` that represent a system resource that is different for each tenant.
For example, imagine you need a different redis instance for each tenant:
* The `:header` strategy expects the tenant name to be sent in a request header.
* The `:host` strategy expects a hash with tenants as keys, and arrays of hosts as values.
* The `:default` strategy always returns the same tenant.

```ruby
tenant_1_redis = Redis.new(:url => "redis://...") # => #<Redis client 1>
tenant_2_redis = Redis.new(:url => "redis://...") # => #<Redis client 2>
You can configure Tenantify to use one or more strategies.

correspondence = {
"tenant_1" => tenant_1_redis,
"tenant_2" => tenant_2_redis
}
The following example configures Tenantify to:

Redis = Tenantify::Manager.new(correspondence)
* Check the "X-Tenant" header to look for a tenant.
* If the header does not exist, select a tenant associated to the current host.
* If no tenant provided for the current host, use de tenant: `:main_tenant`

# It forces the block to use a tenant (used internally by the middleware)
Tenantify::Perform.with "tenant_2" do
Redis.current # => <#Redis client 2>
end
```ruby
# when initializing your application
hosts_per_tenant = {
:tenant_1 => ["www.host_a.com", "www.host_b.com"],
:tenant_2 => ["www.host_c.com"]
}

# Iterates over all tenants and its associated resources
Redis.each do |tenant, redis_instance|
puts "#{ tenant }: #{ redis_instance }"
Tenantify.configure do |config|
config.strategy :header, :name => "X-Tenant"
config.strategy :hosts, hosts_per_tenant
config.strategy :default, :tenant => :main_tenant
end
# It puts:
# "tenant_1: <#Redis client 1>"
# "tenant_2: <#Redis client 2>"


# on your config.ru
use Tenantify::Middleware
run MyRackApplication
```
53 changes: 53 additions & 0 deletions lib/tenantify.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,57 @@
require "tenantify/version"

require "tenantify/configuration"
require "tenantify/tenant"
require "tenantify/resource"
require "tenantify/middleware"

module Tenantify
# Tenantify configuration
#
# @return [Configuration] the current configuration
def self.configuration
@configuration ||= Configuration.new
end

# A helper to configure Tenantify
#
# @yield [configuration] Configures tenantify
def self.configure
yield configuration
end

# An alias to {Tenant::using}
#
# @example Run some code on a particular tenant
# Tenantify.using :a_tenant do
# # some code...
# end
# @see Tenant.using
def self.using tenant, &block
Tenant.using(tenant, &block)
end

# An alias to {Tenant::use!}
#
# @example Change the current tenant
# Tenanfify.use! :a_tenant
# # using :a_tenant from now on
# @see Tenant.use!
def self.use! tenant
Tenant.use!(tenant)
end

# An alias to {Tenant::current}
#
# @see Tenant.current
def self.current
Tenant.current
end

# An alias to {Resource::new}
#
# @see Resource
def self.resource correspondence
Resource.new(correspondence)
end
end
25 changes: 25 additions & 0 deletions lib/tenantify/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Tenantify
# It stores a configuration for {Tenantify::Middleware}.
class Configuration
# All configured strategies in order of priority.
#
# @return [Array<strategy_config>] a collection of strategy configurations.
attr_reader :strategies

# Constructor.
def initialize
@strategies = []
end

# Adds a new strategy for the Tenantify middleware. The order the strategies
# are added is the priority order they have to match the tenant.
#
# @param [Symbol, Class] the name of a known strategy or a custom strategy
# class.
# @param [Hash] strategy configuration.
# @return [Array<strategy_config>] a collection of strategy configurations.
def strategy name_or_class, strategy_config = {}
strategies << [name_or_class, strategy_config]
end
end
end
40 changes: 40 additions & 0 deletions lib/tenantify/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'tenantify/tenant'
require 'tenantify/middleware/builder'

module Tenantify
# Rack middleware responsible for setting the tenant during the http request.
#
# This middleware builds a set of strategies from the given configuration, and
# sets the tenant returned from those strategies.
class Middleware
# Constructor.
#
# @param [#call] the Rack application
# @param [Tenantify::Configuration] the Rack application
def initialize app, config = Tenantify.configuration
@app = app
@config = config
end

# Calls the rack middleware.
#
# @param [rack_environment] the Rack environment
# @return [rack_response] the Rack response
def call env
tenant = strategies.tenant_for(env)

Tenant.using(tenant) { app.call(env) }
end

private

attr_reader :app, :config

def strategies
@strategies ||= begin
builder = Builder.new(config)
builder.call
end
end
end
end
Loading

0 comments on commit 5f00b6c

Please sign in to comment.