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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.pryrc +3 -0
  4. data/.travis.yml +15 -0
  5. data/Appraisals +20 -0
  6. data/Gemfile +10 -0
  7. data/Guardfile +24 -0
  8. data/HISTORY.md +337 -0
  9. data/README.md +485 -0
  10. data/Rakefile +92 -0
  11. data/TODO.md +51 -0
  12. data/gemfiles/rails_5_1.gemfile +12 -0
  13. data/innkeeper.gemspec +42 -0
  14. data/lib/generators/innkeeper/install/USAGE +5 -0
  15. data/lib/generators/innkeeper/install/install_generator.rb +10 -0
  16. data/lib/generators/innkeeper/install/templates/innkeeper.rb +76 -0
  17. data/lib/innkeeper.rb +110 -0
  18. data/lib/innkeeper/adapters/abstract_adapter.rb +172 -0
  19. data/lib/innkeeper/adapters/mysql2_adapter.rb +47 -0
  20. data/lib/innkeeper/adapters/postgresql_adapter.rb +112 -0
  21. data/lib/innkeeper/console.rb +12 -0
  22. data/lib/innkeeper/deprecation.rb +10 -0
  23. data/lib/innkeeper/elevators/domain.rb +20 -0
  24. data/lib/innkeeper/elevators/generic.rb +32 -0
  25. data/lib/innkeeper/elevators/host_hash.rb +22 -0
  26. data/lib/innkeeper/elevators/subdomain.rb +62 -0
  27. data/lib/innkeeper/migrator.rb +33 -0
  28. data/lib/innkeeper/railtie.rb +56 -0
  29. data/lib/innkeeper/resolvers/abstract.rb +15 -0
  30. data/lib/innkeeper/resolvers/database.rb +11 -0
  31. data/lib/innkeeper/resolvers/schema.rb +14 -0
  32. data/lib/innkeeper/tasks/enhancements.rb +36 -0
  33. data/lib/innkeeper/tenant.rb +47 -0
  34. data/lib/innkeeper/version.rb +3 -0
  35. data/lib/tasks/innkeeper.rake +128 -0
  36. data/notes.md +31 -0
  37. data/test/config_test.rb +52 -0
  38. data/test/databases.yml.sample +37 -0
  39. data/test/decorator_test.rb +21 -0
  40. data/test/domain_elevator_test.rb +38 -0
  41. data/test/dummy/Rakefile +7 -0
  42. data/test/dummy/app/controllers/application_controller.rb +6 -0
  43. data/test/dummy/app/helpers/application_helper.rb +2 -0
  44. data/test/dummy/app/models/company.rb +3 -0
  45. data/test/dummy/app/models/user.rb +3 -0
  46. data/test/dummy/app/views/application/index.html.erb +1 -0
  47. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  48. data/test/dummy/config.ru +4 -0
  49. data/test/dummy/config/application.rb +49 -0
  50. data/test/dummy/config/boot.rb +11 -0
  51. data/test/dummy/config/database.yml.sample +38 -0
  52. data/test/dummy/config/environment.rb +5 -0
  53. data/test/dummy/config/environments/development.rb +27 -0
  54. data/test/dummy/config/environments/production.rb +51 -0
  55. data/test/dummy/config/environments/test.rb +34 -0
  56. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  57. data/test/dummy/config/initializers/inflections.rb +10 -0
  58. data/test/dummy/config/initializers/innkeeper.rb +4 -0
  59. data/test/dummy/config/initializers/mime_types.rb +5 -0
  60. data/test/dummy/config/initializers/secret_token.rb +7 -0
  61. data/test/dummy/config/initializers/session_store.rb +8 -0
  62. data/test/dummy/config/locales/en.yml +5 -0
  63. data/test/dummy/config/routes.rb +3 -0
  64. data/test/dummy/db/schema.rb +19 -0
  65. data/test/dummy/db/seeds.rb +5 -0
  66. data/test/dummy/db/seeds/import.rb +5 -0
  67. data/test/dummy/public/404.html +26 -0
  68. data/test/dummy/public/422.html +26 -0
  69. data/test/dummy/public/500.html +26 -0
  70. data/test/dummy/public/favicon.ico +0 -0
  71. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  72. data/test/dummy/script/rails +6 -0
  73. data/test/excluded_models_test.rb +32 -0
  74. data/test/generic_elevator_test.rb +63 -0
  75. data/test/host_hash_elevator_test.rb +42 -0
  76. data/test/innkeeper_test.rb +96 -0
  77. data/test/mocks/adapter_mock.rb +11 -0
  78. data/test/multithreading_test.rb +37 -0
  79. data/test/mysql2_adapter_test.rb +17 -0
  80. data/test/postgresql_adapter_test.rb +39 -0
  81. data/test/railtie_test.rb +31 -0
  82. data/test/rake_task_test.rb +57 -0
  83. data/test/resolver_test.rb +21 -0
  84. data/test/shared/shared_adapter_tests.rb +95 -0
  85. data/test/subdomain_elevator_test.rb +75 -0
  86. data/test/test_helper.rb +24 -0
  87. metadata +325 -0
@@ -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.
@@ -0,0 +1,12 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 5.1.0"
6
+
7
+ group :local do
8
+ gem "pry"
9
+ gem "guard-rspec", "~> 4.2"
10
+ end
11
+
12
+ gemspec :path => "../"
@@ -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,5 @@
1
+ Description:
2
+ Creates an initializer for innkeeper.
3
+
4
+ Example:
5
+ `rails generate innkeeper:install`
@@ -0,0 +1,10 @@
1
+ module Innkeeper
2
+ class InstallGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('../templates', __FILE__)
4
+
5
+ def copy_files
6
+ template "innkeeper.rb", File.join("config", "initializers", "innkeeper.rb")
7
+ end
8
+
9
+ end
10
+ 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'
@@ -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