prefab-cloud-ruby 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/.envrc.sample +3 -0
- data/.github/workflows/ruby.yml +46 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +169 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +188 -0
- data/LICENSE.txt +20 -0
- data/README.md +94 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/bin/console +21 -0
- data/compile_protos.sh +18 -0
- data/lib/prefab/client.rb +153 -0
- data/lib/prefab/config_client.rb +292 -0
- data/lib/prefab/config_client_presenter.rb +18 -0
- data/lib/prefab/config_loader.rb +84 -0
- data/lib/prefab/config_resolver.rb +77 -0
- data/lib/prefab/config_value_unwrapper.rb +115 -0
- data/lib/prefab/config_value_wrapper.rb +18 -0
- data/lib/prefab/context.rb +179 -0
- data/lib/prefab/context_shape.rb +20 -0
- data/lib/prefab/context_shape_aggregator.rb +65 -0
- data/lib/prefab/criteria_evaluator.rb +136 -0
- data/lib/prefab/encryption.rb +65 -0
- data/lib/prefab/error.rb +6 -0
- data/lib/prefab/errors/env_var_parse_error.rb +11 -0
- data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
- data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
- data/lib/prefab/errors/missing_default_error.rb +13 -0
- data/lib/prefab/errors/missing_env_var_error.rb +11 -0
- data/lib/prefab/errors/uninitialized_error.rb +13 -0
- data/lib/prefab/evaluation.rb +52 -0
- data/lib/prefab/evaluation_summary_aggregator.rb +87 -0
- data/lib/prefab/example_contexts_aggregator.rb +78 -0
- data/lib/prefab/exponential_backoff.rb +21 -0
- data/lib/prefab/feature_flag_client.rb +42 -0
- data/lib/prefab/http_connection.rb +41 -0
- data/lib/prefab/internal_logger.rb +16 -0
- data/lib/prefab/local_config_parser.rb +151 -0
- data/lib/prefab/log_path_aggregator.rb +69 -0
- data/lib/prefab/logger_client.rb +264 -0
- data/lib/prefab/murmer3.rb +50 -0
- data/lib/prefab/options.rb +208 -0
- data/lib/prefab/periodic_sync.rb +69 -0
- data/lib/prefab/prefab.rb +56 -0
- data/lib/prefab/rate_limit_cache.rb +41 -0
- data/lib/prefab/resolved_config_presenter.rb +86 -0
- data/lib/prefab/time_helpers.rb +7 -0
- data/lib/prefab/weighted_value_resolver.rb +42 -0
- data/lib/prefab/yaml_config_parser.rb +34 -0
- data/lib/prefab-cloud-ruby.rb +57 -0
- data/lib/prefab_pb.rb +93 -0
- data/prefab-cloud-ruby.gemspec +155 -0
- data/test/.prefab.default.config.yaml +2 -0
- data/test/.prefab.unit_tests.config.yaml +28 -0
- data/test/integration_test.rb +150 -0
- data/test/integration_test_helpers.rb +151 -0
- data/test/support/common_helpers.rb +180 -0
- data/test/support/mock_base_client.rb +42 -0
- data/test/support/mock_config_client.rb +19 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_client.rb +444 -0
- data/test/test_config_client.rb +109 -0
- data/test/test_config_loader.rb +117 -0
- data/test/test_config_resolver.rb +430 -0
- data/test/test_config_value_unwrapper.rb +224 -0
- data/test/test_config_value_wrapper.rb +42 -0
- data/test/test_context.rb +203 -0
- data/test/test_context_shape.rb +50 -0
- data/test/test_context_shape_aggregator.rb +147 -0
- data/test/test_criteria_evaluator.rb +726 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluation_summary_aggregator.rb +162 -0
- data/test/test_example_contexts_aggregator.rb +238 -0
- data/test/test_exponential_backoff.rb +18 -0
- data/test/test_feature_flag_client.rb +48 -0
- data/test/test_helper.rb +17 -0
- data/test/test_integration.rb +58 -0
- data/test/test_local_config_parser.rb +147 -0
- data/test/test_log_path_aggregator.rb +62 -0
- data/test/test_logger.rb +621 -0
- data/test/test_logger_initialization.rb +12 -0
- data/test/test_options.rb +75 -0
- data/test/test_prefab.rb +12 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_weighted_value_resolver.rb +71 -0
- metadata +337 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uuid'
|
4
|
+
|
5
|
+
module Prefab
|
6
|
+
class Client
|
7
|
+
MAX_SLEEP_SEC = 10
|
8
|
+
BASE_SLEEP_SEC = 0.5
|
9
|
+
LOG = Prefab::InternalLogger.new(Client)
|
10
|
+
|
11
|
+
attr_reader :namespace, :interceptor, :api_key, :prefab_api_url, :options, :instance_hash
|
12
|
+
|
13
|
+
def initialize(options = Prefab::Options.new)
|
14
|
+
@options = options.is_a?(Prefab::Options) ? options : Prefab::Options.new(options)
|
15
|
+
@namespace = @options.namespace
|
16
|
+
@stubs = {}
|
17
|
+
@instance_hash = UUID.new.generate
|
18
|
+
Prefab::LoggerClient.new(@options.logdev, formatter: @options.log_formatter,
|
19
|
+
prefix: @options.log_prefix,
|
20
|
+
log_path_aggregator: log_path_aggregator
|
21
|
+
)
|
22
|
+
|
23
|
+
if @options.local_only?
|
24
|
+
LOG.debug 'Prefab Running in Local Mode'
|
25
|
+
elsif @options.datafile?
|
26
|
+
LOG.debug 'Prefab Running in DataFile Mode'
|
27
|
+
else
|
28
|
+
@api_key = @options.api_key
|
29
|
+
raise Prefab::Errors::InvalidApiKeyError, @api_key if @api_key.nil? || @api_key.empty? || api_key.count('-') < 1
|
30
|
+
|
31
|
+
@prefab_api_url = @options.prefab_api_url
|
32
|
+
LOG.debug "Prefab Connecting to: #{@prefab_api_url}"
|
33
|
+
end
|
34
|
+
|
35
|
+
context.clear
|
36
|
+
# start config client
|
37
|
+
config_client
|
38
|
+
end
|
39
|
+
|
40
|
+
def with_context(properties, &block)
|
41
|
+
Prefab::Context.with_context(properties, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def context
|
45
|
+
Prefab::Context.current
|
46
|
+
end
|
47
|
+
|
48
|
+
def config_client(timeout: 5.0)
|
49
|
+
@config_client ||= Prefab::ConfigClient.new(self, timeout)
|
50
|
+
end
|
51
|
+
|
52
|
+
def feature_flag_client
|
53
|
+
@feature_flag_client ||= Prefab::FeatureFlagClient.new(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def log_path_aggregator
|
57
|
+
return nil if @options.collect_max_paths <= 0
|
58
|
+
|
59
|
+
@log_path_aggregator ||= LogPathAggregator.new(client: self, max_paths: @options.collect_max_paths,
|
60
|
+
sync_interval: @options.collect_sync_interval)
|
61
|
+
end
|
62
|
+
|
63
|
+
def log
|
64
|
+
Prefab::LoggerClient.instance
|
65
|
+
end
|
66
|
+
|
67
|
+
def context_shape_aggregator
|
68
|
+
return nil if @options.collect_max_shapes <= 0
|
69
|
+
|
70
|
+
@context_shape_aggregator ||= ContextShapeAggregator.new(client: self, max_shapes: @options.collect_max_shapes,
|
71
|
+
sync_interval: @options.collect_sync_interval)
|
72
|
+
end
|
73
|
+
|
74
|
+
def example_contexts_aggregator
|
75
|
+
return nil if @options.collect_max_example_contexts <= 0
|
76
|
+
|
77
|
+
@example_contexts_aggregator ||= ExampleContextsAggregator.new(
|
78
|
+
client: self,
|
79
|
+
max_contexts: @options.collect_max_example_contexts,
|
80
|
+
sync_interval: @options.collect_sync_interval
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def evaluation_summary_aggregator
|
85
|
+
return nil if @options.collect_max_evaluation_summaries <= 0
|
86
|
+
|
87
|
+
@evaluation_summary_aggregator ||= EvaluationSummaryAggregator.new(
|
88
|
+
client: self,
|
89
|
+
max_keys: @options.collect_max_evaluation_summaries,
|
90
|
+
sync_interval: @options.collect_sync_interval
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_rails_loggers
|
95
|
+
Rails.logger = log
|
96
|
+
ActionView::Base.logger = log
|
97
|
+
ActionController::Base.logger = log
|
98
|
+
ActiveJob::Base.logger = log if defined?(ActiveJob)
|
99
|
+
ActiveRecord::Base.logger = log
|
100
|
+
ActiveStorage.logger = log if defined?(ActiveStorage)
|
101
|
+
|
102
|
+
LogSubscribers::ActionControllerSubscriber.attach_to :action_controller unless @options.disable_action_controller_logging
|
103
|
+
end
|
104
|
+
|
105
|
+
def on_update(&block)
|
106
|
+
resolver.on_update(&block)
|
107
|
+
end
|
108
|
+
|
109
|
+
def enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED)
|
110
|
+
feature_flag_client.feature_is_on_for?(feature_name, jit_context)
|
111
|
+
end
|
112
|
+
|
113
|
+
def get(key, default = NO_DEFAULT_PROVIDED, jit_context = NO_DEFAULT_PROVIDED)
|
114
|
+
if is_ff?(key)
|
115
|
+
feature_flag_client.get(key, jit_context, default: default)
|
116
|
+
else
|
117
|
+
config_client.get(key, default, jit_context)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def post(path, body)
|
122
|
+
Prefab::HttpConnection.new(@options.prefab_api_url, @api_key).post(path, body)
|
123
|
+
end
|
124
|
+
|
125
|
+
def inspect
|
126
|
+
"#<Prefab::Client:#{object_id} namespace=#{namespace}>"
|
127
|
+
end
|
128
|
+
|
129
|
+
def resolver
|
130
|
+
config_client.resolver
|
131
|
+
end
|
132
|
+
|
133
|
+
# When starting a forked process, use this to re-use the options
|
134
|
+
# on_worker_boot do
|
135
|
+
# $prefab = $prefab.fork
|
136
|
+
# $prefab.set_rails_loggers
|
137
|
+
# end
|
138
|
+
def fork
|
139
|
+
log_options = self.log.context_keys.to_a # get keys pre-fork
|
140
|
+
Prefab::Client.new(@options.for_fork).tap do |client|
|
141
|
+
client.log.add_context_keys(*log_options)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def is_ff?(key)
|
148
|
+
raw = config_client.send(:raw, key)
|
149
|
+
|
150
|
+
raw && raw.allowable_values.any?
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class ConfigClient
|
5
|
+
RECONNECT_WAIT = 5
|
6
|
+
DEFAULT_CHECKPOINT_FREQ_SEC = 60
|
7
|
+
SSE_READ_TIMEOUT = 300
|
8
|
+
STALE_CACHE_WARN_HOURS = 5
|
9
|
+
AUTH_USER = 'authuser'
|
10
|
+
LOGGING_KEY_PREFIX = "#{Prefab::LoggerClient::BASE_KEY}#{Prefab::LoggerClient::SEP}".freeze
|
11
|
+
LOG = Prefab::InternalLogger.new(ConfigClient)
|
12
|
+
|
13
|
+
def initialize(base_client, timeout)
|
14
|
+
@base_client = base_client
|
15
|
+
@options = base_client.options
|
16
|
+
LOG.debug 'Initialize ConfigClient'
|
17
|
+
@timeout = timeout
|
18
|
+
|
19
|
+
@stream_lock = Concurrent::ReadWriteLock.new
|
20
|
+
|
21
|
+
@checkpoint_freq_secs = DEFAULT_CHECKPOINT_FREQ_SEC
|
22
|
+
|
23
|
+
@config_loader = Prefab::ConfigLoader.new(@base_client)
|
24
|
+
@config_resolver = Prefab::ConfigResolver.new(@base_client, @config_loader)
|
25
|
+
|
26
|
+
@initialization_lock = Concurrent::ReadWriteLock.new
|
27
|
+
LOG.debug 'Initialize ConfigClient: AcquireWriteLock'
|
28
|
+
@initialization_lock.acquire_write_lock
|
29
|
+
LOG.debug 'Initialize ConfigClient: AcquiredWriteLock'
|
30
|
+
@initialized_future = Concurrent::Future.execute { @initialization_lock.acquire_read_lock }
|
31
|
+
|
32
|
+
if @options.local_only?
|
33
|
+
finish_init!(:local_only, nil)
|
34
|
+
elsif @options.datafile?
|
35
|
+
load_json_file(@options.datafile)
|
36
|
+
else
|
37
|
+
load_checkpoint
|
38
|
+
start_checkpointing_thread
|
39
|
+
start_streaming
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def start_streaming
|
44
|
+
@stream_lock.with_write_lock do
|
45
|
+
start_sse_streaming_connection_thread(@config_loader.highwater_mark) if @streaming_thread.nil?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
@config_resolver.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
def resolver
|
54
|
+
@config_resolver
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.value_to_delta(key, config_value, namespace = nil)
|
58
|
+
PrefabProto::Config.new(key: [namespace, key].compact.join(':'),
|
59
|
+
rows: [PrefabProto::ConfigRow.new(value: config_value)])
|
60
|
+
end
|
61
|
+
|
62
|
+
def get(key, default = NO_DEFAULT_PROVIDED, properties = NO_DEFAULT_PROVIDED)
|
63
|
+
context = @config_resolver.make_context(properties)
|
64
|
+
|
65
|
+
if !context.blank? && @base_client.example_contexts_aggregator
|
66
|
+
@base_client.example_contexts_aggregator.record(context)
|
67
|
+
end
|
68
|
+
|
69
|
+
evaluation = _get(key, context)
|
70
|
+
|
71
|
+
@base_client.context_shape_aggregator&.push(context)
|
72
|
+
|
73
|
+
if evaluation
|
74
|
+
evaluation.report_and_return(@base_client.evaluation_summary_aggregator)
|
75
|
+
else
|
76
|
+
handle_default(key, default)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def raw(key)
|
83
|
+
@config_resolver.raw(key)
|
84
|
+
end
|
85
|
+
|
86
|
+
def handle_default(key, default)
|
87
|
+
return default if default != NO_DEFAULT_PROVIDED
|
88
|
+
|
89
|
+
raise Prefab::Errors::MissingDefaultError, key if @options.on_no_default == Prefab::Options::ON_NO_DEFAULT::RAISE
|
90
|
+
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def _get(key, properties)
|
95
|
+
# wait timeout sec for the initialization to be complete
|
96
|
+
@initialized_future.value(@options.initialization_timeout_sec)
|
97
|
+
if @initialized_future.incomplete?
|
98
|
+
unless @options.on_init_failure == Prefab::Options::ON_INITIALIZATION_FAILURE::RETURN
|
99
|
+
raise Prefab::Errors::InitializationTimeoutError.new(@options.initialization_timeout_sec, key)
|
100
|
+
end
|
101
|
+
|
102
|
+
LOG.warn("Couldn't Initialize In #{@options.initialization_timeout_sec}. Key #{key}. Returning what we have")
|
103
|
+
@initialization_lock.release_write_lock
|
104
|
+
end
|
105
|
+
|
106
|
+
@config_resolver.get key, properties
|
107
|
+
end
|
108
|
+
|
109
|
+
def load_checkpoint
|
110
|
+
success = load_checkpoint_api_cdn
|
111
|
+
|
112
|
+
return if success
|
113
|
+
|
114
|
+
success = load_checkpoint_api
|
115
|
+
|
116
|
+
return if success
|
117
|
+
|
118
|
+
success = load_cache
|
119
|
+
|
120
|
+
return if success
|
121
|
+
|
122
|
+
LOG.warn 'No success loading checkpoints'
|
123
|
+
end
|
124
|
+
|
125
|
+
def load_checkpoint_api_cdn
|
126
|
+
conn = Prefab::HttpConnection.new("#{@options.url_for_api_cdn}/api/v1/configs/0", @base_client.api_key)
|
127
|
+
load_url(conn, :remote_cdn_api)
|
128
|
+
end
|
129
|
+
|
130
|
+
def load_checkpoint_api
|
131
|
+
conn = Prefab::HttpConnection.new("#{@options.prefab_api_url}/api/v1/configs/0", @base_client.api_key)
|
132
|
+
load_url(conn, :remote_api)
|
133
|
+
end
|
134
|
+
|
135
|
+
def load_url(conn, source)
|
136
|
+
resp = conn.get('')
|
137
|
+
if resp.status == 200
|
138
|
+
configs = PrefabProto::Configs.decode(resp.body)
|
139
|
+
load_configs(configs, source)
|
140
|
+
cache_configs(configs)
|
141
|
+
true
|
142
|
+
else
|
143
|
+
LOG.info "Checkpoint #{source} failed to load. Response #{resp.status}"
|
144
|
+
false
|
145
|
+
end
|
146
|
+
rescue Faraday::ConnectionFailed => e
|
147
|
+
if @initialization_lock.write_locked?
|
148
|
+
LOG.warn "Connection Fail loading #{source} checkpoint."
|
149
|
+
else
|
150
|
+
LOG.debug "Connection Fail loading #{source} checkpoint."
|
151
|
+
end
|
152
|
+
false
|
153
|
+
rescue StandardError => e
|
154
|
+
LOG.warn "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
|
155
|
+
LOG.debug e.backtrace
|
156
|
+
false
|
157
|
+
end
|
158
|
+
|
159
|
+
def load_configs(configs, source)
|
160
|
+
project_id = configs.config_service_pointer.project_id
|
161
|
+
project_env_id = configs.config_service_pointer.project_env_id
|
162
|
+
@config_resolver.project_env_id = project_env_id
|
163
|
+
starting_highwater_mark = @config_loader.highwater_mark
|
164
|
+
|
165
|
+
default_contexts = configs.default_context&.contexts&.map do |context|
|
166
|
+
[
|
167
|
+
context.type,
|
168
|
+
context.values.keys.map do |k|
|
169
|
+
[k, Prefab::ConfigValueUnwrapper.new(context.values[k], @config_resolver).unwrap]
|
170
|
+
end.to_h
|
171
|
+
]
|
172
|
+
end.to_h
|
173
|
+
|
174
|
+
@config_resolver.default_context = default_contexts || {}
|
175
|
+
|
176
|
+
configs.configs.each do |config|
|
177
|
+
@config_loader.set(config, source)
|
178
|
+
end
|
179
|
+
if @config_loader.highwater_mark > starting_highwater_mark
|
180
|
+
LOG.debug("Found new checkpoint with highwater id #{@config_loader.highwater_mark} from #{source} in project #{project_id} environment: #{project_env_id} and namespace: '#{@namespace}'")
|
181
|
+
else
|
182
|
+
LOG.debug("Checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}. No changes.")
|
183
|
+
end
|
184
|
+
@config_resolver.update
|
185
|
+
finish_init!(source, project_id)
|
186
|
+
end
|
187
|
+
|
188
|
+
def cache_path
|
189
|
+
return @cache_path unless @cache_path.nil?
|
190
|
+
@cache_path ||= calc_cache_path
|
191
|
+
FileUtils.mkdir_p(File.dirname(@cache_path))
|
192
|
+
@cache_path
|
193
|
+
end
|
194
|
+
|
195
|
+
def calc_cache_path
|
196
|
+
file_name = "prefab.cache.#{@base_client.options.api_key_id}.json"
|
197
|
+
dir = ENV.fetch('XDG_CACHE_HOME', File.join(Dir.home, '.cache'))
|
198
|
+
File.join(dir, file_name)
|
199
|
+
end
|
200
|
+
|
201
|
+
def cache_configs(configs)
|
202
|
+
return unless @options.use_local_cache && !@options.is_fork
|
203
|
+
File.open(cache_path, "w") do |f|
|
204
|
+
f.flock(File::LOCK_EX)
|
205
|
+
f.write(PrefabProto::Configs.encode_json(configs))
|
206
|
+
end
|
207
|
+
LOG.debug "Cached configs to #{cache_path}"
|
208
|
+
rescue => e
|
209
|
+
LOG.debug "Failed to cache configs to #{cache_path} #{e}"
|
210
|
+
end
|
211
|
+
|
212
|
+
def load_cache
|
213
|
+
return false unless @options.use_local_cache
|
214
|
+
File.open(cache_path) do |f|
|
215
|
+
f.flock(File::LOCK_SH)
|
216
|
+
configs = PrefabProto::Configs.decode_json(f.read)
|
217
|
+
load_configs(configs, :cache)
|
218
|
+
|
219
|
+
hours_old = ((Time.now - File.mtime(f)) / 60 / 60).round(2)
|
220
|
+
if hours_old > STALE_CACHE_WARN_HOURS
|
221
|
+
LOG.info "Stale Cache Load: #{hours_old} hours old"
|
222
|
+
end
|
223
|
+
true
|
224
|
+
end
|
225
|
+
rescue => e
|
226
|
+
LOG.debug "Failed to read cached configs at #{cache_path}. #{e}"
|
227
|
+
false
|
228
|
+
end
|
229
|
+
|
230
|
+
def load_json_file(file)
|
231
|
+
File.open(file) do |f|
|
232
|
+
f.flock(File::LOCK_SH)
|
233
|
+
configs = PrefabProto::Configs.decode_json(f.read)
|
234
|
+
load_configs(configs, :datafile)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# A thread that checks for a checkpoint
|
239
|
+
def start_checkpointing_thread
|
240
|
+
Thread.new do
|
241
|
+
loop do
|
242
|
+
started_at = Time.now
|
243
|
+
delta = @checkpoint_freq_secs - (Time.now - started_at)
|
244
|
+
sleep(delta) if delta > 0
|
245
|
+
|
246
|
+
load_checkpoint
|
247
|
+
rescue StandardError => e
|
248
|
+
LOG.debug "Issue Checkpointing #{e.message}"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def finish_init!(source, project_id)
|
254
|
+
return unless @initialization_lock.write_locked?
|
255
|
+
|
256
|
+
LOG.debug "Unlocked Config via #{source}"
|
257
|
+
@initialization_lock.release_write_lock
|
258
|
+
|
259
|
+
Prefab::LoggerClient.instance.config_client = self
|
260
|
+
presenter = Prefab::ConfigClientPresenter.new(
|
261
|
+
size: @config_resolver.local_store.size,
|
262
|
+
source: source,
|
263
|
+
project_id: project_id,
|
264
|
+
project_env_id: @config_resolver.project_env_id,
|
265
|
+
api_key_id: @base_client.options.api_key_id
|
266
|
+
)
|
267
|
+
LOG.info presenter.to_s
|
268
|
+
LOG.debug to_s
|
269
|
+
end
|
270
|
+
|
271
|
+
def start_sse_streaming_connection_thread(start_at_id)
|
272
|
+
auth = "#{AUTH_USER}:#{@base_client.api_key}"
|
273
|
+
auth_string = Base64.strict_encode64(auth)
|
274
|
+
headers = {
|
275
|
+
'x-prefab-start-at-id' => start_at_id,
|
276
|
+
'Authorization' => "Basic #{auth_string}",
|
277
|
+
'X-PrefabCloud-Client-Version' => "prefab-cloud-ruby-#{Prefab::VERSION}"
|
278
|
+
}
|
279
|
+
url = "#{@base_client.prefab_api_url}/api/v1/sse/config"
|
280
|
+
LOG.debug "SSE Streaming Connect to #{url} start_at #{start_at_id}"
|
281
|
+
@streaming_thread = SSE::Client.new(url,
|
282
|
+
headers: headers,
|
283
|
+
read_timeout: SSE_READ_TIMEOUT,
|
284
|
+
logger: Prefab::SseLogger.new) do |client|
|
285
|
+
client.on_event do |event|
|
286
|
+
configs = PrefabProto::Configs.decode(Base64.decode64(event.data))
|
287
|
+
load_configs(configs, :sse)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class ConfigClientPresenter
|
5
|
+
def initialize(size:, source:, project_id:, project_env_id:, api_key_id:)
|
6
|
+
@size = size
|
7
|
+
@source = source
|
8
|
+
@project_id = project_id
|
9
|
+
@project_env_id = project_env_id
|
10
|
+
@api_key_id = api_key_id
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"Configuration Loaded count=#{@size} source=#{@source} project=#{@project_id} project-env=#{@project_env_id} prefab.api-key-id=#{@api_key_id}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class ConfigLoader
|
5
|
+
LOG = Prefab::InternalLogger.new(ConfigLoader)
|
6
|
+
|
7
|
+
attr_reader :highwater_mark
|
8
|
+
|
9
|
+
def initialize(base_client)
|
10
|
+
@base_client = base_client
|
11
|
+
@prefab_options = base_client.options
|
12
|
+
@highwater_mark = 0
|
13
|
+
@classpath_config = load_classpath_config
|
14
|
+
@local_overrides = load_local_overrides
|
15
|
+
@api_config = Concurrent::Map.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def calc_config
|
19
|
+
rtn = @classpath_config.clone
|
20
|
+
@api_config.each_key do |k|
|
21
|
+
rtn[k] = @api_config[k]
|
22
|
+
end
|
23
|
+
rtn.merge(@local_overrides)
|
24
|
+
end
|
25
|
+
|
26
|
+
def set(config, source)
|
27
|
+
# don't overwrite newer values
|
28
|
+
return if @api_config[config.key] && @api_config[config.key][:config].id >= config.id
|
29
|
+
|
30
|
+
if config.rows.empty?
|
31
|
+
@api_config.delete(config.key)
|
32
|
+
else
|
33
|
+
if @api_config[config.key]
|
34
|
+
LOG.debug(
|
35
|
+
"Replace #{config.key} with value from #{source} #{@api_config[config.key][:config].id} -> #{config.id}")
|
36
|
+
end
|
37
|
+
@api_config[config.key] = { source: source, config: config }
|
38
|
+
end
|
39
|
+
@highwater_mark = [config.id, @highwater_mark].max
|
40
|
+
end
|
41
|
+
|
42
|
+
def rm(key)
|
43
|
+
@api_config.delete key
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_api_deltas
|
47
|
+
configs = PrefabProto::Configs.new
|
48
|
+
@api_config.each_value do |config_value|
|
49
|
+
configs.configs << config_value[:config]
|
50
|
+
end
|
51
|
+
configs
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def load_classpath_config
|
57
|
+
return {} if @prefab_options.datafile?
|
58
|
+
classpath_dir = @prefab_options.prefab_config_classpath_dir
|
59
|
+
rtn = load_glob(File.join(classpath_dir, '.prefab.default.config.yaml'))
|
60
|
+
@prefab_options.prefab_envs.each do |env|
|
61
|
+
rtn = rtn.merge load_glob(File.join(classpath_dir, ".prefab.#{env}.config.yaml"))
|
62
|
+
end
|
63
|
+
rtn
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_local_overrides
|
67
|
+
return {} if @prefab_options.datafile?
|
68
|
+
override_dir = @prefab_options.prefab_config_override_dir
|
69
|
+
rtn = load_glob(File.join(override_dir, '.prefab.default.config.yaml'))
|
70
|
+
@prefab_options.prefab_envs.each do |env|
|
71
|
+
rtn = rtn.merge load_glob(File.join(override_dir, ".prefab.#{env}.config.yaml"))
|
72
|
+
end
|
73
|
+
rtn
|
74
|
+
end
|
75
|
+
|
76
|
+
def load_glob(glob)
|
77
|
+
rtn = {}
|
78
|
+
Dir.glob(glob).each do |file|
|
79
|
+
Prefab::YAMLConfigParser.new(file, @base_client).merge(rtn)
|
80
|
+
end
|
81
|
+
rtn
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class ConfigResolver
|
5
|
+
attr_accessor :project_env_id # this will be set by the config_client when it gets an API response
|
6
|
+
attr_reader :local_store
|
7
|
+
|
8
|
+
attr_accessor :default_context
|
9
|
+
|
10
|
+
def initialize(base_client, config_loader)
|
11
|
+
@lock = Concurrent::ReadWriteLock.new
|
12
|
+
@local_store = {}
|
13
|
+
@config_loader = config_loader
|
14
|
+
@project_env_id = 0 # we don't know this yet, it is set from the API results
|
15
|
+
@base_client = base_client
|
16
|
+
@on_update = nil
|
17
|
+
@default_context = {}
|
18
|
+
make_local
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
presenter.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def presenter
|
26
|
+
Prefab::ResolvedConfigPresenter.new(self, @lock, @local_store)
|
27
|
+
end
|
28
|
+
|
29
|
+
def raw(key)
|
30
|
+
@local_store.dig(key, :config)
|
31
|
+
end
|
32
|
+
|
33
|
+
def get(key, properties = NO_DEFAULT_PROVIDED)
|
34
|
+
@lock.with_read_lock do
|
35
|
+
raw_config = raw(key)
|
36
|
+
|
37
|
+
return nil unless raw_config
|
38
|
+
|
39
|
+
evaluate(raw_config, properties)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def evaluate(config, properties = NO_DEFAULT_PROVIDED)
|
44
|
+
Prefab::CriteriaEvaluator.new(config,
|
45
|
+
project_env_id: @project_env_id,
|
46
|
+
resolver: self,
|
47
|
+
namespace: @base_client.options.namespace,
|
48
|
+
base_client: @base_client).evaluate(make_context(properties))
|
49
|
+
end
|
50
|
+
|
51
|
+
def update
|
52
|
+
make_local
|
53
|
+
|
54
|
+
@on_update ? @on_update.call : nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def on_update(&block)
|
58
|
+
@on_update = block
|
59
|
+
end
|
60
|
+
|
61
|
+
def make_context(properties)
|
62
|
+
if properties == NO_DEFAULT_PROVIDED || properties.nil?
|
63
|
+
Context.current
|
64
|
+
else
|
65
|
+
Context.merge_with_current(properties)
|
66
|
+
end.merge_default(default_context || {})
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def make_local
|
72
|
+
@lock.with_write_lock do
|
73
|
+
@local_store = @config_loader.calc_config
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|