innkeeper 0.1.0
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 +15 -0
- data/.pryrc +3 -0
- data/.travis.yml +15 -0
- data/Appraisals +20 -0
- data/Gemfile +10 -0
- data/Guardfile +24 -0
- data/HISTORY.md +337 -0
- data/README.md +485 -0
- data/Rakefile +92 -0
- data/TODO.md +51 -0
- data/gemfiles/rails_5_1.gemfile +12 -0
- data/innkeeper.gemspec +42 -0
- data/lib/generators/innkeeper/install/USAGE +5 -0
- data/lib/generators/innkeeper/install/install_generator.rb +10 -0
- data/lib/generators/innkeeper/install/templates/innkeeper.rb +76 -0
- data/lib/innkeeper.rb +110 -0
- data/lib/innkeeper/adapters/abstract_adapter.rb +172 -0
- data/lib/innkeeper/adapters/mysql2_adapter.rb +47 -0
- data/lib/innkeeper/adapters/postgresql_adapter.rb +112 -0
- data/lib/innkeeper/console.rb +12 -0
- data/lib/innkeeper/deprecation.rb +10 -0
- data/lib/innkeeper/elevators/domain.rb +20 -0
- data/lib/innkeeper/elevators/generic.rb +32 -0
- data/lib/innkeeper/elevators/host_hash.rb +22 -0
- data/lib/innkeeper/elevators/subdomain.rb +62 -0
- data/lib/innkeeper/migrator.rb +33 -0
- data/lib/innkeeper/railtie.rb +56 -0
- data/lib/innkeeper/resolvers/abstract.rb +15 -0
- data/lib/innkeeper/resolvers/database.rb +11 -0
- data/lib/innkeeper/resolvers/schema.rb +14 -0
- data/lib/innkeeper/tasks/enhancements.rb +36 -0
- data/lib/innkeeper/tenant.rb +47 -0
- data/lib/innkeeper/version.rb +3 -0
- data/lib/tasks/innkeeper.rake +128 -0
- data/notes.md +31 -0
- data/test/config_test.rb +52 -0
- data/test/databases.yml.sample +37 -0
- data/test/decorator_test.rb +21 -0
- data/test/domain_elevator_test.rb +38 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/controllers/application_controller.rb +6 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/company.rb +3 -0
- data/test/dummy/app/models/user.rb +3 -0
- data/test/dummy/app/views/application/index.html.erb +1 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +49 -0
- data/test/dummy/config/boot.rb +11 -0
- data/test/dummy/config/database.yml.sample +38 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +27 -0
- data/test/dummy/config/environments/production.rb +51 -0
- data/test/dummy/config/environments/test.rb +34 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/innkeeper.rb +4 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/db/schema.rb +19 -0
- data/test/dummy/db/seeds.rb +5 -0
- data/test/dummy/db/seeds/import.rb +5 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/stylesheets/.gitkeep +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/excluded_models_test.rb +32 -0
- data/test/generic_elevator_test.rb +63 -0
- data/test/host_hash_elevator_test.rb +42 -0
- data/test/innkeeper_test.rb +96 -0
- data/test/mocks/adapter_mock.rb +11 -0
- data/test/multithreading_test.rb +37 -0
- data/test/mysql2_adapter_test.rb +17 -0
- data/test/postgresql_adapter_test.rb +39 -0
- data/test/railtie_test.rb +31 -0
- data/test/rake_task_test.rb +57 -0
- data/test/resolver_test.rb +21 -0
- data/test/shared/shared_adapter_tests.rb +95 -0
- data/test/subdomain_elevator_test.rb +75 -0
- data/test/test_helper.rb +24 -0
- metadata +325 -0
data/Rakefile
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'bundler' rescue 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
2
|
+
Bundler.setup
|
3
|
+
Bundler::GemHelper.install_tasks
|
4
|
+
require "rake/testtask"
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs = ["lib"]
|
9
|
+
t.warning = false
|
10
|
+
# t.verbose = true
|
11
|
+
t.test_files = FileList['test/*_test.rb']
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'appraisal'
|
15
|
+
# require "#{File.join(File.dirname(__FILE__), 'test', 'test_helper')}"
|
16
|
+
|
17
|
+
task :console do
|
18
|
+
require 'pry'
|
19
|
+
require 'innkeeper'
|
20
|
+
ARGV.clear
|
21
|
+
Pry.start
|
22
|
+
end
|
23
|
+
|
24
|
+
task default: :test
|
25
|
+
|
26
|
+
namespace :db do
|
27
|
+
namespace :test do
|
28
|
+
task prepare: %w{postgres:drop_db postgres:build_db mysql:drop_db mysql:build_db}
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "copy sample database credential files over if real files don't exist"
|
32
|
+
task :copy_credentials do
|
33
|
+
require 'fileutils'
|
34
|
+
innkeeper_db_file = 'test/databases.yml'
|
35
|
+
rails_db_file = 'test/dummy/config/database.yml'
|
36
|
+
|
37
|
+
FileUtils.copy(innkeeper_db_file + '.sample', innkeeper_db_file, :verbose => true) unless File.exists?(innkeeper_db_file)
|
38
|
+
FileUtils.copy(rails_db_file + '.sample', rails_db_file, :verbose => true) unless File.exists?(rails_db_file)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
namespace :postgres do
|
43
|
+
require 'active_record'
|
44
|
+
|
45
|
+
desc 'Build the PostgreSQL test databases'
|
46
|
+
task :build_db do
|
47
|
+
%x{ createdb -E UTF8 #{pg_config['database']} -U#{pg_config['username']} } rescue "test db already exists"
|
48
|
+
ActiveRecord::Base.establish_connection pg_config
|
49
|
+
ActiveRecord::Migration.suppress_messages do
|
50
|
+
load(File.join(File.dirname(__FILE__), "test/dummy/db/schema.rb"))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "drop the PostgreSQL test database"
|
55
|
+
task :drop_db do
|
56
|
+
puts "dropping database #{pg_config['database']}"
|
57
|
+
%x{ dropdb #{pg_config['database']} -U#{pg_config['username']} }
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
namespace :mysql do
|
63
|
+
require 'active_record'
|
64
|
+
|
65
|
+
desc 'Build the MySQL test databases'
|
66
|
+
task :build_db do
|
67
|
+
%x{ /usr/local/mysql/bin/mysqladmin -u #{my_config['username']} --password=#{my_config['password']} create #{my_config['database']} } rescue "test db already exists"
|
68
|
+
ActiveRecord::Base.establish_connection my_config
|
69
|
+
ActiveRecord::Migration.suppress_messages do
|
70
|
+
load(File.join(File.dirname(__FILE__), "test/dummy/db/schema.rb"))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "drop the MySQL test database"
|
75
|
+
task :drop_db do
|
76
|
+
puts "dropping database #{my_config['database']}"
|
77
|
+
%x{ /usr/local/mysql/bin/mysqladmin -u #{my_config['username']} --password=#{my_config['password']} drop #{my_config['database']} --force}
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
def config
|
83
|
+
@config ||= YAML.load(ERB.new(IO.read('test/databases.yml')).result)['connections']
|
84
|
+
end
|
85
|
+
|
86
|
+
def pg_config
|
87
|
+
config['postgresql']
|
88
|
+
end
|
89
|
+
|
90
|
+
def my_config
|
91
|
+
config['mysql']
|
92
|
+
end
|
data/TODO.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Innkeeper TODOs
|
2
|
+
|
3
|
+
### Below is a list of tasks in the approximate order to be completed of for Innkeeper
|
4
|
+
### Any help along the way is greatly appreciated (on any items, not particularly in order)
|
5
|
+
|
6
|
+
1. Innkeeper was originally written (and TDD'd) with just Postgresql in mind. Different adapters were added at a later date.
|
7
|
+
As such, the test suite is a bit of a mess. There's no formal structure for fully integration testing all adapters to ensure
|
8
|
+
proper quality and prevent regressions.
|
9
|
+
|
10
|
+
There's also a test order dependency as some tests run assuming a db connection and if that test randomly ran before a previous
|
11
|
+
one that makes the connection, it would fail.
|
12
|
+
|
13
|
+
I'm proposing the first thing to be done is to write up a standard, high livel integration test case that can be applied to all adapters
|
14
|
+
and makes no assumptions about implementation. It should ensure that each adapter conforms to the Innkeeper Interface and CRUD's properly.
|
15
|
+
It would be nice if a user can 'register' an adapter such that it would automatically be tested (nice to have). Otherwise one could just use
|
16
|
+
a shared behaviour to run through all of this.
|
17
|
+
|
18
|
+
Then, I'd like to see all of the implementation specific tests just in their own test file for each adapter (ie the postgresql schema adapter checks a lot of things with `schema_search_path`)
|
19
|
+
|
20
|
+
This should ensure that going forward nothing breaks, and we should *ideally* be able to randomize the test order
|
21
|
+
|
22
|
+
2. <del>`Innkeeper::Database` is the wrong abstraction. When dealing with a multi-tenanted system, users shouldn't thing about 'Databases', they should
|
23
|
+
think about Tenants. I proprose that we deprecate the `Innkeeper::Database` constant in favour of `Innkeeper::Tenant` for a nicer abstraction. See
|
24
|
+
http://myronmars.to/n/dev-blog/2011/09/deprecating-constants-and-classes-in-ruby for ideas on how to achieve this.</del>
|
25
|
+
|
26
|
+
4. Innkeeper::Database.process should be deprecated in favour of just passing a block to `switch`
|
27
|
+
5. Innkeeper::Database.switch should be renamed to switch! to indicate that using it on its own has side effects
|
28
|
+
|
29
|
+
6. Migrations right now can be a bit of a pain. Innkeeper currently migrates a single tenant completely up to date, then goes onto the next. If one of these
|
30
|
+
migrations fails on a tenant, the previous one does NOT get reverted and leaves you in an awkward state. Ideally we'd want to wrap all of the migrations in
|
31
|
+
a transaction so if one fails, the whole thing reverts. Once we can ensure an all-or-nothing approach to migrations, we can optimize the migration strategy
|
32
|
+
to not even iterate over the tenants if there are no migrations to run on public.
|
33
|
+
|
34
|
+
7. Innkeeper has be come one of the most popular/robust Multi-tenant gems for Rails, but it still doesn't work for everyone's use case. It's fairly limited in implementation to either schema based (ie postgresql schemas) or connection based. I'd like to abstract out these implementation details such that one could write a pluggable strategy for Innkeeper and choose it based on a config selection (something like `config.strategy = :schema`). The next implementation I'd like to see is a scoped based approach that uses a `tenant_id` scoping on all records for multi-tenancy. This is probably the most popular multi-tenant approach and is db independent and really the simplest mechanism for a type of multi-tenancy.
|
35
|
+
|
36
|
+
8. Right now excluded tables still live in all tenanted environments. This is basically because it doesn't matter if they're there, we always query from the public.
|
37
|
+
It's a bit of an annoyance though and confuses lots of people. I'd love to see only tenanted tables in the tenants and only excluded tables in the public tenant.
|
38
|
+
This will be hard because Rails uses public to generate schema.rb. One idea is to have an `excluded` schema that holds all the excluded models and the public can
|
39
|
+
maintain everything.
|
40
|
+
|
41
|
+
9. This one is pretty lofty, but I'd also like to abstract out the fact that Innkeeper uses ActiveRecord. With the new DataMapper coming out soon and other popular
|
42
|
+
DBMS's (ie. mongo, couch etc...), it'd be nice if Innkeeper could be the de-facto interface for multi-tenancy on these systems.
|
43
|
+
|
44
|
+
|
45
|
+
===================
|
46
|
+
|
47
|
+
Quick TODOs
|
48
|
+
|
49
|
+
1. `default_tenant` should be up to the adapter, not the Innkeeper class, deprecate `default_schema`
|
50
|
+
2. deprecation.rb rescues everything, we have a hard dependency on ActiveSupport so this is unnecessary
|
51
|
+
3.
|
data/innkeeper.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$: << File.expand_path("../lib", __FILE__)
|
3
|
+
require "innkeeper/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = %q{innkeeper}
|
7
|
+
s.version = Innkeeper::VERSION
|
8
|
+
|
9
|
+
s.authors = ["Mike Campbell"]
|
10
|
+
s.summary = %q{A Ruby gem for managing database multitenancy}
|
11
|
+
s.description = %q{Innkeeper allows Rack applications to deal with database multitenancy through ActiveRecord}
|
12
|
+
s.email = ["mike@ydd.io"]
|
13
|
+
s.files = `git ls-files`.split($/)
|
14
|
+
s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.homepage = %q{https://github.com/cpoms/innkeeper}
|
19
|
+
s.licenses = ["MIT"]
|
20
|
+
|
21
|
+
s.add_dependency 'activerecord', '>= 5.1.0'
|
22
|
+
s.add_dependency 'rack', '>= 1.3.6'
|
23
|
+
s.add_dependency 'public_suffix', '~> 2.0.5'
|
24
|
+
s.add_dependency 'parallel', '>= 0.7.1'
|
25
|
+
|
26
|
+
s.add_development_dependency 'appraisal'
|
27
|
+
s.add_development_dependency 'rake', '~> 0.9'
|
28
|
+
s.add_development_dependency 'capybara', '~> 2.0'
|
29
|
+
|
30
|
+
if defined?(JRUBY_VERSION)
|
31
|
+
s.add_development_dependency 'activerecord-jdbc-adapter'
|
32
|
+
s.add_development_dependency 'activerecord-jdbcpostgresql-adapter'
|
33
|
+
s.add_development_dependency 'activerecord-jdbcmysql-adapter'
|
34
|
+
s.add_development_dependency 'jdbc-postgres', '9.2.1002'
|
35
|
+
s.add_development_dependency 'jdbc-mysql'
|
36
|
+
s.add_development_dependency 'jruby-openssl'
|
37
|
+
else
|
38
|
+
s.add_development_dependency 'mysql2', '>= 0.3.10'
|
39
|
+
s.add_development_dependency 'pg', '>= 0.11.0'
|
40
|
+
s.add_development_dependency 'sqlite3'
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# You can have Innkeeper route to the appropriate Tenant by adding some Rack middleware.
|
2
|
+
# Innkeeper can support many different "Elevators" that can take care of this routing to your data.
|
3
|
+
# Require whichever Elevator you're using below or none if you have a custom one.
|
4
|
+
#
|
5
|
+
# require 'innkeeper/elevators/generic'
|
6
|
+
# require 'innkeeper/elevators/domain'
|
7
|
+
require 'innkeeper/elevators/subdomain'
|
8
|
+
# require 'innkeeper/resolvers/schema'
|
9
|
+
|
10
|
+
#
|
11
|
+
# Innkeeper Configuration
|
12
|
+
#
|
13
|
+
Innkeeper.configure do |config|
|
14
|
+
# Add any models that you do not want to be multi-tenanted, but remain in the global (public) namespace.
|
15
|
+
# A typical example would be a Customer or Tenant model that stores each Tenant's information.
|
16
|
+
#
|
17
|
+
# config.excluded_models = %w{ Tenant }
|
18
|
+
|
19
|
+
# In order to migrate all of your Tenants you need to provide a list of Tenant names to Innkeeper.
|
20
|
+
# You can make this dynamic by providing a Proc object to be called on migrations.
|
21
|
+
# This object should yield either:
|
22
|
+
# - an array of strings representing each Tenant name.
|
23
|
+
# - a hash which keys are tenant names, and values custom db config (must contain all key/values required in database.yml)
|
24
|
+
#
|
25
|
+
# config.tenant_names = lambda{ Customer.pluck(:tenant_name) }
|
26
|
+
# config.tenant_names = ['tenant1', 'tenant2']
|
27
|
+
# config.tenant_names = [
|
28
|
+
# {
|
29
|
+
# adapter: 'postgresql',
|
30
|
+
# host: 'some_server',
|
31
|
+
# port: 5555,
|
32
|
+
# database: 'postgres' # this is not the name of the tenant's db
|
33
|
+
# # but the name of the database to connect to before creating the tenant's db
|
34
|
+
# # mandatory in postgresql
|
35
|
+
# schema_search_path: '"tenant1"'
|
36
|
+
# },
|
37
|
+
# 'tenant2' => {
|
38
|
+
# adapter: 'postgresql',
|
39
|
+
# database: 'postgres' # this is not the name of the tenant's db
|
40
|
+
# # but the name of the database to connect to before creating the tenant's db
|
41
|
+
# # mandatory in postgresql
|
42
|
+
#
|
43
|
+
# }
|
44
|
+
# }
|
45
|
+
#
|
46
|
+
config.tenant_names = lambda { ToDo_Tenant_Or_User_Model.pluck :database }
|
47
|
+
|
48
|
+
# The tenant decorator setting should be a callable which receives the tenant
|
49
|
+
# as an argument, and returns the a modified version of the tenant name which
|
50
|
+
# you want to use in the resolver as a database or schema name, for example.
|
51
|
+
#
|
52
|
+
# A typical use-case might be prepending or appending the rails environment,
|
53
|
+
# as shown below.
|
54
|
+
#
|
55
|
+
# config.tenant_decorator = ->(tenant){ "#{Rails.env}_#{tenant}" }
|
56
|
+
|
57
|
+
# The resolver is used to convert a tenant name into a full spec. The two
|
58
|
+
# provided resolvers are Database and Schema. When you issue
|
59
|
+
# Innkeeper.switch("some_tenant"){ ... }, Innkeeper passes "some_tenant" to
|
60
|
+
# the selected resolver (after it's been decorated). The Database resolver
|
61
|
+
# takes the decorated tenant name, and inserts it into the :database key of a
|
62
|
+
# full connection specification (the full spec is whatever the database spec
|
63
|
+
# was at Innkeeper initialization. The schema resolver, does the same but
|
64
|
+
# for the :schema_search_path option in the configuration.
|
65
|
+
#
|
66
|
+
# config.tenant_resolver = Innkeeper::Resolvers::Schema
|
67
|
+
end
|
68
|
+
|
69
|
+
# Setup a custom Tenant switching middleware. The Proc should return the name of the Tenant that
|
70
|
+
# you want to switch to.
|
71
|
+
# Rails.application.config.middleware.use 'Innkeeper::Elevators::Generic', lambda { |request|
|
72
|
+
# request.host.split('.').first
|
73
|
+
# }
|
74
|
+
|
75
|
+
# Rails.application.config.middleware.use 'Innkeeper::Elevators::Domain'
|
76
|
+
Rails.application.config.middleware.use 'Innkeeper::Elevators::Subdomain'
|
data/lib/innkeeper.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'innkeeper/railtie' if defined?(Rails)
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
require 'forwardable'
|
4
|
+
require 'active_record'
|
5
|
+
require 'innkeeper/tenant'
|
6
|
+
|
7
|
+
module Innkeeper
|
8
|
+
class << self
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
ACCESSOR_METHODS = [
|
12
|
+
:use_sql, :seed_after_create, :tenant_decorator,
|
13
|
+
:force_reconnect_on_switch, :pool_per_config
|
14
|
+
]
|
15
|
+
WRITER_METHODS = [
|
16
|
+
:tenant_names, :database_schema_file, :excluded_models,
|
17
|
+
:persistent_schemas, :connection_class, :tld_length, :db_migrate_tenants,
|
18
|
+
:seed_data_file, :default_tenant, :parallel_migration_threads
|
19
|
+
]
|
20
|
+
OTHER_METHODS = [:tenant_resolver, :resolver_class]
|
21
|
+
|
22
|
+
attr_accessor(*ACCESSOR_METHODS)
|
23
|
+
attr_writer(*WRITER_METHODS)
|
24
|
+
|
25
|
+
def_delegators :connection_class, :connection, :connection_config,
|
26
|
+
:establish_connection, :connection_handler
|
27
|
+
|
28
|
+
def configure
|
29
|
+
yield self if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def tenant_resolver
|
33
|
+
@tenant_resolver ||= @resolver_class.new(connection_config)
|
34
|
+
end
|
35
|
+
|
36
|
+
def tenant_resolver=(resolver_class)
|
37
|
+
remove_instance_variable(:@tenant_resolver) if instance_variable_defined?(:@tenant_resolver)
|
38
|
+
@resolver_class = resolver_class
|
39
|
+
end
|
40
|
+
|
41
|
+
def tenant_names
|
42
|
+
@tenant_names.respond_to?(:call) ? @tenant_names.call : (@tenant_names || [])
|
43
|
+
end
|
44
|
+
|
45
|
+
def tenants_with_config
|
46
|
+
extract_tenant_config
|
47
|
+
end
|
48
|
+
|
49
|
+
# Whether or not db:migrate should also migrate tenants
|
50
|
+
# defaults to true
|
51
|
+
def db_migrate_tenants
|
52
|
+
return @db_migrate_tenants if defined?(@db_migrate_tenants)
|
53
|
+
|
54
|
+
@db_migrate_tenants = true
|
55
|
+
end
|
56
|
+
|
57
|
+
# Default to empty array
|
58
|
+
def excluded_models
|
59
|
+
@excluded_models || []
|
60
|
+
end
|
61
|
+
|
62
|
+
def default_tenant
|
63
|
+
@default_tenant || tenant_resolver.init_config
|
64
|
+
end
|
65
|
+
|
66
|
+
def parallel_migration_threads
|
67
|
+
@parallel_migration_threads || 0
|
68
|
+
end
|
69
|
+
|
70
|
+
def persistent_schemas
|
71
|
+
@persistent_schemas || []
|
72
|
+
end
|
73
|
+
|
74
|
+
def connection_class
|
75
|
+
@connection_class || ActiveRecord::Base
|
76
|
+
end
|
77
|
+
|
78
|
+
def database_schema_file
|
79
|
+
return @database_schema_file if defined?(@database_schema_file)
|
80
|
+
|
81
|
+
@database_schema_file = Rails.root.join('db', 'schema.rb')
|
82
|
+
end
|
83
|
+
|
84
|
+
def seed_data_file
|
85
|
+
return @seed_data_file if defined?(@seed_data_file)
|
86
|
+
|
87
|
+
@seed_data_file = Rails.root.join('db', 'seeds.rb')
|
88
|
+
end
|
89
|
+
|
90
|
+
def reset
|
91
|
+
(ACCESSOR_METHODS + WRITER_METHODS + OTHER_METHODS).each do |method|
|
92
|
+
remove_instance_variable(:"@#{method}") if instance_variable_defined?(:"@#{method}")
|
93
|
+
end
|
94
|
+
|
95
|
+
Thread.current[:_innkeeper_connection_specification_name] = nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Exceptions
|
100
|
+
InnkeeperError = Class.new(StandardError)
|
101
|
+
|
102
|
+
# Raised when innkeeper cannot find the adapter specified in <tt>config/database.yml</tt>
|
103
|
+
AdapterNotFound = Class.new(InnkeeperError)
|
104
|
+
|
105
|
+
# Tenant specified is unknown
|
106
|
+
TenantNotFound = Class.new(InnkeeperError)
|
107
|
+
|
108
|
+
# The Tenant attempting to be created already exists
|
109
|
+
TenantExists = Class.new(InnkeeperError)
|
110
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Innkeeper
|
2
|
+
module Adapters
|
3
|
+
class AbstractAdapter
|
4
|
+
include ActiveSupport::Callbacks
|
5
|
+
define_callbacks :create, :switch
|
6
|
+
|
7
|
+
attr_reader :current
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
reset
|
11
|
+
rescue Innkeeper::TenantNotFound
|
12
|
+
puts "WARN: Unable to connect to default tenant"
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset
|
16
|
+
switch!(Innkeeper.default_tenant)
|
17
|
+
end
|
18
|
+
|
19
|
+
def switch(tenant = nil)
|
20
|
+
previous_tenant = @current
|
21
|
+
switch!(tenant)
|
22
|
+
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
switch!(previous_tenant) rescue reset
|
26
|
+
end
|
27
|
+
|
28
|
+
def create(tenant)
|
29
|
+
run_callbacks :create do
|
30
|
+
begin
|
31
|
+
previous_tenant = @current
|
32
|
+
config = config_for(tenant)
|
33
|
+
difference = current_difference_from(config)
|
34
|
+
|
35
|
+
if difference[:host]
|
36
|
+
connection_switch!(config, without_keys: [:database, :schema_search_path])
|
37
|
+
end
|
38
|
+
|
39
|
+
create_tenant!(config)
|
40
|
+
simple_switch(config)
|
41
|
+
@current = tenant
|
42
|
+
|
43
|
+
import_database_schema
|
44
|
+
seed_data if Innkeeper.seed_after_create
|
45
|
+
|
46
|
+
yield if block_given?
|
47
|
+
ensure
|
48
|
+
switch!(previous_tenant) rescue reset
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def drop(tenant)
|
54
|
+
previous_tenant = @current
|
55
|
+
|
56
|
+
config = config_for(tenant)
|
57
|
+
difference = current_difference_from(config)
|
58
|
+
|
59
|
+
if difference[:host]
|
60
|
+
connection_switch!(config, without_keys: [:database])
|
61
|
+
end
|
62
|
+
|
63
|
+
unless database_exists?(config[:database])
|
64
|
+
raise TenantNotFound, "Error while dropping database #{config[:database]} for tenant #{tenant}"
|
65
|
+
end
|
66
|
+
|
67
|
+
Innkeeper.connection.drop_database(config[:database])
|
68
|
+
|
69
|
+
@current = tenant
|
70
|
+
ensure
|
71
|
+
switch!(previous_tenant) rescue reset
|
72
|
+
end
|
73
|
+
|
74
|
+
def switch!(tenant)
|
75
|
+
run_callbacks :switch do
|
76
|
+
return reset if tenant.nil?
|
77
|
+
|
78
|
+
config = config_for(tenant)
|
79
|
+
|
80
|
+
if Innkeeper.force_reconnect_on_switch
|
81
|
+
connection_switch!(config)
|
82
|
+
else
|
83
|
+
switch_tenant(config)
|
84
|
+
end
|
85
|
+
|
86
|
+
@current = tenant
|
87
|
+
|
88
|
+
Innkeeper.connection.clear_query_cache
|
89
|
+
|
90
|
+
tenant
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def config_for(tenant)
|
95
|
+
return tenant if tenant.is_a?(Hash)
|
96
|
+
|
97
|
+
decorated_tenant = decorate(tenant)
|
98
|
+
Innkeeper.tenant_resolver.resolve(decorated_tenant)
|
99
|
+
end
|
100
|
+
|
101
|
+
def decorate(tenant)
|
102
|
+
decorator = Innkeeper.tenant_decorator
|
103
|
+
decorator ? decorator.call(tenant) : tenant
|
104
|
+
end
|
105
|
+
|
106
|
+
def process_excluded_models
|
107
|
+
excluded_config = config_for(Innkeeper.default_tenant).merge(name: :_innkeeper_excluded)
|
108
|
+
Innkeeper.connection_handler.establish_connection(excluded_config)
|
109
|
+
|
110
|
+
Innkeeper.excluded_models.each do |excluded_model|
|
111
|
+
# user mustn't have overridden `connection_specification_name`
|
112
|
+
# cattr_accessor in model
|
113
|
+
excluded_model.constantize.connection_specification_name = :_innkeeper_excluded
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def setup_connection_specification_name
|
118
|
+
Innkeeper.connection_class.connection_specification_name = nil
|
119
|
+
Innkeeper.connection_class.instance_eval do
|
120
|
+
def connection_specification_name
|
121
|
+
if !defined?(@connection_specification_name) || @connection_specification_name.nil?
|
122
|
+
innkeeper_spec_name = Thread.current[:_innkeeper_connection_specification_name]
|
123
|
+
return innkeeper_spec_name ||
|
124
|
+
(self == ActiveRecord::Base ? "primary" : superclass.connection_specification_name)
|
125
|
+
end
|
126
|
+
@connection_specification_name
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def current_difference_from(config)
|
132
|
+
current_config = config_for(@current)
|
133
|
+
config.select{ |k, v| current_config[k] != v }
|
134
|
+
end
|
135
|
+
|
136
|
+
def connection_switch!(config, without_keys: [])
|
137
|
+
config = config.reject{ |k, _| without_keys.include?(k) }
|
138
|
+
|
139
|
+
config.merge!(name: connection_specification_name(config))
|
140
|
+
|
141
|
+
unless Innkeeper.connection_handler.retrieve_connection_pool(config[:name])
|
142
|
+
Innkeeper.connection_handler.establish_connection(config)
|
143
|
+
end
|
144
|
+
|
145
|
+
Thread.current[:_innkeeper_connection_specification_name] = config[:name]
|
146
|
+
simple_switch(config) if config[:database] || config[:schema_search_path]
|
147
|
+
end
|
148
|
+
|
149
|
+
def import_database_schema
|
150
|
+
ActiveRecord::Schema.verbose = false
|
151
|
+
|
152
|
+
load_or_abort(Innkeeper.database_schema_file) if Innkeeper.database_schema_file
|
153
|
+
end
|
154
|
+
|
155
|
+
def seed_data
|
156
|
+
silence_warnings{ load_or_abort(Innkeeper.seed_data_file) } if Innkeeper.seed_data_file
|
157
|
+
end
|
158
|
+
|
159
|
+
def load_or_abort(file)
|
160
|
+
if File.exist?(file)
|
161
|
+
load(file)
|
162
|
+
else
|
163
|
+
abort %{#{file} doesn't exist yet}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def raise_connect_error!(tenant, exception)
|
168
|
+
raise TenantNotFound, "Error while connecting to tenant #{tenant}: #{exception.message}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|