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.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/workflows/ruby.yml +46 -0
  4. data/.gitmodules +3 -0
  5. data/.rubocop.yml +13 -0
  6. data/.tool-versions +1 -0
  7. data/CHANGELOG.md +169 -0
  8. data/CODEOWNERS +1 -0
  9. data/Gemfile +26 -0
  10. data/Gemfile.lock +188 -0
  11. data/LICENSE.txt +20 -0
  12. data/README.md +94 -0
  13. data/Rakefile +50 -0
  14. data/VERSION +1 -0
  15. data/bin/console +21 -0
  16. data/compile_protos.sh +18 -0
  17. data/lib/prefab/client.rb +153 -0
  18. data/lib/prefab/config_client.rb +292 -0
  19. data/lib/prefab/config_client_presenter.rb +18 -0
  20. data/lib/prefab/config_loader.rb +84 -0
  21. data/lib/prefab/config_resolver.rb +77 -0
  22. data/lib/prefab/config_value_unwrapper.rb +115 -0
  23. data/lib/prefab/config_value_wrapper.rb +18 -0
  24. data/lib/prefab/context.rb +179 -0
  25. data/lib/prefab/context_shape.rb +20 -0
  26. data/lib/prefab/context_shape_aggregator.rb +65 -0
  27. data/lib/prefab/criteria_evaluator.rb +136 -0
  28. data/lib/prefab/encryption.rb +65 -0
  29. data/lib/prefab/error.rb +6 -0
  30. data/lib/prefab/errors/env_var_parse_error.rb +11 -0
  31. data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
  32. data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
  33. data/lib/prefab/errors/missing_default_error.rb +13 -0
  34. data/lib/prefab/errors/missing_env_var_error.rb +11 -0
  35. data/lib/prefab/errors/uninitialized_error.rb +13 -0
  36. data/lib/prefab/evaluation.rb +52 -0
  37. data/lib/prefab/evaluation_summary_aggregator.rb +87 -0
  38. data/lib/prefab/example_contexts_aggregator.rb +78 -0
  39. data/lib/prefab/exponential_backoff.rb +21 -0
  40. data/lib/prefab/feature_flag_client.rb +42 -0
  41. data/lib/prefab/http_connection.rb +41 -0
  42. data/lib/prefab/internal_logger.rb +16 -0
  43. data/lib/prefab/local_config_parser.rb +151 -0
  44. data/lib/prefab/log_path_aggregator.rb +69 -0
  45. data/lib/prefab/logger_client.rb +264 -0
  46. data/lib/prefab/murmer3.rb +50 -0
  47. data/lib/prefab/options.rb +208 -0
  48. data/lib/prefab/periodic_sync.rb +69 -0
  49. data/lib/prefab/prefab.rb +56 -0
  50. data/lib/prefab/rate_limit_cache.rb +41 -0
  51. data/lib/prefab/resolved_config_presenter.rb +86 -0
  52. data/lib/prefab/time_helpers.rb +7 -0
  53. data/lib/prefab/weighted_value_resolver.rb +42 -0
  54. data/lib/prefab/yaml_config_parser.rb +34 -0
  55. data/lib/prefab-cloud-ruby.rb +57 -0
  56. data/lib/prefab_pb.rb +93 -0
  57. data/prefab-cloud-ruby.gemspec +155 -0
  58. data/test/.prefab.default.config.yaml +2 -0
  59. data/test/.prefab.unit_tests.config.yaml +28 -0
  60. data/test/integration_test.rb +150 -0
  61. data/test/integration_test_helpers.rb +151 -0
  62. data/test/support/common_helpers.rb +180 -0
  63. data/test/support/mock_base_client.rb +42 -0
  64. data/test/support/mock_config_client.rb +19 -0
  65. data/test/support/mock_config_loader.rb +1 -0
  66. data/test/test_client.rb +444 -0
  67. data/test/test_config_client.rb +109 -0
  68. data/test/test_config_loader.rb +117 -0
  69. data/test/test_config_resolver.rb +430 -0
  70. data/test/test_config_value_unwrapper.rb +224 -0
  71. data/test/test_config_value_wrapper.rb +42 -0
  72. data/test/test_context.rb +203 -0
  73. data/test/test_context_shape.rb +50 -0
  74. data/test/test_context_shape_aggregator.rb +147 -0
  75. data/test/test_criteria_evaluator.rb +726 -0
  76. data/test/test_encryption.rb +16 -0
  77. data/test/test_evaluation_summary_aggregator.rb +162 -0
  78. data/test/test_example_contexts_aggregator.rb +238 -0
  79. data/test/test_exponential_backoff.rb +18 -0
  80. data/test/test_feature_flag_client.rb +48 -0
  81. data/test/test_helper.rb +17 -0
  82. data/test/test_integration.rb +58 -0
  83. data/test/test_local_config_parser.rb +147 -0
  84. data/test/test_log_path_aggregator.rb +62 -0
  85. data/test/test_logger.rb +621 -0
  86. data/test/test_logger_initialization.rb +12 -0
  87. data/test/test_options.rb +75 -0
  88. data/test/test_prefab.rb +12 -0
  89. data/test/test_rate_limit_cache.rb +44 -0
  90. data/test/test_weighted_value_resolver.rb +71 -0
  91. 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