gorynich 1.1.0.142046
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/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
|