gorynich 1.1.0.142046
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +414 -0
- data/Rakefile +20 -0
- data/config/locales/en.yml +6 -0
- data/lib/generators/gorynich/gorynich_generator.rb +9 -0
- data/lib/generators/gorynich/install_generator.rb +17 -0
- data/lib/generators/templates/gorynich.rb +32 -0
- data/lib/gorynich/config.rb +241 -0
- data/lib/gorynich/configuration.rb +17 -0
- data/lib/gorynich/current.rb +13 -0
- data/lib/gorynich/engine.rb +11 -0
- data/lib/gorynich/fetcher.rb +54 -0
- data/lib/gorynich/fetchers/consul.rb +19 -0
- data/lib/gorynich/fetchers/file.rb +22 -0
- data/lib/gorynich/head/action_cable.rb +44 -0
- data/lib/gorynich/head/active_job.rb +36 -0
- data/lib/gorynich/head/active_record.rb +46 -0
- data/lib/gorynich/head/delayed_job.rb +22 -0
- data/lib/gorynich/head/rack_middleware.rb +34 -0
- data/lib/gorynich/head.rb +9 -0
- data/lib/gorynich/switcher.rb +48 -0
- data/lib/gorynich/version.rb +3 -0
- data/lib/gorynich.rb +137 -0
- data/lib/tasks/gorynich_tasks.rake +27 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +3 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/controllers/users_controller.rb +10 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/javascript/packs/application.js +15 -0
- data/spec/dummy/app/jobs/application_job.rb +7 -0
- data/spec/dummy/app/jobs/test_job.rb +6 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/user.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/app/views/users/index.html.erb +9 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +33 -0
- data/spec/dummy/config/application.rb +34 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/database.yml +3 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +46 -0
- data/spec/dummy/config/environments/production.rb +112 -0
- data/spec/dummy/config/environments/test.rb +49 -0
- data/spec/dummy/config/gorynich_config.yml +49 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/assets.rb +12 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/content_security_policy.rb +28 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/gorynich.rb +34 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/middleware.rb +0 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/puma.rb +38 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/db/migrate/20220907101031_create_users.rb +8 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/tmp/development_secret.txt +1 -0
- data/spec/fixtures/env_test_database.yml +7 -0
- data/spec/fixtures/fetchers/consul_config.yml +1 -0
- data/spec/fixtures/fetchers/file_config.yml +46 -0
- data/spec/fixtures/fetchers/wrong_file_config.yml +44 -0
- data/spec/fixtures/test_database.yml +1 -0
- data/spec/lib/gorynich/config_spec.rb +428 -0
- data/spec/lib/gorynich/fetcher_spec.rb +112 -0
- data/spec/lib/gorynich/fetchers/consul_spec.rb +58 -0
- data/spec/lib/gorynich/fetchers/file_spec.rb +29 -0
- data/spec/lib/gorynich/version_spec.rb +6 -0
- data/spec/lib/gorynich_spec.rb +99 -0
- data/spec/rails_helper.rb +8 -0
- data/spec/spec_helper.rb +48 -0
- metadata +458 -0
@@ -0,0 +1,241 @@
|
|
1
|
+
module Gorynich
|
2
|
+
class Config
|
3
|
+
attr_reader :fetcher,
|
4
|
+
:tenants,
|
5
|
+
:databases,
|
6
|
+
:hosts,
|
7
|
+
:uris,
|
8
|
+
:default
|
9
|
+
|
10
|
+
#
|
11
|
+
# Create instance of config
|
12
|
+
#
|
13
|
+
# @param [Fetcher] fetcher data loader
|
14
|
+
#
|
15
|
+
def initialize(**opts)
|
16
|
+
@default = 'default'
|
17
|
+
@fetcher = opts.fetch(:fetcher, Fetcher.new)
|
18
|
+
@mx = Mutex.new
|
19
|
+
|
20
|
+
actualize
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Update configs from data source
|
25
|
+
#
|
26
|
+
def actualize
|
27
|
+
cfg = fetcher.fetch.fetch(Rails.env)
|
28
|
+
|
29
|
+
@mx.synchronize do
|
30
|
+
@tenants = tenants_from_config(cfg)
|
31
|
+
@databases = databases_from_config(cfg)
|
32
|
+
@secrets = secrets_from_config(cfg)
|
33
|
+
@uris = uris_from_config(@secrets)
|
34
|
+
@hosts = hosts_from_config(@secrets)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Database config
|
40
|
+
#
|
41
|
+
# @param [String, Symbol] tenant
|
42
|
+
#
|
43
|
+
# @return [Hash]
|
44
|
+
#
|
45
|
+
def database(tenant)
|
46
|
+
databases.fetch(tenant.to_s)
|
47
|
+
rescue StandardError
|
48
|
+
raise TenantNotFound, tenant
|
49
|
+
end
|
50
|
+
|
51
|
+
%i[uris hosts secrets].each do |name|
|
52
|
+
define_method(name) do |tenant = nil|
|
53
|
+
values = instance_variable_get("@#{name}")
|
54
|
+
return values if tenant.nil?
|
55
|
+
|
56
|
+
values.fetch(tenant.to_s)
|
57
|
+
rescue StandardError
|
58
|
+
raise TenantNotFound, tenant
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Find tenant by URI
|
64
|
+
#
|
65
|
+
# @param [String] uri
|
66
|
+
#
|
67
|
+
# @return [String]
|
68
|
+
#
|
69
|
+
def tenant_by_uri(uri)
|
70
|
+
uri = URI(uri)
|
71
|
+
search_tenant = uris.select do |tenant, tenant_uris|
|
72
|
+
tenant if tenant_uris.map { |t_uri| URI(t_uri) }.include?(uri)
|
73
|
+
end.keys.first
|
74
|
+
|
75
|
+
raise UriNotFound, uri.host if search_tenant.nil?
|
76
|
+
|
77
|
+
search_tenant
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Find tenant by host
|
82
|
+
#
|
83
|
+
# @param [String] host
|
84
|
+
#
|
85
|
+
# @return [String]
|
86
|
+
#
|
87
|
+
def tenant_by_host(host)
|
88
|
+
tenant = hosts.select { |t, h| t if h.include?(host) }.keys.first
|
89
|
+
raise HostNotFound, host if tenant.nil?
|
90
|
+
|
91
|
+
tenant
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Find URI by host
|
96
|
+
#
|
97
|
+
# @param [String] host
|
98
|
+
# @param [String, Symbol] tenant tenant of config (optional)
|
99
|
+
#
|
100
|
+
# @return [String]
|
101
|
+
#
|
102
|
+
def uri_by_host(host, tenant = nil)
|
103
|
+
tenant ||= tenant_by_host(host)
|
104
|
+
tenant_uris = uris(tenant)
|
105
|
+
search_uri = tenant_uris.select { |uri| uri.include?(host) }.first
|
106
|
+
|
107
|
+
raise UriNotFound, search_uri.host if search_uri.nil?
|
108
|
+
|
109
|
+
search_uri
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Full config from data source by tenant
|
114
|
+
#
|
115
|
+
# @param [String, Symbol] tenant
|
116
|
+
#
|
117
|
+
# @return [Hash]
|
118
|
+
#
|
119
|
+
def config(tenant)
|
120
|
+
{
|
121
|
+
tenant: tenant.to_s,
|
122
|
+
database: database(tenant),
|
123
|
+
secrets: secrets(tenant)
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Database config for database.yml
|
129
|
+
#
|
130
|
+
# @param [String] env enviroment
|
131
|
+
# @param [Boolean] with_ignore ignore if there is no configuration for the environment
|
132
|
+
#
|
133
|
+
# @return [String] yaml result
|
134
|
+
#
|
135
|
+
def database_config(env = nil, fail_ignore: false)
|
136
|
+
envs = Dir.glob(Rails.root.join('config/environments/*.rb').to_s).map { |f| File.basename(f, '.rb') }
|
137
|
+
cfg = fetcher.fetch.extract!(*envs)
|
138
|
+
|
139
|
+
result =
|
140
|
+
if env.nil?
|
141
|
+
cfg.to_h do |cfg_env, tenant_cfg|
|
142
|
+
[
|
143
|
+
cfg_env,
|
144
|
+
configs_sort(tenant_cfg).to_h { |t, c| [t, c.fetch('db_config')] }
|
145
|
+
]
|
146
|
+
end
|
147
|
+
else
|
148
|
+
if fail_ignore && cfg.fetch(env, nil).nil?
|
149
|
+
{ env => nil}
|
150
|
+
else
|
151
|
+
{
|
152
|
+
env => configs_sort(cfg.fetch(env)).to_h { |t, c| [t, c.fetch('db_config')] }
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
result.to_yaml.gsub('---', '')
|
158
|
+
end
|
159
|
+
|
160
|
+
#
|
161
|
+
# For connection to ActiveRecord
|
162
|
+
#
|
163
|
+
# @return [Hash]
|
164
|
+
#
|
165
|
+
def connects_to_config
|
166
|
+
actualize
|
167
|
+
tenants.each_with_object({ default: :default }) do |tenant, cfg|
|
168
|
+
cfg[tenant.to_sym] = tenant.to_sym
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def databases_from_config(cfg)
|
175
|
+
cfg.to_h { |tenant, config| [tenant, config.fetch('db_config')] }
|
176
|
+
end
|
177
|
+
|
178
|
+
def tenants_from_config(cfg)
|
179
|
+
available_tenants = cfg.keys
|
180
|
+
raise TenantNotFound, default unless available_tenants.include?(default)
|
181
|
+
|
182
|
+
available_tenants
|
183
|
+
end
|
184
|
+
|
185
|
+
def secrets_from_config(cfg)
|
186
|
+
cfg.to_h { |tenant, config| [tenant, config.fetch('secrets', {})] }
|
187
|
+
end
|
188
|
+
|
189
|
+
def uris_from_config(secrets_by_tenant)
|
190
|
+
secrets_by_tenant.to_h { |tenant, secrets| [tenant, processed_uris(secrets)] }
|
191
|
+
end
|
192
|
+
|
193
|
+
def hosts_from_config(secrets_by_tenant)
|
194
|
+
secrets_by_tenant.to_h do |tenant, secrets|
|
195
|
+
hosts = processed_uris(secrets).map { |uri| URI(uri).host }
|
196
|
+
[tenant, hosts]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
#
|
201
|
+
# Make default database first in the list
|
202
|
+
#
|
203
|
+
# @param [Hash] cfg config of enviroment
|
204
|
+
#
|
205
|
+
# @return [Hash]
|
206
|
+
#
|
207
|
+
def configs_sort(cfg)
|
208
|
+
sorted_config = { 'default' => cfg['default'] }
|
209
|
+
sorted_config.merge!(cfg)
|
210
|
+
end
|
211
|
+
|
212
|
+
# consul KV can store only array into json string, so need to parse
|
213
|
+
def processed_uris(secrets)
|
214
|
+
secrets_uris = secrets.fetch('uris', [])
|
215
|
+
is_array = secrets_uris.is_a?(Array)
|
216
|
+
return secrets_uris if is_array && secrets_uris.empty?
|
217
|
+
|
218
|
+
if is_array
|
219
|
+
validate_uris(secrets_uris)
|
220
|
+
return secrets_uris
|
221
|
+
end
|
222
|
+
|
223
|
+
secrets_uris = JSON.parse(secrets_uris)
|
224
|
+
validate_uris(secrets_uris)
|
225
|
+
secrets_uris
|
226
|
+
rescue JSON::ParserError => e
|
227
|
+
raise ConfigError, 'URI parse error. Must be an array of URI'
|
228
|
+
end
|
229
|
+
|
230
|
+
def valid_url?(uri)
|
231
|
+
uri = URI.parse(uri)
|
232
|
+
!!uri.host && !!uri.scheme
|
233
|
+
rescue URI::InvalidURIError
|
234
|
+
false
|
235
|
+
end
|
236
|
+
|
237
|
+
def validate_uris(secrets_uris)
|
238
|
+
secrets_uris.each { |uri| raise ConfigError, 'URI parse error. Must be an array of URI' unless valid_url?(uri) }
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Gorynich
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :cache,
|
4
|
+
:fetcher,
|
5
|
+
:namespace,
|
6
|
+
:cache_expiration,
|
7
|
+
:rack_env_handler
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@cache = ActiveSupport::Cache::NullStore.new
|
11
|
+
@fetcher = nil
|
12
|
+
@namespace = nil
|
13
|
+
@cache_expiration = 30
|
14
|
+
@rack_env_handler = nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Gorynich
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace Gorynich
|
4
|
+
|
5
|
+
initializer 'gorynich.add_middleware' do |app|
|
6
|
+
app.middleware.insert_after ActionDispatch::RemoteIp, Gorynich::Head::RackMiddleware
|
7
|
+
app.config.active_record.writing_role = :default
|
8
|
+
app.config.hosts.clear
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative 'fetchers/file'
|
2
|
+
require_relative 'fetchers/consul'
|
3
|
+
|
4
|
+
module Gorynich
|
5
|
+
class Fetcher
|
6
|
+
attr_reader :fetcher, :namespace, :cache_expiration
|
7
|
+
|
8
|
+
#
|
9
|
+
# Create instance of fetcher
|
10
|
+
#
|
11
|
+
# @param [Object] fetcher data source
|
12
|
+
# @param [String, Symbol] namespace cache namespace
|
13
|
+
# @param [Integer] cache_expiration how long your cache will be alive
|
14
|
+
#
|
15
|
+
def initialize(fetcher: nil, namespace: nil, cache_expiration: nil)
|
16
|
+
@fetcher = fetcher || Gorynich.configuration.fetcher
|
17
|
+
@namespace = namespace || Gorynich.configuration.namespace
|
18
|
+
@cache_expiration = cache_expiration || Gorynich.configuration.cache_expiration
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Load data from source
|
23
|
+
#
|
24
|
+
# @return [Hash]
|
25
|
+
#
|
26
|
+
def fetch
|
27
|
+
cfg = Gorynich.configuration.cache.fetch(
|
28
|
+
%i[gorynich fetcher fetch], expires_in: @cache_expiration.seconds, namespace: @namespace
|
29
|
+
) do
|
30
|
+
if @fetcher.nil?
|
31
|
+
{}
|
32
|
+
elsif @fetcher.is_a?(Array)
|
33
|
+
result = {}
|
34
|
+
@fetcher.each do |f|
|
35
|
+
result =
|
36
|
+
begin
|
37
|
+
f.fetch
|
38
|
+
rescue ::StandardError
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
break unless result.empty?
|
42
|
+
end
|
43
|
+
result
|
44
|
+
else
|
45
|
+
@fetcher.fetch
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
raise Error, 'Config is empty' if cfg.empty?
|
50
|
+
|
51
|
+
cfg.deep_transform_keys(&:downcase)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'diplomat'
|
2
|
+
|
3
|
+
module Gorynich
|
4
|
+
module Fetchers
|
5
|
+
class Consul
|
6
|
+
attr_reader :storage, :consul_opts
|
7
|
+
|
8
|
+
def initialize(storage:, **opts)
|
9
|
+
@storage = storage
|
10
|
+
@consul_opts = opts
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch
|
14
|
+
config = ::Diplomat::Kv.get_all(storage, convert_to_hash: true, **consul_opts)
|
15
|
+
config.dig(*storage.split('/')) || {}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Gorynich
|
5
|
+
module Fetchers
|
6
|
+
class File
|
7
|
+
attr_reader :file_path
|
8
|
+
|
9
|
+
def initialize(file_path:)
|
10
|
+
@file_path = file_path
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch
|
14
|
+
data = ::ERB.new(::File.read(file_path)).result
|
15
|
+
|
16
|
+
::YAML.load(data, aliases: true) || {}
|
17
|
+
rescue ArgumentError
|
18
|
+
::YAML.load(data) || {}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Gorynich
|
2
|
+
module Head
|
3
|
+
module ActionCable
|
4
|
+
module Connection
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attr_reader :host, :tenant
|
9
|
+
|
10
|
+
def connect
|
11
|
+
@host = env['SERVER_NAME']
|
12
|
+
@tenant = ::Gorynich.instance.tenant_by_host(@host)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Channel
|
18
|
+
def subscribe_to_channel(*args)
|
19
|
+
::Gorynich.with(tenant, host: host) do
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def unsubscribe_from_channel(*args)
|
25
|
+
::Gorynich.with(tenant, host: host) do
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def perform_action(*args)
|
31
|
+
::Gorynich.with(tenant, host: host) do
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.broadcasting_for(model)
|
37
|
+
raise 'unable to broadcast message without tenant' if ::Gorynich::Current.tenant.nil?
|
38
|
+
|
39
|
+
serialize_broadcasting([channel_name, ::Gorynich::Current.tenant, model])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Gorynich
|
2
|
+
module Head
|
3
|
+
module ActiveJob
|
4
|
+
extend ::ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
attr_reader :current_uri, :current_tenant
|
8
|
+
|
9
|
+
def serialize
|
10
|
+
super.merge(uri: Gorynich::Current.uri, tenant: Gorynich::Current.tenant)
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(job_data)
|
14
|
+
super
|
15
|
+
@current_uri = job_data.fetch(:uri)
|
16
|
+
@current_tenant = job_data.fetch(:tenant)
|
17
|
+
end
|
18
|
+
|
19
|
+
around_perform do |job, block|
|
20
|
+
Gorynich.with(
|
21
|
+
job.current_tenant || Gorynich::Current.tenant,
|
22
|
+
uri: job.current_uri || Gorynich::Current.uri
|
23
|
+
) do |_current|
|
24
|
+
block.call
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
around_enqueue do |_job, block|
|
29
|
+
Gorynich.with_database(Gorynich.instance.default) do
|
30
|
+
block.call
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Gorynich
|
2
|
+
module Head
|
3
|
+
module ActiveRecord
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
module ::GlobalID::Locator
|
8
|
+
class << self
|
9
|
+
def original_locate(gid, options = {})
|
10
|
+
if (gid = GlobalID.parse(gid)) && find_allowed?(gid.model_class, options[:only])
|
11
|
+
locator_for(gid).locate gid
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def locate(gid, options = {})
|
16
|
+
gid = GlobalID.parse(gid)
|
17
|
+
return original_locate(gid, options) unless gid
|
18
|
+
|
19
|
+
tenant = gid.params['tenant']
|
20
|
+
if tenant && tenant != Gorynich::Current.tenant
|
21
|
+
Gorynich.with(tenant) do
|
22
|
+
original_locate(gid, options)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
original_locate(gid, options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
connects_to database: Gorynich.instance.connects_to_config
|
32
|
+
|
33
|
+
def cache_key(*args)
|
34
|
+
"#{Gorynich::Current.tenant}:#{super}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_global_id(options = {})
|
38
|
+
options[:tenant] ||= Gorynich::Current.tenant
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
alias_method :to_gid, :to_global_id
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Do not require this until want to use DelayedJob
|
2
|
+
|
3
|
+
module Gorynich
|
4
|
+
module Head
|
5
|
+
class DelayedJob < ::Delayed::Plugin
|
6
|
+
|
7
|
+
callbacks do |lifecycle|
|
8
|
+
lifecycle.around(:execute) do |*_args, &block|
|
9
|
+
Gorynich.with(Gorynich.instance.default) do |current|
|
10
|
+
if ::Delayed::Worker.logger.respond_to?(:tagged)
|
11
|
+
::Delayed::Worker.logger.tagged(tenant: current.tenant) { block.call }
|
12
|
+
else
|
13
|
+
block.call
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# connects_to database:
|
2
|
+
module Gorynich
|
3
|
+
module Head
|
4
|
+
class RackMiddleware
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
@config ||= Gorynich.instance
|
11
|
+
@config.actualize
|
12
|
+
|
13
|
+
tenant, opts = Gorynich.switcher.analyze(env)
|
14
|
+
|
15
|
+
Gorynich.with(tenant, **opts) do
|
16
|
+
if Rails.logger.respond_to?(:tagged)
|
17
|
+
Rails.logger.tagged("Tenant(#{tenant})") { @app.call(env) }
|
18
|
+
else
|
19
|
+
@app.call(env)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
rescue Gorynich::UriNotFound => e
|
23
|
+
Rails.logger.error(e.inspect)
|
24
|
+
[404, { 'Content-Type' => 'text/plain', 'charset' => 'UTF-8' }, [e.message]]
|
25
|
+
rescue Gorynich::HostNotFound => e
|
26
|
+
Rails.logger.error(e.inspect)
|
27
|
+
[404, { 'Content-Type' => 'text/plain', 'charset' => 'UTF-8' }, [e.message]]
|
28
|
+
rescue Gorynich::TenantNotFound => e
|
29
|
+
Rails.logger.error(e.inspect)
|
30
|
+
[404, { 'Content-Type' => 'text/plain', 'charset' => 'UTF-8' }, [e.message]]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Gorynich
|
2
|
+
class Switcher
|
3
|
+
def initialize(config:)
|
4
|
+
@config = config
|
5
|
+
end
|
6
|
+
|
7
|
+
#
|
8
|
+
# Hander for rack middleware's variables
|
9
|
+
#
|
10
|
+
# @param [Hash] env middleware's variables
|
11
|
+
#
|
12
|
+
# @return [[String, Hash]] tenant, options
|
13
|
+
#
|
14
|
+
def analyze(env)
|
15
|
+
return Gorynich.configuration.rack_env_handler.call(env) unless Gorynich.configuration.rack_env_handler.nil?
|
16
|
+
|
17
|
+
host = env['SERVER_NAME']
|
18
|
+
tenant = Gorynich.instance.tenant_by_host(host)
|
19
|
+
uri = Gorynich.instance.uri_by_host(host, tenant)
|
20
|
+
[tenant, { host: host, uri: uri }]
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_database(tenant)
|
24
|
+
::ActiveRecord::Base.connected_to role: tenant.to_sym do
|
25
|
+
yield(tenant)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_current(tenant, **opts, &block)
|
30
|
+
Gorynich::Current.set(@config.config(tenant.to_s).merge(opts)) do
|
31
|
+
block.call(Gorynich::Current.instance) if block.present?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def with(tenant, **opts, &block)
|
36
|
+
with_database(tenant) do
|
37
|
+
with_current(tenant, **opts, &block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def with_each_tenant(except: [], &block)
|
42
|
+
except = except.map(&:to_s)
|
43
|
+
@config.tenants.reject { |v| except.include?(v) }.each do |tenant|
|
44
|
+
with(tenant, &block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|