prefab-cloud-ruby 0.20.0 → 0.22.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 +4 -4
- data/.envrc.sample +3 -0
- data/.github/workflows/ruby.yml +5 -1
- data/.gitmodules +3 -0
- data/Gemfile +14 -12
- data/Gemfile.lock +24 -14
- data/README.md +12 -10
- data/Rakefile +13 -14
- data/VERSION +1 -1
- data/lib/prefab/auth_interceptor.rb +2 -1
- data/lib/prefab/cancellable_interceptor.rb +8 -7
- data/lib/prefab/client.rb +52 -27
- data/lib/prefab/config_client.rb +59 -70
- data/lib/prefab/config_loader.rb +7 -114
- data/lib/prefab/config_resolver.rb +27 -57
- data/lib/prefab/config_value_unwrapper.rb +23 -0
- data/lib/prefab/criteria_evaluator.rb +96 -0
- data/lib/prefab/errors/invalid_api_key_error.rb +1 -1
- data/lib/prefab/feature_flag_client.rb +13 -145
- data/lib/prefab/internal_logger.rb +7 -6
- data/lib/prefab/local_config_parser.rb +110 -0
- data/lib/prefab/log_path_collector.rb +98 -0
- data/lib/prefab/logger_client.rb +46 -44
- data/lib/prefab/murmer3.rb +3 -4
- data/lib/prefab/noop_cache.rb +5 -7
- data/lib/prefab/noop_stats.rb +2 -3
- data/lib/prefab/options.rb +32 -11
- data/lib/prefab/ratelimit_client.rb +11 -13
- data/lib/prefab/sse_logger.rb +3 -2
- data/lib/prefab/weighted_value_resolver.rb +42 -0
- data/lib/prefab/yaml_config_parser.rb +32 -0
- data/lib/prefab-cloud-ruby.rb +7 -2
- data/lib/prefab_pb.rb +70 -43
- data/lib/prefab_services_pb.rb +14 -1
- data/prefab-cloud-ruby.gemspec +33 -19
- data/test/.prefab.unit_tests.config.yaml +3 -2
- data/test/integration_test.rb +98 -0
- data/test/integration_test_helpers.rb +37 -0
- data/test/test_client.rb +56 -31
- data/test/test_config_client.rb +21 -20
- data/test/test_config_loader.rb +48 -37
- data/test/test_config_resolver.rb +312 -135
- data/test/test_config_value_unwrapper.rb +83 -0
- data/test/test_criteria_evaluator.rb +533 -0
- data/test/test_feature_flag_client.rb +35 -347
- data/test/test_helper.rb +18 -14
- data/test/test_integration.rb +33 -0
- data/test/test_local_config_parser.rb +78 -0
- data/test/test_log_path_collector.rb +56 -0
- data/test/test_logger.rb +52 -51
- data/test/test_options.rb +32 -0
- data/test/test_weighted_value_resolver.rb +65 -0
- metadata +30 -16
- data/lib/prefab/config_helper.rb +0 -31
- data/run_test_harness_server.sh +0 -8
- data/test/harness_server.rb +0 -64
data/lib/prefab/config_client.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Prefab
|
3
4
|
class ConfigClient
|
4
|
-
include Prefab::ConfigHelper
|
5
|
-
|
6
5
|
RECONNECT_WAIT = 5
|
7
6
|
DEFAULT_CHECKPOINT_FREQ_SEC = 60
|
8
7
|
SSE_READ_TIMEOUT = 300
|
9
|
-
AUTH_USER =
|
8
|
+
AUTH_USER = 'authuser'
|
10
9
|
|
11
10
|
def initialize(base_client, timeout)
|
12
11
|
@base_client = base_client
|
13
12
|
@options = base_client.options
|
14
|
-
@base_client.log_internal Logger::DEBUG,
|
13
|
+
@base_client.log_internal ::Logger::DEBUG, 'Initialize ConfigClient'
|
15
14
|
@timeout = timeout
|
16
15
|
|
17
16
|
@stream_lock = Concurrent::ReadWriteLock.new
|
@@ -22,9 +21,9 @@ module Prefab
|
|
22
21
|
@config_resolver = Prefab::ConfigResolver.new(@base_client, @config_loader)
|
23
22
|
|
24
23
|
@initialization_lock = Concurrent::ReadWriteLock.new
|
25
|
-
@base_client.log_internal Logger::DEBUG,
|
24
|
+
@base_client.log_internal ::Logger::DEBUG, 'Initialize ConfigClient: AcquireWriteLock'
|
26
25
|
@initialization_lock.acquire_write_lock
|
27
|
-
@base_client.log_internal Logger::DEBUG,
|
26
|
+
@base_client.log_internal ::Logger::DEBUG, 'Initialize ConfigClient: AcquiredWriteLock'
|
28
27
|
@initialized_future = Concurrent::Future.execute { @initialization_lock.acquire_read_lock }
|
29
28
|
|
30
29
|
@cancellable_interceptor = Prefab::CancellableInterceptor.new(@base_client)
|
@@ -45,14 +44,15 @@ module Prefab
|
|
45
44
|
end
|
46
45
|
|
47
46
|
def upsert(key, config_value, namespace = nil, previous_key = nil)
|
48
|
-
raise "Key must not contain ':' set namespaces separately" if key.include?
|
49
|
-
raise "Namespace must not contain ':'" if namespace&.include?(
|
47
|
+
raise "Key must not contain ':' set namespaces separately" if key.include? ':'
|
48
|
+
raise "Namespace must not contain ':'" if namespace&.include?(':')
|
49
|
+
|
50
50
|
config_delta = Prefab::ConfigClient.value_to_delta(key, config_value, namespace)
|
51
51
|
upsert_req = Prefab::UpsertRequest.new(config_delta: config_delta)
|
52
52
|
upsert_req.previous_key = previous_key if previous_key&.present?
|
53
53
|
|
54
54
|
@base_client.request Prefab::ConfigService, :upsert, req_options: { timeout: @timeout }, params: upsert_req
|
55
|
-
@base_client.stats.increment(
|
55
|
+
@base_client.stats.increment('prefab.config.upsert')
|
56
56
|
@config_loader.set(config_delta, :upsert)
|
57
57
|
@config_loader.rm(previous_key) if previous_key&.present?
|
58
58
|
@config_resolver.update
|
@@ -68,46 +68,43 @@ module Prefab
|
|
68
68
|
end
|
69
69
|
|
70
70
|
def self.value_to_delta(key, config_value, namespace = nil)
|
71
|
-
Prefab::Config.new(key: [namespace, key].compact.join(
|
71
|
+
Prefab::Config.new(key: [namespace, key].compact.join(':'),
|
72
72
|
rows: [Prefab::ConfigRow.new(value: config_value)])
|
73
73
|
end
|
74
74
|
|
75
|
-
def get(key, default=Prefab::Client::NO_DEFAULT_PROVIDED)
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
def get_config_obj(key)
|
81
|
-
config = _get(key)
|
82
|
-
config ? config[:config] : nil
|
75
|
+
def get(key, default = Prefab::Client::NO_DEFAULT_PROVIDED, properties = {}, lookup_key = nil)
|
76
|
+
value = _get(key, lookup_key, properties)
|
77
|
+
value ? Prefab::ConfigValueUnwrapper.unwrap(value, key, properties) : handle_default(key, default)
|
83
78
|
end
|
84
79
|
|
85
80
|
private
|
86
81
|
|
82
|
+
def raw(key)
|
83
|
+
@config_resolver.raw(key)
|
84
|
+
end
|
85
|
+
|
87
86
|
def handle_default(key, default)
|
88
|
-
if default != Prefab::Client::NO_DEFAULT_PROVIDED
|
89
|
-
return default
|
90
|
-
end
|
87
|
+
return default if default != Prefab::Client::NO_DEFAULT_PROVIDED
|
91
88
|
|
92
|
-
if @options.on_no_default == Prefab::Options::ON_NO_DEFAULT::RAISE
|
93
|
-
raise Prefab::Errors::MissingDefaultError.new(key)
|
94
|
-
end
|
89
|
+
raise Prefab::Errors::MissingDefaultError, key if @options.on_no_default == Prefab::Options::ON_NO_DEFAULT::RAISE
|
95
90
|
|
96
91
|
nil
|
97
92
|
end
|
98
93
|
|
99
|
-
def _get(key)
|
94
|
+
def _get(key, lookup_key, properties)
|
100
95
|
# wait timeout sec for the initalization to be complete
|
101
96
|
@initialized_future.value(@options.initialization_timeout_sec)
|
102
97
|
if @initialized_future.incomplete?
|
103
|
-
|
104
|
-
@base_client.log_internal Logger::WARN, "Couldn't Initialize In #{@options.initialization_timeout_sec}. Key #{key}. Returning what we have"
|
105
|
-
@initialization_lock.release_write_lock
|
106
|
-
else
|
98
|
+
unless @options.on_init_failure == Prefab::Options::ON_INITIALIZATION_FAILURE::RETURN
|
107
99
|
raise Prefab::Errors::InitializationTimeoutError.new(@options.initialization_timeout_sec, key)
|
108
100
|
end
|
101
|
+
|
102
|
+
@base_client.log_internal ::Logger::WARN,
|
103
|
+
"Couldn't Initialize In #{@options.initialization_timeout_sec}. Key #{key}. Returning what we have"
|
104
|
+
@initialization_lock.release_write_lock
|
105
|
+
|
109
106
|
end
|
110
|
-
@config_resolver.
|
107
|
+
@config_resolver.get(key, lookup_key, properties)
|
111
108
|
end
|
112
109
|
|
113
110
|
def stub
|
@@ -120,19 +117,15 @@ module Prefab
|
|
120
117
|
def load_checkpoint
|
121
118
|
success = load_checkpoint_api_cdn
|
122
119
|
|
123
|
-
if success
|
124
|
-
|
125
|
-
|
126
|
-
@base_client.log_internal Logger::INFO, "LoadCheckpoint: Fallback to GRPC API"
|
127
|
-
end
|
120
|
+
return if success
|
121
|
+
|
122
|
+
@base_client.log_internal ::Logger::INFO, 'LoadCheckpoint: Fallback to GRPC API'
|
128
123
|
|
129
124
|
success = load_checkpoint_from_grpc_api
|
130
125
|
|
131
|
-
if success
|
132
|
-
|
133
|
-
|
134
|
-
@base_client.log_internal Logger::WARN, "No success loading checkpoints"
|
135
|
-
end
|
126
|
+
return if success
|
127
|
+
|
128
|
+
@base_client.log_internal ::Logger::WARN, 'No success loading checkpoints'
|
136
129
|
end
|
137
130
|
|
138
131
|
def load_checkpoint_from_grpc_api
|
@@ -142,9 +135,9 @@ module Prefab
|
|
142
135
|
load_configs(resp, :remote_api_grpc)
|
143
136
|
true
|
144
137
|
rescue GRPC::Unauthenticated
|
145
|
-
@base_client.log_internal Logger::WARN,
|
146
|
-
rescue => e
|
147
|
-
@base_client.log_internal Logger::WARN, "Unexpected grpc_api problem loading checkpoint #{e}"
|
138
|
+
@base_client.log_internal ::Logger::WARN, 'Unauthenticated'
|
139
|
+
rescue StandardError => e
|
140
|
+
@base_client.log_internal ::Logger::WARN, "Unexpected grpc_api problem loading checkpoint #{e}"
|
148
141
|
false
|
149
142
|
end
|
150
143
|
|
@@ -169,11 +162,11 @@ module Prefab
|
|
169
162
|
load_configs(configs, source)
|
170
163
|
true
|
171
164
|
else
|
172
|
-
@base_client.log_internal Logger::INFO, "Checkpoint #{source} failed to load. Response #{resp.status}"
|
165
|
+
@base_client.log_internal ::Logger::INFO, "Checkpoint #{source} failed to load. Response #{resp.status}"
|
173
166
|
false
|
174
167
|
end
|
175
|
-
rescue => e
|
176
|
-
@base_client.log_internal Logger::WARN, "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
|
168
|
+
rescue StandardError => e
|
169
|
+
@base_client.log_internal ::Logger::WARN, "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
|
177
170
|
false
|
178
171
|
end
|
179
172
|
|
@@ -187,42 +180,39 @@ module Prefab
|
|
187
180
|
@config_loader.set(config, source)
|
188
181
|
end
|
189
182
|
if @config_loader.highwater_mark > starting_highwater_mark
|
190
|
-
@base_client.log_internal Logger::INFO,
|
183
|
+
@base_client.log_internal ::Logger::INFO,
|
184
|
+
"Found new checkpoint with highwater id #{@config_loader.highwater_mark} from #{source} in project #{project_id} environment: #{project_env_id} and namespace: '#{@namespace}'"
|
191
185
|
else
|
192
|
-
@base_client.log_internal Logger::DEBUG,
|
186
|
+
@base_client.log_internal ::Logger::DEBUG,
|
187
|
+
"Checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}. No changes.", 'load_configs'
|
193
188
|
end
|
194
|
-
@base_client.stats.increment(
|
189
|
+
@base_client.stats.increment('prefab.config.checkpoint.load')
|
195
190
|
@config_resolver.update
|
196
191
|
finish_init!(source)
|
197
192
|
end
|
198
193
|
|
199
194
|
# A thread that checks for a checkpoint
|
200
195
|
def start_checkpointing_thread
|
201
|
-
|
202
196
|
Thread.new do
|
203
197
|
loop do
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
end
|
212
|
-
rescue StandardError => exn
|
213
|
-
@base_client.log_internal Logger::INFO, "Issue Checkpointing #{exn.message}"
|
214
|
-
end
|
198
|
+
load_checkpoint
|
199
|
+
|
200
|
+
started_at = Time.now
|
201
|
+
delta = @checkpoint_freq_secs - (Time.now - started_at)
|
202
|
+
sleep(delta) if delta > 0
|
203
|
+
rescue StandardError => e
|
204
|
+
@base_client.log_internal ::Logger::INFO, "Issue Checkpointing #{e.message}"
|
215
205
|
end
|
216
206
|
end
|
217
207
|
end
|
218
208
|
|
219
209
|
def finish_init!(source)
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
210
|
+
return unless @initialization_lock.write_locked?
|
211
|
+
|
212
|
+
@base_client.log_internal ::Logger::INFO, "Unlocked Config via #{source}"
|
213
|
+
@initialization_lock.release_write_lock
|
214
|
+
@base_client.log.set_config_client(self)
|
215
|
+
@base_client.log_internal ::Logger::INFO, to_s
|
226
216
|
end
|
227
217
|
|
228
218
|
def start_sse_streaming_connection_thread(start_at_id)
|
@@ -230,10 +220,10 @@ module Prefab
|
|
230
220
|
auth_string = Base64.strict_encode64(auth)
|
231
221
|
headers = {
|
232
222
|
"x-prefab-start-at-id": start_at_id,
|
233
|
-
"Authorization": "Basic #{auth_string}"
|
223
|
+
"Authorization": "Basic #{auth_string}"
|
234
224
|
}
|
235
225
|
url = "#{@base_client.prefab_api_url}/api/v1/sse/config"
|
236
|
-
@base_client.log_internal Logger::INFO, "SSE Streaming Connect to #{url} start_at #{start_at_id}"
|
226
|
+
@base_client.log_internal ::Logger::INFO, "SSE Streaming Connect to #{url} start_at #{start_at_id}"
|
237
227
|
@streaming_thread = SSE::Client.new(url,
|
238
228
|
headers: headers,
|
239
229
|
read_timeout: SSE_READ_TIMEOUT,
|
@@ -246,4 +236,3 @@ module Prefab
|
|
246
236
|
end
|
247
237
|
end
|
248
238
|
end
|
249
|
-
|
data/lib/prefab/config_loader.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'yaml'
|
4
3
|
module Prefab
|
5
4
|
class ConfigLoader
|
6
5
|
attr_reader :highwater_mark
|
@@ -19,21 +18,19 @@ module Prefab
|
|
19
18
|
@api_config.each_key do |k|
|
20
19
|
rtn[k] = @api_config[k]
|
21
20
|
end
|
22
|
-
rtn
|
23
|
-
rtn
|
21
|
+
rtn.merge(@local_overrides)
|
24
22
|
end
|
25
23
|
|
26
24
|
def set(config, source)
|
27
25
|
# don't overwrite newer values
|
28
|
-
if @api_config[config.key] && @api_config[config.key][:config].id >= config.id
|
29
|
-
return
|
30
|
-
end
|
26
|
+
return if @api_config[config.key] && @api_config[config.key][:config].id >= config.id
|
31
27
|
|
32
28
|
if config.rows.empty?
|
33
29
|
@api_config.delete(config.key)
|
34
30
|
else
|
35
31
|
if @api_config[config.key]
|
36
|
-
@base_client.log_internal Logger::DEBUG,
|
32
|
+
@base_client.log_internal ::Logger::DEBUG,
|
33
|
+
"Replace #{config.key} with value from #{source} #{@api_config[config.key][:config].id} -> #{config.id}"
|
37
34
|
end
|
38
35
|
@api_config[config.key] = { source: source, config: config }
|
39
36
|
end
|
@@ -56,7 +53,7 @@ module Prefab
|
|
56
53
|
|
57
54
|
def load_classpath_config
|
58
55
|
classpath_dir = @prefab_options.prefab_config_classpath_dir
|
59
|
-
rtn = load_glob(File.join(classpath_dir,
|
56
|
+
rtn = load_glob(File.join(classpath_dir, '.prefab.default.config.yaml'))
|
60
57
|
@prefab_options.prefab_envs.each do |env|
|
61
58
|
rtn = rtn.merge load_glob(File.join(classpath_dir, ".prefab.#{env}.config.yaml"))
|
62
59
|
end
|
@@ -65,7 +62,7 @@ module Prefab
|
|
65
62
|
|
66
63
|
def load_local_overrides
|
67
64
|
override_dir = @prefab_options.prefab_config_override_dir
|
68
|
-
rtn = load_glob(File.join(override_dir,
|
65
|
+
rtn = load_glob(File.join(override_dir, '.prefab.default.config.yaml'))
|
69
66
|
@prefab_options.prefab_envs.each do |env|
|
70
67
|
rtn = rtn.merge load_glob(File.join(override_dir, ".prefab.#{env}.config.yaml"))
|
71
68
|
end
|
@@ -75,113 +72,9 @@ module Prefab
|
|
75
72
|
def load_glob(glob)
|
76
73
|
rtn = {}
|
77
74
|
Dir.glob(glob).each do |file|
|
78
|
-
@base_client.
|
79
|
-
yaml = load(file)
|
80
|
-
yaml.each do |k, v|
|
81
|
-
load_kv(k, v, rtn, file)
|
82
|
-
end
|
75
|
+
Prefab::YAMLConfigParser.new(file, @base_client).merge(rtn)
|
83
76
|
end
|
84
77
|
rtn
|
85
78
|
end
|
86
|
-
|
87
|
-
def load_kv(k, v, rtn, file)
|
88
|
-
if v.class == Hash
|
89
|
-
if v['feature_flag']
|
90
|
-
rtn[k] = feature_flag_config(file, k, v)
|
91
|
-
else
|
92
|
-
v.each do |nest_k, nest_v|
|
93
|
-
nested_key = "#{k}.#{nest_k}"
|
94
|
-
nested_key = k if nest_k == "_"
|
95
|
-
load_kv(nested_key, nest_v, rtn, file)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
else
|
99
|
-
rtn[k] = {
|
100
|
-
source: file,
|
101
|
-
match: "default",
|
102
|
-
config: Prefab::Config.new(
|
103
|
-
key: k,
|
104
|
-
rows: [
|
105
|
-
Prefab::ConfigRow.new(value: Prefab::ConfigValue.new(value_from(k, v)))
|
106
|
-
]
|
107
|
-
)
|
108
|
-
}
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def load(filename)
|
113
|
-
if File.exist? filename
|
114
|
-
@base_client.log_internal Logger::INFO, "Load #{filename}"
|
115
|
-
YAML.load_file(filename)
|
116
|
-
else
|
117
|
-
@base_client.log_internal Logger::INFO, "No file #{filename}"
|
118
|
-
{}
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def value_from(key, raw)
|
123
|
-
case raw
|
124
|
-
when String
|
125
|
-
if key.start_with? Prefab::LoggerClient::BASE_KEY
|
126
|
-
prefab_log_level_resolve = Prefab::LogLevel.resolve(raw.upcase.to_sym) || Prefab::LogLevel::NOT_SET_LOG_LEVEL
|
127
|
-
{ log_level: prefab_log_level_resolve }
|
128
|
-
else
|
129
|
-
{ string: raw }
|
130
|
-
end
|
131
|
-
when Integer
|
132
|
-
{ int: raw }
|
133
|
-
when TrueClass, FalseClass
|
134
|
-
{ bool: raw }
|
135
|
-
when Float
|
136
|
-
{ double: raw }
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
def feature_flag_config(file, key, value)
|
141
|
-
criteria = Prefab::Criteria.new(operator: 'ALWAYS_TRUE')
|
142
|
-
|
143
|
-
if value['criteria']
|
144
|
-
criteria = Prefab::Criteria.new(criteria_values(value['criteria']))
|
145
|
-
end
|
146
|
-
|
147
|
-
row = Prefab::ConfigRow.new(
|
148
|
-
value: Prefab::ConfigValue.new(
|
149
|
-
feature_flag: Prefab::FeatureFlag.new(
|
150
|
-
active: true,
|
151
|
-
inactive_variant_idx: -1, # not supported
|
152
|
-
rules: [
|
153
|
-
Prefab::Rule.new(
|
154
|
-
variant_weights: [
|
155
|
-
Prefab::VariantWeight.new(variant_idx: 0, weight: 1000)
|
156
|
-
],
|
157
|
-
criteria: criteria
|
158
|
-
)
|
159
|
-
]
|
160
|
-
)
|
161
|
-
)
|
162
|
-
)
|
163
|
-
|
164
|
-
unless value.has_key?('value')
|
165
|
-
raise Prefab::Error, "Feature flag config `#{key}` #{file} must have a `value`"
|
166
|
-
end
|
167
|
-
|
168
|
-
{
|
169
|
-
source: file,
|
170
|
-
match: key,
|
171
|
-
config: Prefab::Config.new(
|
172
|
-
key: key,
|
173
|
-
variants: [Prefab::FeatureFlagVariant.new(value_from(key, value['value']))],
|
174
|
-
rows: [row]
|
175
|
-
)
|
176
|
-
}
|
177
|
-
end
|
178
|
-
|
179
|
-
def criteria_values(criteria_hash)
|
180
|
-
if RUBY_VERSION < '2.7'
|
181
|
-
criteria_hash.transform_keys(&:to_sym)
|
182
|
-
else
|
183
|
-
criteria_hash
|
184
|
-
end
|
185
|
-
end
|
186
79
|
end
|
187
80
|
end
|
@@ -1,17 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Prefab
|
3
4
|
class ConfigResolver
|
4
|
-
include Prefab::ConfigHelper
|
5
|
-
NAMESPACE_DELIMITER = "."
|
6
|
-
|
7
5
|
attr_accessor :project_env_id # this will be set by the config_client when it gets an API response
|
8
6
|
|
9
7
|
def initialize(base_client, config_loader)
|
10
8
|
@lock = Concurrent::ReadWriteLock.new
|
11
9
|
@local_store = {}
|
12
|
-
@
|
10
|
+
@additional_properties = { Prefab::CriteriaEvaluator::NAMESPACE_KEY => base_client.options.namespace }
|
13
11
|
@config_loader = config_loader
|
14
|
-
@project_env_id = 0
|
12
|
+
@project_env_id = 0 # we don't know this yet, it is set from the API results
|
13
|
+
@base_client = base_client
|
15
14
|
make_local
|
16
15
|
end
|
17
16
|
|
@@ -22,82 +21,53 @@ module Prefab
|
|
22
21
|
v = @local_store[k]
|
23
22
|
elements = [k.slice(0..49).ljust(50)]
|
24
23
|
if v.nil?
|
25
|
-
elements <<
|
24
|
+
elements << 'tombstone'
|
26
25
|
else
|
27
|
-
|
28
|
-
|
29
|
-
elements <<
|
26
|
+
config = evaluate(v[:config], {})
|
27
|
+
value = Prefab::ConfigValueUnwrapper.unwrap(config, k, {})
|
28
|
+
elements << value.to_s.slice(0..34).ljust(35)
|
29
|
+
elements << value.class.to_s.slice(0..6).ljust(7)
|
30
30
|
elements << "Match: #{v[:match]}".slice(0..29).ljust(30)
|
31
31
|
elements << "Source: #{v[:source]}"
|
32
32
|
end
|
33
|
-
str += elements.join(
|
33
|
+
str += elements.join(' | ') << "\n"
|
34
34
|
end
|
35
35
|
end
|
36
36
|
str
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
40
|
-
|
41
|
-
config ? value_of(config[:value]) : nil
|
42
|
-
end
|
39
|
+
def raw(key)
|
40
|
+
via_key = @local_store[key]
|
43
41
|
|
44
|
-
|
45
|
-
config = _get(property)
|
46
|
-
config ? config[:config] : nil
|
42
|
+
via_key ? via_key[:config] : nil
|
47
43
|
end
|
48
44
|
|
49
|
-
def
|
45
|
+
def get(key, lookup_key, properties = {})
|
50
46
|
@lock.with_read_lock do
|
51
|
-
|
47
|
+
raw_config = raw(key)
|
48
|
+
|
49
|
+
return nil unless raw_config
|
50
|
+
|
51
|
+
evaluate(raw(key), lookup_key, properties)
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
def evaluate(config, lookup_key, properties = {})
|
56
|
+
props = properties.merge(@additional_properties).merge(Prefab::CriteriaEvaluator::LOOKUP_KEY => lookup_key)
|
57
|
+
|
58
|
+
Prefab::CriteriaEvaluator.new(config,
|
59
|
+
project_env_id: @project_env_id, resolver: self, base_client: @base_client).evaluate(props)
|
60
|
+
end
|
61
|
+
|
55
62
|
def update
|
56
63
|
make_local
|
57
64
|
end
|
58
65
|
|
59
66
|
private
|
60
67
|
|
61
|
-
# Should client a.b.c see key in namespace a.b? yes
|
62
|
-
# Should client a.b.c see key in namespace a.b.c? yes
|
63
|
-
# Should client a.b.c see key in namespace a.b.d? no
|
64
|
-
# Should client a.b.c see key in namespace ""? yes
|
65
|
-
#
|
66
|
-
def starts_with_ns?(key_namespace, client_namespace)
|
67
|
-
zipped = key_namespace.split(NAMESPACE_DELIMITER).zip(client_namespace.split(NAMESPACE_DELIMITER))
|
68
|
-
mapped = zipped.map do |k, c|
|
69
|
-
(k.nil? || k.empty?) || k == c
|
70
|
-
end
|
71
|
-
[mapped.all?, mapped.size]
|
72
|
-
end
|
73
|
-
|
74
68
|
def make_local
|
75
|
-
store = {}
|
76
|
-
@config_loader.calc_config.each do |key, config_resolver_obj|
|
77
|
-
config = config_resolver_obj[:config]
|
78
|
-
sortable = config.rows.map do |row|
|
79
|
-
if row.project_env_id != 0
|
80
|
-
if row.project_env_id == @project_env_id
|
81
|
-
if !row.namespace.empty?
|
82
|
-
(starts_with, count) = starts_with_ns?(row.namespace, @namespace)
|
83
|
-
# rubocop:disable BlockNesting
|
84
|
-
{ sortable: 2 + count, match: "nm:#{row.namespace}", value: row.value, config: config} if starts_with
|
85
|
-
else
|
86
|
-
{ sortable: 1, match: "env:#{row.project_env_id}", value: row.value, config: config}
|
87
|
-
end
|
88
|
-
end
|
89
|
-
else
|
90
|
-
match = config_resolver_obj[:match] || "default"
|
91
|
-
{ sortable: 0, match: match, value: row.value, config: config}
|
92
|
-
end
|
93
|
-
end.compact
|
94
|
-
to_store = sortable.sort_by { |h| h[:sortable] }.last
|
95
|
-
to_store[:source] = config_resolver_obj[:source]
|
96
|
-
store[key] = to_store
|
97
|
-
end
|
98
|
-
|
99
69
|
@lock.with_write_lock do
|
100
|
-
@local_store =
|
70
|
+
@local_store = @config_loader.calc_config
|
101
71
|
end
|
102
72
|
end
|
103
73
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class ConfigValueUnwrapper
|
5
|
+
def self.unwrap(config_value, config_key, properties)
|
6
|
+
return nil unless config_value
|
7
|
+
|
8
|
+
case config_value.type
|
9
|
+
when :int, :string, :double, :bool, :log_level
|
10
|
+
config_value.public_send(config_value.type)
|
11
|
+
when :string_list
|
12
|
+
config_value.string_list.values
|
13
|
+
when :weighted_values
|
14
|
+
lookup_key = properties[Prefab::CriteriaEvaluator::LOOKUP_KEY]
|
15
|
+
weights = config_value.weighted_values.weighted_values
|
16
|
+
value = Prefab::WeightedValueResolver.new(weights, config_key, lookup_key).resolve
|
17
|
+
unwrap(value.value, config_key, properties)
|
18
|
+
else
|
19
|
+
raise "Unknown type: #{config_value.type}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class CriteriaEvaluator
|
5
|
+
LOOKUP_KEY = 'LOOKUP'
|
6
|
+
NAMESPACE_KEY = 'NAMESPACE'
|
7
|
+
NO_MATCHING_ROWS = [].freeze
|
8
|
+
|
9
|
+
def initialize(config, project_env_id:, resolver:, base_client:)
|
10
|
+
@config = config
|
11
|
+
@project_env_id = project_env_id
|
12
|
+
@resolver = resolver
|
13
|
+
@base_client = base_client
|
14
|
+
end
|
15
|
+
|
16
|
+
def evaluate(properties)
|
17
|
+
# TODO: optimize this and perhaps do it elsewhere
|
18
|
+
props = properties.transform_keys(&:to_s)
|
19
|
+
|
20
|
+
matching_environment_row_values.each do |conditional_value|
|
21
|
+
return conditional_value.value if all_criteria_match?(conditional_value, props)
|
22
|
+
end
|
23
|
+
|
24
|
+
default_row_values.each do |conditional_value|
|
25
|
+
return conditional_value.value if all_criteria_match?(conditional_value, props)
|
26
|
+
end
|
27
|
+
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def all_criteria_match?(conditional_value, props)
|
32
|
+
conditional_value.criteria.all? do |criterion|
|
33
|
+
evaluate_criteron(criterion, props)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def evaluate_criteron(criterion, properties)
|
38
|
+
value_from_properties = properties[criterion.property_name]
|
39
|
+
|
40
|
+
case criterion.operator
|
41
|
+
when :LOOKUP_KEY_IN, :PROP_IS_ONE_OF
|
42
|
+
matches?(criterion, value_from_properties, properties)
|
43
|
+
when :LOOKUP_KEY_NOT_IN, :PROP_IS_NOT_ONE_OF
|
44
|
+
!matches?(criterion, value_from_properties, properties)
|
45
|
+
when :IN_SEG
|
46
|
+
in_segment?(criterion, properties)
|
47
|
+
when :NOT_IN_SEG
|
48
|
+
!in_segment?(criterion, properties)
|
49
|
+
when :PROP_ENDS_WITH_ONE_OF
|
50
|
+
return false unless value_from_properties
|
51
|
+
|
52
|
+
criterion.value_to_match.string_list.values.any? do |ending|
|
53
|
+
value_from_properties.end_with?(ending)
|
54
|
+
end
|
55
|
+
when :PROP_DOES_NOT_END_WITH_ONE_OF
|
56
|
+
return true unless value_from_properties
|
57
|
+
|
58
|
+
criterion.value_to_match.string_list.values.none? do |ending|
|
59
|
+
value_from_properties.end_with?(ending)
|
60
|
+
end
|
61
|
+
when :HIERARCHICAL_MATCH
|
62
|
+
value_from_properties.start_with?(criterion.value_to_match.string)
|
63
|
+
when :ALWAYS_TRUE
|
64
|
+
true
|
65
|
+
else
|
66
|
+
@base_client.log.info("Unknown Operator: #{criterion.operator}")
|
67
|
+
false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def matching_environment_row_values
|
74
|
+
@config.rows.find { |row| row.project_env_id == @project_env_id }&.values || NO_MATCHING_ROWS
|
75
|
+
end
|
76
|
+
|
77
|
+
def default_row_values
|
78
|
+
@config.rows.find { |row| row.project_env_id != @project_env_id }&.values || NO_MATCHING_ROWS
|
79
|
+
end
|
80
|
+
|
81
|
+
def in_segment?(criterion, properties)
|
82
|
+
@resolver.get(criterion.value_to_match.string, properties[LOOKUP_KEY], properties).bool
|
83
|
+
end
|
84
|
+
|
85
|
+
def matches?(criterion, value_from_properties, properties)
|
86
|
+
criterion_value_or_values = Prefab::ConfigValueUnwrapper.unwrap(criterion.value_to_match, @config.key, properties)
|
87
|
+
|
88
|
+
case criterion_value_or_values
|
89
|
+
when Google::Protobuf::RepeatedField
|
90
|
+
criterion_value_or_values.include?(value_from_properties)
|
91
|
+
else
|
92
|
+
criterion_value_or_values == value_from_properties
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -5,7 +5,7 @@ module Prefab
|
|
5
5
|
class InvalidApiKeyError < Prefab::Error
|
6
6
|
def initialize(key)
|
7
7
|
if key.nil? || key.empty?
|
8
|
-
message =
|
8
|
+
message = 'No API key. Set PREFAB_API_KEY env var or use PREFAB_DATASOURCES=LOCAL_ONLY'
|
9
9
|
|
10
10
|
super(message)
|
11
11
|
else
|