sdk-reforge 1.9.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/CODEOWNERS +2 -0
- data/.github/pull_request_template.md +8 -0
- data/.github/workflows/ruby.yml +48 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +257 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +182 -0
- data/LICENSE.txt +20 -0
- data/README.md +105 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/compile_protos.sh +20 -0
- data/dev/allocation_stats +60 -0
- data/dev/benchmark +40 -0
- data/dev/console +12 -0
- data/dev/script_setup.rb +18 -0
- data/lib/prefab_pb.rb +77 -0
- data/lib/reforge/caching_http_connection.rb +95 -0
- data/lib/reforge/client.rb +133 -0
- data/lib/reforge/config_client.rb +275 -0
- data/lib/reforge/config_client_presenter.rb +18 -0
- data/lib/reforge/config_loader.rb +67 -0
- data/lib/reforge/config_resolver.rb +84 -0
- data/lib/reforge/config_value_unwrapper.rb +123 -0
- data/lib/reforge/config_value_wrapper.rb +18 -0
- data/lib/reforge/context.rb +241 -0
- data/lib/reforge/context_shape.rb +20 -0
- data/lib/reforge/context_shape_aggregator.rb +70 -0
- data/lib/reforge/criteria_evaluator.rb +345 -0
- data/lib/reforge/duration.rb +58 -0
- data/lib/reforge/encryption.rb +65 -0
- data/lib/reforge/error.rb +6 -0
- data/lib/reforge/errors/env_var_parse_error.rb +11 -0
- data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
- data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
- data/lib/reforge/errors/missing_default_error.rb +13 -0
- data/lib/reforge/errors/missing_env_var_error.rb +11 -0
- data/lib/reforge/errors/uninitialized_error.rb +13 -0
- data/lib/reforge/evaluation.rb +53 -0
- data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
- data/lib/reforge/example_contexts_aggregator.rb +77 -0
- data/lib/reforge/exponential_backoff.rb +21 -0
- data/lib/reforge/feature_flag_client.rb +43 -0
- data/lib/reforge/fixed_size_hash.rb +14 -0
- data/lib/reforge/http_connection.rb +45 -0
- data/lib/reforge/internal_logger.rb +43 -0
- data/lib/reforge/javascript_stub.rb +99 -0
- data/lib/reforge/local_config_parser.rb +151 -0
- data/lib/reforge/murmer3.rb +50 -0
- data/lib/reforge/options.rb +191 -0
- data/lib/reforge/periodic_sync.rb +74 -0
- data/lib/reforge/prefab.rb +120 -0
- data/lib/reforge/rate_limit_cache.rb +41 -0
- data/lib/reforge/resolved_config_presenter.rb +86 -0
- data/lib/reforge/semver.rb +132 -0
- data/lib/reforge/sse_config_client.rb +112 -0
- data/lib/reforge/time_helpers.rb +7 -0
- data/lib/reforge/weighted_value_resolver.rb +42 -0
- data/lib/reforge/yaml_config_parser.rb +34 -0
- data/lib/reforge-sdk.rb +57 -0
- data/test/fixtures/datafile.json +87 -0
- data/test/integration_test.rb +171 -0
- data/test/integration_test_helpers.rb +114 -0
- data/test/support/common_helpers.rb +201 -0
- data/test/support/mock_base_client.rb +41 -0
- data/test/support/mock_config_client.rb +19 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_caching_http_connection.rb +218 -0
- data/test/test_client.rb +351 -0
- data/test/test_config_client.rb +84 -0
- data/test/test_config_loader.rb +82 -0
- data/test/test_config_resolver.rb +502 -0
- data/test/test_config_value_unwrapper.rb +270 -0
- data/test/test_config_value_wrapper.rb +42 -0
- data/test/test_context.rb +271 -0
- data/test/test_context_shape.rb +50 -0
- data/test/test_context_shape_aggregator.rb +150 -0
- data/test/test_criteria_evaluator.rb +1180 -0
- data/test/test_duration.rb +37 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluation_summary_aggregator.rb +162 -0
- data/test/test_example_contexts_aggregator.rb +233 -0
- data/test/test_exponential_backoff.rb +18 -0
- data/test/test_feature_flag_client.rb +16 -0
- data/test/test_fixed_size_hash.rb +119 -0
- data/test/test_helper.rb +17 -0
- data/test/test_integration.rb +75 -0
- data/test/test_internal_logger.rb +25 -0
- data/test/test_javascript_stub.rb +176 -0
- data/test/test_local_config_parser.rb +147 -0
- data/test/test_logger_initialization.rb +12 -0
- data/test/test_options.rb +93 -0
- data/test/test_prefab.rb +16 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_semver.rb +108 -0
- data/test/test_sse_config_client.rb +211 -0
- data/test/test_weighted_value_resolver.rb +71 -0
- metadata +345 -0
@@ -0,0 +1,275 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class ConfigClient
|
5
|
+
LOG = Reforge::InternalLogger.new(self)
|
6
|
+
DEFAULT_CHECKPOINT_FREQ_SEC = 60
|
7
|
+
STALE_CACHE_WARN_HOURS = 5
|
8
|
+
|
9
|
+
def initialize(base_client, timeout)
|
10
|
+
@base_client = base_client
|
11
|
+
@options = base_client.options
|
12
|
+
LOG.debug 'Initialize ConfigClient'
|
13
|
+
@timeout = timeout
|
14
|
+
|
15
|
+
@checkpoint_freq_secs = DEFAULT_CHECKPOINT_FREQ_SEC
|
16
|
+
|
17
|
+
@config_loader = Reforge::ConfigLoader.new(@base_client)
|
18
|
+
@config_resolver = Reforge::ConfigResolver.new(@base_client, @config_loader)
|
19
|
+
|
20
|
+
@initialization_lock = Concurrent::CountDownLatch.new(1)
|
21
|
+
|
22
|
+
if @options.local_only?
|
23
|
+
finish_init!(:local_only, nil)
|
24
|
+
elsif @options.datafile?
|
25
|
+
load_json_file(@options.datafile)
|
26
|
+
else
|
27
|
+
load_checkpoint
|
28
|
+
start_checkpointing_thread
|
29
|
+
start_streaming
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
@sse_config_client&.close
|
35
|
+
end
|
36
|
+
|
37
|
+
def start_streaming
|
38
|
+
Thread.new do
|
39
|
+
# wait for the config loader to have a highwater mark before we connect the SSE
|
40
|
+
loop do
|
41
|
+
break if @config_loader.highwater_mark > 0
|
42
|
+
|
43
|
+
sleep 0.1
|
44
|
+
end
|
45
|
+
|
46
|
+
stream_lock = Concurrent::ReadWriteLock.new
|
47
|
+
@sse_config_client = Reforge::SSEConfigClient.new(@options, @config_loader)
|
48
|
+
|
49
|
+
@sse_config_client.start do |configs, _event, source|
|
50
|
+
stream_lock.with_write_lock do
|
51
|
+
load_configs(configs, source)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
@config_resolver.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
def resolver
|
62
|
+
@config_resolver
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.value_to_delta(key, config_value, namespace = nil)
|
66
|
+
PrefabProto::Config.new(key: [namespace, key].compact.join(':'),
|
67
|
+
rows: [PrefabProto::ConfigRow.new(value: config_value)])
|
68
|
+
end
|
69
|
+
|
70
|
+
def get(key, default = NO_DEFAULT_PROVIDED, properties = NO_DEFAULT_PROVIDED)
|
71
|
+
context = @config_resolver.make_context(properties)
|
72
|
+
|
73
|
+
if !context.blank? && @base_client.example_contexts_aggregator
|
74
|
+
@base_client.example_contexts_aggregator.record(context)
|
75
|
+
end
|
76
|
+
|
77
|
+
evaluation = _get(key, properties)
|
78
|
+
|
79
|
+
@base_client.context_shape_aggregator&.push(context)
|
80
|
+
|
81
|
+
if evaluation
|
82
|
+
evaluation.report_and_return(@base_client.evaluation_summary_aggregator)
|
83
|
+
else
|
84
|
+
handle_default(key, default)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def initialized?
|
89
|
+
@initialization_lock.count <= 0
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def raw(key)
|
95
|
+
@config_resolver.raw(key)
|
96
|
+
end
|
97
|
+
|
98
|
+
def handle_default(key, default)
|
99
|
+
return default if default != NO_DEFAULT_PROVIDED
|
100
|
+
|
101
|
+
raise Reforge::Errors::MissingDefaultError, key if @options.on_no_default == Reforge::Options::ON_NO_DEFAULT::RAISE
|
102
|
+
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
|
106
|
+
def _get(key, properties)
|
107
|
+
# wait timeout sec for the initialization to be complete
|
108
|
+
success = @initialization_lock.wait(@options.initialization_timeout_sec)
|
109
|
+
if !success
|
110
|
+
unless @options.on_init_failure == Reforge::Options::ON_INITIALIZATION_FAILURE::RETURN
|
111
|
+
raise Reforge::Errors::InitializationTimeoutError.new(@options.initialization_timeout_sec, key)
|
112
|
+
end
|
113
|
+
|
114
|
+
LOG.warn("Couldn't Initialize In #{@options.initialization_timeout_sec}. Key #{key}. Returning what we have")
|
115
|
+
end
|
116
|
+
|
117
|
+
@config_resolver.get key, properties
|
118
|
+
end
|
119
|
+
|
120
|
+
def load_checkpoint
|
121
|
+
success = load_source_checkpoint(:start_at_id => @config_loader.highwater_mark)
|
122
|
+
return if success
|
123
|
+
|
124
|
+
success = load_cache
|
125
|
+
return if success
|
126
|
+
|
127
|
+
LOG.warn 'No success loading checkpoints'
|
128
|
+
end
|
129
|
+
|
130
|
+
def load_source_checkpoint(start_at_id: 0)
|
131
|
+
@options.config_sources.each do |source|
|
132
|
+
conn = Reforge::CachingHttpConnection.new("#{source}/api/v2/configs/#{start_at_id}", @base_client.sdk_key)
|
133
|
+
result = load_url(conn, :remote_api)
|
134
|
+
return true if result
|
135
|
+
end
|
136
|
+
|
137
|
+
return false
|
138
|
+
end
|
139
|
+
|
140
|
+
def load_url(conn, source)
|
141
|
+
resp = conn.get('')
|
142
|
+
if resp.status == 200
|
143
|
+
configs = PrefabProto::Configs.decode(resp.body)
|
144
|
+
load_configs(configs, source)
|
145
|
+
cache_configs(configs)
|
146
|
+
true
|
147
|
+
else
|
148
|
+
LOG.info "Checkpoint #{source} [#{conn.uri}] failed to load. Response #{resp.status}"
|
149
|
+
false
|
150
|
+
end
|
151
|
+
rescue Faraday::ConnectionFailed => e
|
152
|
+
if !initialized?
|
153
|
+
LOG.warn "Connection Fail loading #{source} [#{conn.uri}] checkpoint."
|
154
|
+
else
|
155
|
+
LOG.debug "Connection Fail loading #{source} [#{conn.uri}] checkpoint."
|
156
|
+
end
|
157
|
+
false
|
158
|
+
rescue StandardError => e
|
159
|
+
LOG.warn "Unexpected #{source} [#{conn.uri}] problem loading checkpoint #{e} #{conn}"
|
160
|
+
LOG.debug e.backtrace
|
161
|
+
false
|
162
|
+
end
|
163
|
+
|
164
|
+
def load_configs(configs, source)
|
165
|
+
project_id = configs.config_service_pointer.project_id
|
166
|
+
project_env_id = configs.config_service_pointer.project_env_id
|
167
|
+
@config_resolver.project_env_id = project_env_id
|
168
|
+
starting_highwater_mark = @config_loader.highwater_mark
|
169
|
+
|
170
|
+
default_contexts = configs.default_context&.contexts&.map do |context|
|
171
|
+
[
|
172
|
+
context.type,
|
173
|
+
context.values.keys.map do |k|
|
174
|
+
[k, Reforge::ConfigValueUnwrapper.new(context.values[k], @config_resolver).unwrap]
|
175
|
+
end.to_h
|
176
|
+
]
|
177
|
+
end.to_h
|
178
|
+
|
179
|
+
Reforge::Context.default_context = default_contexts || {}
|
180
|
+
|
181
|
+
configs.configs.each do |config|
|
182
|
+
@config_loader.set(config, source)
|
183
|
+
end
|
184
|
+
if @config_loader.highwater_mark > starting_highwater_mark
|
185
|
+
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}'")
|
186
|
+
else
|
187
|
+
LOG.debug("Checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}. No changes.")
|
188
|
+
end
|
189
|
+
@config_resolver.update
|
190
|
+
finish_init!(source, project_id)
|
191
|
+
end
|
192
|
+
|
193
|
+
def cache_path
|
194
|
+
return @cache_path unless @cache_path.nil?
|
195
|
+
@cache_path ||= calc_cache_path
|
196
|
+
FileUtils.mkdir_p(File.dirname(@cache_path))
|
197
|
+
@cache_path
|
198
|
+
end
|
199
|
+
|
200
|
+
def calc_cache_path
|
201
|
+
file_name = "prefab.cache.#{@base_client.options.sdk_key_id}.json"
|
202
|
+
dir = ENV.fetch('XDG_CACHE_HOME', File.join(Dir.home, '.cache'))
|
203
|
+
File.join(dir, file_name)
|
204
|
+
end
|
205
|
+
|
206
|
+
def cache_configs(configs)
|
207
|
+
return unless @options.use_local_cache && !@options.is_fork
|
208
|
+
File.open(cache_path, "w") do |f|
|
209
|
+
f.flock(File::LOCK_EX)
|
210
|
+
f.write(PrefabProto::Configs.encode_json(configs))
|
211
|
+
end
|
212
|
+
LOG.debug "Cached configs to #{cache_path}"
|
213
|
+
rescue => e
|
214
|
+
LOG.debug "Failed to cache configs to #{cache_path} #{e}"
|
215
|
+
end
|
216
|
+
|
217
|
+
def load_cache
|
218
|
+
return false unless @options.use_local_cache
|
219
|
+
File.open(cache_path) do |f|
|
220
|
+
f.flock(File::LOCK_SH)
|
221
|
+
configs = PrefabProto::Configs.decode_json(f.read)
|
222
|
+
load_configs(configs, :cache)
|
223
|
+
|
224
|
+
hours_old = ((Time.now - File.mtime(f)) / 60 / 60).round(2)
|
225
|
+
if hours_old > STALE_CACHE_WARN_HOURS
|
226
|
+
LOG.info "Stale Cache Load: #{hours_old} hours old"
|
227
|
+
end
|
228
|
+
true
|
229
|
+
end
|
230
|
+
rescue => e
|
231
|
+
LOG.debug "Failed to read cached configs at #{cache_path}. #{e}"
|
232
|
+
false
|
233
|
+
end
|
234
|
+
|
235
|
+
def load_json_file(file)
|
236
|
+
File.open(file) do |f|
|
237
|
+
f.flock(File::LOCK_SH)
|
238
|
+
configs = PrefabProto::Configs.decode_json(f.read)
|
239
|
+
load_configs(configs, :datafile)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# A thread that checks for a checkpoint
|
244
|
+
def start_checkpointing_thread
|
245
|
+
Thread.new do
|
246
|
+
loop do
|
247
|
+
started_at = Time.now
|
248
|
+
delta = @checkpoint_freq_secs - (Time.now - started_at)
|
249
|
+
sleep(delta) if delta > 0
|
250
|
+
|
251
|
+
load_checkpoint
|
252
|
+
rescue StandardError => e
|
253
|
+
LOG.debug "Issue Checkpointing #{e.message}"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def finish_init!(source, project_id)
|
259
|
+
return if initialized?
|
260
|
+
|
261
|
+
LOG.debug "Unlocked Config via #{source}"
|
262
|
+
@initialization_lock.count_down
|
263
|
+
|
264
|
+
presenter = Reforge::ConfigClientPresenter.new(
|
265
|
+
size: @config_resolver.local_store.size,
|
266
|
+
source: source,
|
267
|
+
project_id: project_id,
|
268
|
+
project_env_id: @config_resolver.project_env_id,
|
269
|
+
sdk_key_id: @base_client.options.sdk_key_id
|
270
|
+
)
|
271
|
+
LOG.info presenter.to_s
|
272
|
+
LOG.debug to_s
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class ConfigClientPresenter
|
5
|
+
def initialize(size:, source:, project_id:, project_env_id:, sdk_key_id:)
|
6
|
+
@size = size
|
7
|
+
@source = source
|
8
|
+
@project_id = project_id
|
9
|
+
@project_env_id = project_env_id
|
10
|
+
@sdk_key_id = sdk_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.sdk-key-id=#{@sdk_key_id}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class ConfigLoader
|
5
|
+
LOG = Reforge::InternalLogger.new(self)
|
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
|
+
{}
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_local_overrides
|
62
|
+
return {} if @prefab_options.datafile?
|
63
|
+
{}
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
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
|
+
def initialize(base_client, config_loader)
|
9
|
+
@lock = Concurrent::ReadWriteLock.new
|
10
|
+
@local_store = {}
|
11
|
+
@config_loader = config_loader
|
12
|
+
@project_env_id = 0 # we don't know this yet, it is set from the API results
|
13
|
+
@base_client = base_client
|
14
|
+
@on_update = nil
|
15
|
+
make_local
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
presenter.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def presenter
|
23
|
+
Reforge::ResolvedConfigPresenter.new(self, @lock, @local_store)
|
24
|
+
end
|
25
|
+
|
26
|
+
def keys
|
27
|
+
@local_store.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
def raw(key)
|
31
|
+
@local_store.dig(key, :config)
|
32
|
+
end
|
33
|
+
|
34
|
+
def get(key, properties = NO_DEFAULT_PROVIDED)
|
35
|
+
@lock.with_read_lock do
|
36
|
+
raw_config = raw(key)
|
37
|
+
|
38
|
+
return nil unless raw_config
|
39
|
+
|
40
|
+
evaluate(raw_config, properties)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def evaluate(config, properties = NO_DEFAULT_PROVIDED)
|
45
|
+
Reforge::CriteriaEvaluator.new(config,
|
46
|
+
project_env_id: @project_env_id,
|
47
|
+
resolver: self,
|
48
|
+
namespace: @base_client.options.namespace,
|
49
|
+
base_client: @base_client).evaluate(make_context(properties))
|
50
|
+
end
|
51
|
+
|
52
|
+
def update
|
53
|
+
make_local
|
54
|
+
|
55
|
+
@on_update ? @on_update.call : nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_update(&block)
|
59
|
+
@on_update = block
|
60
|
+
end
|
61
|
+
|
62
|
+
def make_context(properties)
|
63
|
+
if properties.is_a?(Context)
|
64
|
+
properties
|
65
|
+
elsif properties == NO_DEFAULT_PROVIDED || properties.nil?
|
66
|
+
Context.current
|
67
|
+
else
|
68
|
+
Context.join(parent: Context.current, hash: properties, id: :jit)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def symbolize_json_names?
|
73
|
+
@base_client.options.symbolize_json_names
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def make_local
|
79
|
+
@lock.with_write_lock do
|
80
|
+
@local_store = @config_loader.calc_config
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class ConfigValueUnwrapper
|
5
|
+
LOG = Reforge::InternalLogger.new(self)
|
6
|
+
CONFIDENTIAL_PREFIX = "*****"
|
7
|
+
attr_reader :weighted_value_index
|
8
|
+
|
9
|
+
def initialize(config_value, resolver, weighted_value_index = nil)
|
10
|
+
@config_value = config_value
|
11
|
+
@resolver = resolver
|
12
|
+
@weighted_value_index = weighted_value_index
|
13
|
+
end
|
14
|
+
|
15
|
+
def reportable_wrapped_value
|
16
|
+
if @config_value.confidential || @config_value.decrypt_with&.length&.positive?
|
17
|
+
# Unique hash for differentiation
|
18
|
+
Reforge::ConfigValueWrapper.wrap("#{CONFIDENTIAL_PREFIX}#{Digest::MD5.hexdigest(unwrap)[0, 5]}")
|
19
|
+
else
|
20
|
+
@config_value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def reportable_value
|
25
|
+
Reforge::ConfigValueUnwrapper.new(reportable_wrapped_value, @resolver, @weighted_value_index).unwrap
|
26
|
+
end
|
27
|
+
|
28
|
+
def raw_config_value
|
29
|
+
@config_value
|
30
|
+
end
|
31
|
+
|
32
|
+
# this will return the actual value of confidential, use reportable_value unless you need it
|
33
|
+
def unwrap(raw_json: false)
|
34
|
+
raw = case @config_value.type
|
35
|
+
when :int, :string, :double, :bool, :log_level
|
36
|
+
@config_value.public_send(@config_value.type)
|
37
|
+
when :string_list
|
38
|
+
@config_value.string_list.values
|
39
|
+
when :duration
|
40
|
+
Reforge::Duration.new(@config_value.duration.definition)
|
41
|
+
when :json
|
42
|
+
if raw_json
|
43
|
+
@config_value.json.json
|
44
|
+
else
|
45
|
+
JSON.parse(@config_value.json.json, symbolize_names: @resolver.symbolize_json_names?)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
LOG.error "Unknown type: #{@config_value.type}"
|
49
|
+
raise "Unknown type: #{@config_value.type}"
|
50
|
+
end
|
51
|
+
if @config_value.has_decrypt_with?
|
52
|
+
decryption_key = @resolver.get(@config_value.decrypt_with)&.unwrapped_value
|
53
|
+
if decryption_key.nil?
|
54
|
+
LOG.warn "No value for decryption key #{@config_value.decrypt_with} found."
|
55
|
+
return ""
|
56
|
+
else
|
57
|
+
unencrypted = Reforge::Encryption.new(decryption_key).decrypt(raw)
|
58
|
+
return unencrypted
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
raw
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.deepest_value(config_value, config, context, resolver)
|
66
|
+
if config_value&.type == :weighted_values
|
67
|
+
value, index = Reforge::WeightedValueResolver.new(
|
68
|
+
config_value.weighted_values.weighted_values,
|
69
|
+
config.key,
|
70
|
+
context.get(config_value.weighted_values.hash_by_property_name)
|
71
|
+
).resolve
|
72
|
+
|
73
|
+
new(deepest_value(value.value, config, context, resolver).raw_config_value, resolver, index)
|
74
|
+
|
75
|
+
elsif config_value&.type == :provided
|
76
|
+
if :ENV_VAR == config_value.provided.source
|
77
|
+
raw = ENV[config_value.provided.lookup]
|
78
|
+
if raw.nil?
|
79
|
+
raise Reforge::Errors::MissingEnvVarError.new("Missing environment variable #{config_value.provided.lookup}")
|
80
|
+
else
|
81
|
+
coerced = coerce_into_type(raw, config, config_value.provided.lookup)
|
82
|
+
new(Reforge::ConfigValueWrapper.wrap(coerced, confidential: config_value.confidential), resolver)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
raise "Unknown Provided Source #{config_value.provided.source}"
|
86
|
+
end
|
87
|
+
else
|
88
|
+
new(config_value, resolver)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Don't allow env vars to resolve to a value_type other than the config's value_type
|
93
|
+
def self.coerce_into_type(value_string, config, env_var_name)
|
94
|
+
case config.value_type
|
95
|
+
when :INT then Integer(value_string)
|
96
|
+
when :DOUBLE then Float(value_string)
|
97
|
+
when :STRING then String(value_string)
|
98
|
+
when :STRING_LIST then
|
99
|
+
maybe_string_list = YAML.load(value_string)
|
100
|
+
case maybe_string_list
|
101
|
+
when Array
|
102
|
+
maybe_string_list
|
103
|
+
else
|
104
|
+
raise raise Reforge::Errors::EnvVarParseError.new(value_string, config, env_var_name)
|
105
|
+
end
|
106
|
+
when :BOOL then
|
107
|
+
maybe_bool = YAML.load(value_string)
|
108
|
+
case maybe_bool
|
109
|
+
when TrueClass, FalseClass
|
110
|
+
maybe_bool
|
111
|
+
else
|
112
|
+
raise Reforge::Errors::EnvVarParseError.new(value_string, config, env_var_name)
|
113
|
+
end
|
114
|
+
when :NOT_SET_VALUE_TYPE
|
115
|
+
YAML.load(value_string)
|
116
|
+
else
|
117
|
+
raise Reforge::Errors::EnvVarParseError.new(value_string, config, env_var_name)
|
118
|
+
end
|
119
|
+
rescue ArgumentError
|
120
|
+
raise Reforge::Errors::EnvVarParseError.new(value_string, config, env_var_name)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Reforge
|
2
|
+
class ConfigValueWrapper
|
3
|
+
def self.wrap(value, confidential: nil)
|
4
|
+
case value
|
5
|
+
when Integer
|
6
|
+
PrefabProto::ConfigValue.new(int: value, confidential: confidential)
|
7
|
+
when Float
|
8
|
+
PrefabProto::ConfigValue.new(double: value, confidential: confidential)
|
9
|
+
when TrueClass, FalseClass
|
10
|
+
PrefabProto::ConfigValue.new(bool: value, confidential: confidential)
|
11
|
+
when Array
|
12
|
+
PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: value.map(&:to_s)), confidential: confidential)
|
13
|
+
else
|
14
|
+
PrefabProto::ConfigValue.new(string: value.to_s, confidential: confidential)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|