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
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'innkeeper/resolvers/abstract'
|
2
|
+
|
3
|
+
module Innkeeper
|
4
|
+
module Resolvers
|
5
|
+
class Schema < Abstract
|
6
|
+
def resolve(tenant)
|
7
|
+
schemas = [tenant, Innkeeper.persistent_schemas].flatten
|
8
|
+
search_path = schemas.map(&:inspect).join(", ")
|
9
|
+
|
10
|
+
init_config.dup.tap{ |c| c[:schema_search_path] = search_path }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Require this file to append Innkeeper rake tasks to ActiveRecord db rake tasks
|
2
|
+
# Enabled by default in the initializer
|
3
|
+
|
4
|
+
module Innkeeper
|
5
|
+
class RakeTaskEnhancer
|
6
|
+
|
7
|
+
TASKS = %w(db:migrate db:rollback db:migrate:up db:migrate:down db:migrate:redo db:seed)
|
8
|
+
|
9
|
+
# This is a bit convoluted, but helps solve problems when using Innkeeper within an engine
|
10
|
+
# See spec/integration/use_within_an_engine.rb
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def enhance!
|
14
|
+
TASKS.each do |name|
|
15
|
+
task = Rake::Task[name]
|
16
|
+
task.enhance do
|
17
|
+
if should_enhance?
|
18
|
+
enhance_task(task)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def should_enhance?
|
25
|
+
Innkeeper.db_migrate_tenants
|
26
|
+
end
|
27
|
+
|
28
|
+
def enhance_task(task)
|
29
|
+
Rake::Task[task.name.sub(/db:/, 'innkeeper:')].invoke
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
Innkeeper::RakeTaskEnhancer.enhance!
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Innkeeper
|
4
|
+
# The main entry point to Innkeeper functions
|
5
|
+
#
|
6
|
+
module Tenant
|
7
|
+
|
8
|
+
extend self
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def_delegators :adapter, :create, :drop, :drop_schema, :switch, :switch!,
|
12
|
+
:current, :each, :reset, :set_callback, :seed, :current_tenant,
|
13
|
+
:default_tenant, :config_for
|
14
|
+
|
15
|
+
# Initialize Innkeeper config options such as excluded_models
|
16
|
+
#
|
17
|
+
def init
|
18
|
+
adapter.setup_connection_specification_name
|
19
|
+
adapter.process_excluded_models
|
20
|
+
end
|
21
|
+
|
22
|
+
# Fetch the proper multi-tenant adapter based on Rails config
|
23
|
+
#
|
24
|
+
# @return {subclass of Innkeeper::AbstractAdapter}
|
25
|
+
#
|
26
|
+
def adapter
|
27
|
+
Thread.current[:innkeeper_adapter] ||= begin
|
28
|
+
config = Innkeeper.default_tenant
|
29
|
+
|
30
|
+
adapter_name = "#{config[:adapter]}_adapter"
|
31
|
+
|
32
|
+
begin
|
33
|
+
require "innkeeper/adapters/#{adapter_name}"
|
34
|
+
adapter_class = Adapters.const_get(adapter_name.classify)
|
35
|
+
rescue LoadError, NameError
|
36
|
+
raise AdapterNotFound, "The adapter `#{adapter_name}` is not yet supported"
|
37
|
+
end
|
38
|
+
|
39
|
+
adapter_class.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def reload!
|
44
|
+
Thread.current[:innkeeper_adapter] = nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'innkeeper/migrator'
|
2
|
+
require 'parallel'
|
3
|
+
|
4
|
+
innkeeper_namespace = namespace :innkeeper do
|
5
|
+
|
6
|
+
desc "Create all tenants"
|
7
|
+
task create: 'db:migrate' do
|
8
|
+
tenants.each do |tenant|
|
9
|
+
begin
|
10
|
+
quietly { Innkeeper::Tenant.create(tenant) }
|
11
|
+
rescue Innkeeper::TenantExists => e
|
12
|
+
puts e.message
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Migrate all tenants"
|
18
|
+
task :migrate do
|
19
|
+
warn_if_tenants_empty
|
20
|
+
|
21
|
+
each_tenant do |tenant|
|
22
|
+
begin
|
23
|
+
Innkeeper::Migrator.migrate tenant
|
24
|
+
rescue Innkeeper::TenantNotFound => e
|
25
|
+
puts e.message
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Seed all tenants"
|
31
|
+
task :seed do
|
32
|
+
warn_if_tenants_empty
|
33
|
+
|
34
|
+
each_tenant do |tenant|
|
35
|
+
begin
|
36
|
+
Innkeeper::Tenant.switch(tenant) do
|
37
|
+
Innkeeper::Tenant.seed
|
38
|
+
end
|
39
|
+
rescue Innkeeper::TenantNotFound => e
|
40
|
+
puts e.message
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Rolls the migration back to the previous version (specify steps w/ STEP=n) across all tenants."
|
46
|
+
task :rollback do
|
47
|
+
warn_if_tenants_empty
|
48
|
+
|
49
|
+
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
|
50
|
+
|
51
|
+
each_tenant do |tenant|
|
52
|
+
begin
|
53
|
+
Innkeeper::Migrator.rollback tenant, step
|
54
|
+
rescue Innkeeper::TenantNotFound => e
|
55
|
+
puts e.message
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
namespace :migrate do
|
61
|
+
desc 'Runs the "up" for a given migration VERSION across all tenants.'
|
62
|
+
task :up do
|
63
|
+
warn_if_tenants_empty
|
64
|
+
|
65
|
+
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
|
66
|
+
raise 'VERSION is required' unless version
|
67
|
+
|
68
|
+
each_tenant do |tenant|
|
69
|
+
begin
|
70
|
+
Innkeeper::Migrator.run :up, tenant, version
|
71
|
+
rescue Innkeeper::TenantNotFound => e
|
72
|
+
puts e.message
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
desc 'Runs the "down" for a given migration VERSION across all tenants.'
|
78
|
+
task :down do
|
79
|
+
warn_if_tenants_empty
|
80
|
+
|
81
|
+
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
|
82
|
+
raise 'VERSION is required' unless version
|
83
|
+
|
84
|
+
each_tenant do |tenant|
|
85
|
+
begin
|
86
|
+
Innkeeper::Migrator.run :down, tenant, version
|
87
|
+
rescue Innkeeper::TenantNotFound => e
|
88
|
+
puts e.message
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
desc 'Rolls back the tenant one migration and re migrate up (options: STEP=x, VERSION=x).'
|
94
|
+
task :redo do
|
95
|
+
if ENV['VERSION']
|
96
|
+
innkeeper_namespace['migrate:down'].invoke
|
97
|
+
innkeeper_namespace['migrate:up'].invoke
|
98
|
+
else
|
99
|
+
innkeeper_namespace['rollback'].invoke
|
100
|
+
innkeeper_namespace['migrate'].invoke
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def each_tenant(&block)
|
106
|
+
Parallel.each(tenants, in_threads: Innkeeper.parallel_migration_threads) do |tenant|
|
107
|
+
block.call(tenant)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def tenants
|
112
|
+
ENV['DB'] ? ENV['DB'].split(',').map { |s| s.strip } : Innkeeper.tenant_names || []
|
113
|
+
end
|
114
|
+
|
115
|
+
def warn_if_tenants_empty
|
116
|
+
if tenants.empty?
|
117
|
+
puts <<-WARNING
|
118
|
+
[WARNING] - The list of tenants to migrate appears to be empty. This could mean a few things:
|
119
|
+
|
120
|
+
1. You may not have created any, in which case you can ignore this message
|
121
|
+
2. You've run `innkeeper:migrate` directly without loading the Rails environment
|
122
|
+
* `innkeeper:migrate` is now deprecated. Tenants will automatically be migrated with `db:migrate`
|
123
|
+
|
124
|
+
Note that your tenants currently haven't been migrated. You'll need to run `db:migrate` to rectify this.
|
125
|
+
WARNING
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/notes.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Notes
|
2
|
+
|
3
|
+
- API:
|
4
|
+
|
5
|
+
Innkeeper::Tenant.switch("blah") do ...
|
6
|
+
-> - pass "blah" into `config_for`
|
7
|
+
- `config_for` uses the configured resolver to return a config hash
|
8
|
+
(either database or schema by default)
|
9
|
+
- connect_to_new determines what's different in the config with the
|
10
|
+
current
|
11
|
+
- if a 'local' switch is possible (e.g. host is unchanged), do that,
|
12
|
+
otherwise reconnect
|
13
|
+
Innkeeper::Tenant.switch({ host: etc }) do ...
|
14
|
+
-> - pass straight through to connection_handler
|
15
|
+
|
16
|
+
|
17
|
+
Innkeeper.configure do |config|
|
18
|
+
config.tenants = proc{ Customer.map(&:subdomain) }
|
19
|
+
config.tenant_decorator = ->(tenant){ "#{Rails.env}_#{tenant}" }
|
20
|
+
config.tenant_resolver = Resolvers::Database
|
21
|
+
end
|
22
|
+
|
23
|
+
## Todo
|
24
|
+
|
25
|
+
- rewrite generator
|
26
|
+
- finish config tests (tenant resolver specifically)
|
27
|
+
- write multi-threading tests
|
28
|
+
- remove deprecated silencers?
|
29
|
+
- rewrite readme
|
30
|
+
|
31
|
+
- what happens when host is switched in pg without a database specified?
|
data/test/config_test.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class ConfigTest < Minitest::Test
|
4
|
+
def teardown
|
5
|
+
Innkeeper.reset
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_configure_yields_innkeeper
|
9
|
+
Innkeeper.configure{ |config| assert_equal Innkeeper, config }
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_setting_excluded_models
|
13
|
+
Innkeeper.configure{ |c| c.excluded_models = ["Company"] }
|
14
|
+
|
15
|
+
assert_equal ["Company"], Innkeeper.excluded_models
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_setting_force_reconnect_on_switch
|
19
|
+
Innkeeper.configure{ |c| c.force_reconnect_on_switch = true }
|
20
|
+
|
21
|
+
assert_equal true, Innkeeper.force_reconnect_on_switch
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_setting_seed_data_file
|
25
|
+
Innkeeper.configure{ |c| c.seed_data_file = "#{Rails.root}/db/seeds/import.rb" }
|
26
|
+
|
27
|
+
assert_equal "#{Rails.root}/db/seeds/import.rb", Innkeeper.seed_data_file
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_setting_seed_after_create
|
31
|
+
Innkeeper.configure{ |c| c.seed_after_create = true }
|
32
|
+
|
33
|
+
assert_equal true, Innkeeper.seed_after_create
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_setting_tenant_names_to_array
|
37
|
+
Innkeeper.configure{ |c| c.tenant_names = ['tenant_a', 'tenant_b'] }
|
38
|
+
|
39
|
+
assert_equal ['tenant_a', 'tenant_b'], Innkeeper.tenant_names
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_setting_tenant_names_to_proc
|
43
|
+
tenant_names = ["foo", "bar"]
|
44
|
+
tenant_names.each{ |db| Company.create!(database: db) }
|
45
|
+
|
46
|
+
Innkeeper.configure{ |c| c.tenant_names = ->{ Company.pluck(:database) } }
|
47
|
+
|
48
|
+
assert_equal tenant_names, Innkeeper.tenant_names
|
49
|
+
ensure
|
50
|
+
Company.delete_all
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
<% if defined?(JRUBY_VERSION) %>
|
2
|
+
connections:
|
3
|
+
postgresql:
|
4
|
+
adapter: postgresql
|
5
|
+
database: innkeeper_postgresql_test
|
6
|
+
username: postgres
|
7
|
+
min_messages: WARNING
|
8
|
+
driver: org.postgresql.Driver
|
9
|
+
url: jdbc:postgresql://localhost:5432/innkeeper_postgresql_test
|
10
|
+
timeout: 5000
|
11
|
+
pool: 5
|
12
|
+
|
13
|
+
mysql:
|
14
|
+
adapter: mysql
|
15
|
+
database: innkeeper_mysql_test
|
16
|
+
username: root
|
17
|
+
min_messages: WARNING
|
18
|
+
driver: com.mysql.jdbc.Driver
|
19
|
+
url: jdbc:mysql://localhost:3306/innkeeper_mysql_test
|
20
|
+
timeout: 5000
|
21
|
+
pool: 5
|
22
|
+
<% else %>
|
23
|
+
connections:
|
24
|
+
postgresql:
|
25
|
+
adapter: postgresql
|
26
|
+
database: innkeeper_postgresql_test
|
27
|
+
min_messages: WARNING
|
28
|
+
username: postgres
|
29
|
+
schema_search_path: public
|
30
|
+
password:
|
31
|
+
|
32
|
+
mysql:
|
33
|
+
adapter: mysql2
|
34
|
+
database: innkeeper_mysql_test
|
35
|
+
username: root
|
36
|
+
password:
|
37
|
+
<% end %>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'innkeeper/resolvers/database'
|
3
|
+
|
4
|
+
class DecoratorTest < Innkeeper::Test
|
5
|
+
def setup
|
6
|
+
setup_connection("mysql")
|
7
|
+
|
8
|
+
Innkeeper.configure do |config|
|
9
|
+
config.tenant_resolver = Innkeeper::Resolvers::Database
|
10
|
+
config.tenant_decorator = ->(tenant){ "#{Rails.env}_#{tenant}" }
|
11
|
+
end
|
12
|
+
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_decorator_proc
|
17
|
+
decorated = Innkeeper::Tenant.adapter.decorate("foobar")
|
18
|
+
|
19
|
+
assert_equal "test_foobar", decorated
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require_relative 'mocks/adapter_mock'
|
3
|
+
require 'innkeeper/elevators/domain'
|
4
|
+
|
5
|
+
class DomainElevatorTest < Minitest::Test
|
6
|
+
include AdapterMock
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@elevator = Innkeeper::Elevators::Domain.new(Proc.new{})
|
10
|
+
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_parsing_host_for_domain_name
|
15
|
+
request = ActionDispatch::Request.new('HTTP_HOST' => 'example.com')
|
16
|
+
assert_equal 'example', @elevator.parse_tenant_name(request)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_www_prefix_and_domain_suffix_ignored
|
20
|
+
request = ActionDispatch::Request.new('HTTP_HOST' => 'www.example.bc.ca')
|
21
|
+
assert_equal 'example', @elevator.parse_tenant_name(request)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_no_host_returns_nil
|
25
|
+
request = ActionDispatch::Request.new('HTTP_HOST' => '')
|
26
|
+
assert_nil @elevator.parse_tenant_name(request)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_call_switches_tenant
|
30
|
+
with_adapter_mocked do |adapter|
|
31
|
+
adapter.expect :switch, true, ['example']
|
32
|
+
|
33
|
+
@elevator.call('HTTP_HOST' => 'www.example.com')
|
34
|
+
|
35
|
+
assert adapter.verify
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/test/dummy/Rakefile
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
2
|
+
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
3
|
+
|
4
|
+
require File.expand_path('../config/application', __FILE__)
|
5
|
+
require 'rake'
|
6
|
+
|
7
|
+
Dummy::Application.load_tasks
|