prefab-cloud-ruby 0

Sign up to get free protection for your applications and to get access to all the features.
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