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,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,3 @@
1
+ module Innkeeper
2
+ VERSION = "0.1.0"
3
+ 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
@@ -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?
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery
3
+
4
+ def index
5
+ end
6
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,3 @@
1
+ class Company < ActiveRecord::Base
2
+ # Dummy models
3
+ end
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ # Dummy models
3
+ end