prefab-cloud-ruby 0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.envrc.sample +3 -0
- data/.github/workflows/ruby.yml +46 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +169 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +188 -0
- data/LICENSE.txt +20 -0
- data/README.md +94 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/bin/console +21 -0
- data/compile_protos.sh +18 -0
- data/lib/prefab/client.rb +153 -0
- data/lib/prefab/config_client.rb +292 -0
- data/lib/prefab/config_client_presenter.rb +18 -0
- data/lib/prefab/config_loader.rb +84 -0
- data/lib/prefab/config_resolver.rb +77 -0
- data/lib/prefab/config_value_unwrapper.rb +115 -0
- data/lib/prefab/config_value_wrapper.rb +18 -0
- data/lib/prefab/context.rb +179 -0
- data/lib/prefab/context_shape.rb +20 -0
- data/lib/prefab/context_shape_aggregator.rb +65 -0
- data/lib/prefab/criteria_evaluator.rb +136 -0
- data/lib/prefab/encryption.rb +65 -0
- data/lib/prefab/error.rb +6 -0
- data/lib/prefab/errors/env_var_parse_error.rb +11 -0
- data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
- data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
- data/lib/prefab/errors/missing_default_error.rb +13 -0
- data/lib/prefab/errors/missing_env_var_error.rb +11 -0
- data/lib/prefab/errors/uninitialized_error.rb +13 -0
- data/lib/prefab/evaluation.rb +52 -0
- data/lib/prefab/evaluation_summary_aggregator.rb +87 -0
- data/lib/prefab/example_contexts_aggregator.rb +78 -0
- data/lib/prefab/exponential_backoff.rb +21 -0
- data/lib/prefab/feature_flag_client.rb +42 -0
- data/lib/prefab/http_connection.rb +41 -0
- data/lib/prefab/internal_logger.rb +16 -0
- data/lib/prefab/local_config_parser.rb +151 -0
- data/lib/prefab/log_path_aggregator.rb +69 -0
- data/lib/prefab/logger_client.rb +264 -0
- data/lib/prefab/murmer3.rb +50 -0
- data/lib/prefab/options.rb +208 -0
- data/lib/prefab/periodic_sync.rb +69 -0
- data/lib/prefab/prefab.rb +56 -0
- data/lib/prefab/rate_limit_cache.rb +41 -0
- data/lib/prefab/resolved_config_presenter.rb +86 -0
- data/lib/prefab/time_helpers.rb +7 -0
- data/lib/prefab/weighted_value_resolver.rb +42 -0
- data/lib/prefab/yaml_config_parser.rb +34 -0
- data/lib/prefab-cloud-ruby.rb +57 -0
- data/lib/prefab_pb.rb +93 -0
- data/prefab-cloud-ruby.gemspec +155 -0
- data/test/.prefab.default.config.yaml +2 -0
- data/test/.prefab.unit_tests.config.yaml +28 -0
- data/test/integration_test.rb +150 -0
- data/test/integration_test_helpers.rb +151 -0
- data/test/support/common_helpers.rb +180 -0
- data/test/support/mock_base_client.rb +42 -0
- data/test/support/mock_config_client.rb +19 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_client.rb +444 -0
- data/test/test_config_client.rb +109 -0
- data/test/test_config_loader.rb +117 -0
- data/test/test_config_resolver.rb +430 -0
- data/test/test_config_value_unwrapper.rb +224 -0
- data/test/test_config_value_wrapper.rb +42 -0
- data/test/test_context.rb +203 -0
- data/test/test_context_shape.rb +50 -0
- data/test/test_context_shape_aggregator.rb +147 -0
- data/test/test_criteria_evaluator.rb +726 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluation_summary_aggregator.rb +162 -0
- data/test/test_example_contexts_aggregator.rb +238 -0
- data/test/test_exponential_backoff.rb +18 -0
- data/test/test_feature_flag_client.rb +48 -0
- data/test/test_helper.rb +17 -0
- data/test/test_integration.rb +58 -0
- data/test/test_local_config_parser.rb +147 -0
- data/test/test_log_path_aggregator.rb +62 -0
- data/test/test_logger.rb +621 -0
- data/test/test_logger_initialization.rb +12 -0
- data/test/test_options.rb +75 -0
- data/test/test_prefab.rb +12 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_weighted_value_resolver.rb +71 -0
- metadata +337 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class ConfigValueUnwrapper
|
5
|
+
LOG = Prefab::InternalLogger.new(ConfigValueUnwrapper)
|
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
|
+
Prefab::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
|
+
Prefab::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
|
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
|
+
else
|
40
|
+
LOG.error "Unknown type: #{@config_value.type}"
|
41
|
+
raise "Unknown type: #{@config_value.type}"
|
42
|
+
end
|
43
|
+
if @config_value.has_decrypt_with?
|
44
|
+
decryption_key = @resolver.get(@config_value.decrypt_with)&.unwrapped_value
|
45
|
+
if decryption_key.nil?
|
46
|
+
LOG.warn "No value for decryption key #{@config_value.decrypt_with} found."
|
47
|
+
return ""
|
48
|
+
else
|
49
|
+
unencrypted = Prefab::Encryption.new(decryption_key).decrypt(raw)
|
50
|
+
return unencrypted
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
raw
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.deepest_value(config_value, config, context, resolver)
|
58
|
+
if config_value&.type == :weighted_values
|
59
|
+
value, index = Prefab::WeightedValueResolver.new(
|
60
|
+
config_value.weighted_values.weighted_values,
|
61
|
+
config.key,
|
62
|
+
context.get(config_value.weighted_values.hash_by_property_name)
|
63
|
+
).resolve
|
64
|
+
|
65
|
+
new(deepest_value(value.value, config, context, resolver).raw_config_value, resolver, index)
|
66
|
+
|
67
|
+
elsif config_value&.type == :provided
|
68
|
+
if :ENV_VAR == config_value.provided.source
|
69
|
+
raw = ENV[config_value.provided.lookup]
|
70
|
+
if raw.nil?
|
71
|
+
raise Prefab::Errors::MissingEnvVarError.new("Missing environment variable #{config_value.provided.lookup}")
|
72
|
+
else
|
73
|
+
coerced = coerce_into_type(raw, config, config_value.provided.lookup)
|
74
|
+
new(Prefab::ConfigValueWrapper.wrap(coerced, confidential: config_value.confidential), resolver)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
raise "Unknown Provided Source #{config_value.provided.source}"
|
78
|
+
end
|
79
|
+
else
|
80
|
+
new(config_value, resolver)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Don't allow env vars to resolve to a value_type other than the config's value_type
|
85
|
+
def self.coerce_into_type(value_string, config, env_var_name)
|
86
|
+
case config.value_type
|
87
|
+
when :INT then Integer(value_string)
|
88
|
+
when :DOUBLE then Float(value_string)
|
89
|
+
when :STRING then String(value_string)
|
90
|
+
when :STRING_LIST then
|
91
|
+
maybe_string_list = YAML.load(value_string)
|
92
|
+
case maybe_string_list
|
93
|
+
when Array
|
94
|
+
maybe_string_list
|
95
|
+
else
|
96
|
+
raise raise Prefab::Errors::EnvVarParseError.new(value_string, config, env_var_name)
|
97
|
+
end
|
98
|
+
when :BOOL then
|
99
|
+
maybe_bool = YAML.load(value_string)
|
100
|
+
case maybe_bool
|
101
|
+
when TrueClass, FalseClass
|
102
|
+
maybe_bool
|
103
|
+
else
|
104
|
+
raise Prefab::Errors::EnvVarParseError.new(value_string, config, env_var_name)
|
105
|
+
end
|
106
|
+
when :NOT_SET_VALUE_TYPE
|
107
|
+
YAML.load(value_string)
|
108
|
+
else
|
109
|
+
raise Prefab::Errors::EnvVarParseError.new(value_string, config, env_var_name)
|
110
|
+
end
|
111
|
+
rescue ArgumentError
|
112
|
+
raise Prefab::Errors::EnvVarParseError.new(value_string, config, env_var_name)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Prefab
|
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
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class Context
|
5
|
+
BLANK_CONTEXT_NAME = ''
|
6
|
+
|
7
|
+
class NamedContext
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(name, hash)
|
11
|
+
@hash = {}
|
12
|
+
@name = name.to_s
|
13
|
+
|
14
|
+
merge!(hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(parts)
|
18
|
+
@hash[parts]
|
19
|
+
end
|
20
|
+
|
21
|
+
def merge!(other)
|
22
|
+
@hash = @hash.merge(other.transform_keys(&:to_s))
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
26
|
+
@hash
|
27
|
+
end
|
28
|
+
|
29
|
+
def key
|
30
|
+
"#{@name}:#{get('key')}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_proto
|
34
|
+
PrefabProto::Context.new(
|
35
|
+
type: name,
|
36
|
+
values: to_h.transform_values do |value|
|
37
|
+
ConfigValueWrapper.wrap(value)
|
38
|
+
end
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
THREAD_KEY = :prefab_context
|
44
|
+
attr_reader :contexts, :seen_at
|
45
|
+
|
46
|
+
class << self
|
47
|
+
def current=(context)
|
48
|
+
Thread.current[THREAD_KEY] = context
|
49
|
+
end
|
50
|
+
|
51
|
+
def current
|
52
|
+
Thread.current[THREAD_KEY] ||= new
|
53
|
+
end
|
54
|
+
|
55
|
+
def with_context(context)
|
56
|
+
old_context = Thread.current[THREAD_KEY]
|
57
|
+
Thread.current[THREAD_KEY] = new(context)
|
58
|
+
yield
|
59
|
+
ensure
|
60
|
+
Thread.current[THREAD_KEY] = old_context
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_merged_context(context)
|
64
|
+
old_context = Thread.current[THREAD_KEY]
|
65
|
+
Thread.current[THREAD_KEY] = merge_with_current(context)
|
66
|
+
yield
|
67
|
+
ensure
|
68
|
+
Thread.current[THREAD_KEY] = old_context
|
69
|
+
end
|
70
|
+
|
71
|
+
def clear_current
|
72
|
+
Thread.current[THREAD_KEY] = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def merge_with_current(new_context_properties = {})
|
76
|
+
new(current.to_h.merge(new_context_properties.to_h))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def initialize(context = {})
|
81
|
+
@contexts = {}
|
82
|
+
@seen_at = Time.now.utc.to_i
|
83
|
+
|
84
|
+
if context.is_a?(NamedContext)
|
85
|
+
@contexts[context.name] = context
|
86
|
+
elsif context.is_a?(Hash)
|
87
|
+
context.map do |name, values|
|
88
|
+
if values.is_a?(Hash)
|
89
|
+
@contexts[name.to_s] = NamedContext.new(name, values)
|
90
|
+
else
|
91
|
+
warn '[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash.'
|
92
|
+
|
93
|
+
@contexts[BLANK_CONTEXT_NAME] ||= NamedContext.new(BLANK_CONTEXT_NAME, {})
|
94
|
+
@contexts[BLANK_CONTEXT_NAME].merge!({ name => values })
|
95
|
+
end
|
96
|
+
end
|
97
|
+
else
|
98
|
+
raise ArgumentError, 'must be a Hash or a NamedContext'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def blank?
|
103
|
+
contexts.empty?
|
104
|
+
end
|
105
|
+
|
106
|
+
def set(name, hash)
|
107
|
+
@contexts[name.to_s] = NamedContext.new(name, hash)
|
108
|
+
end
|
109
|
+
|
110
|
+
def get(property_key)
|
111
|
+
name, key = property_key.split('.', 2)
|
112
|
+
|
113
|
+
if key.nil?
|
114
|
+
name = BLANK_CONTEXT_NAME
|
115
|
+
key = property_key
|
116
|
+
end
|
117
|
+
|
118
|
+
contexts[name]&.get(key)
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_h
|
122
|
+
contexts.transform_values(&:to_h)
|
123
|
+
end
|
124
|
+
|
125
|
+
def clear
|
126
|
+
@contexts = {}
|
127
|
+
end
|
128
|
+
|
129
|
+
def context(name)
|
130
|
+
contexts[name.to_s] || NamedContext.new(name, {})
|
131
|
+
end
|
132
|
+
|
133
|
+
def merge_default(defaults)
|
134
|
+
defaults.keys.each do |name|
|
135
|
+
set(name, context(name).merge!(defaults[name]))
|
136
|
+
end
|
137
|
+
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_proto(namespace)
|
142
|
+
prefab_context = {
|
143
|
+
'current-time' => ConfigValueWrapper.wrap(Prefab::TimeHelpers.now_in_ms)
|
144
|
+
}
|
145
|
+
|
146
|
+
prefab_context['namespace'] = ConfigValueWrapper.wrap(namespace) if namespace&.length&.positive?
|
147
|
+
|
148
|
+
PrefabProto::ContextSet.new(
|
149
|
+
contexts: contexts.map do |name, context|
|
150
|
+
context.to_proto
|
151
|
+
end.concat([PrefabProto::Context.new(type: 'prefab',
|
152
|
+
values: prefab_context)])
|
153
|
+
)
|
154
|
+
end
|
155
|
+
|
156
|
+
def slim_proto
|
157
|
+
PrefabProto::ContextSet.new(
|
158
|
+
contexts: contexts.map do |_, context|
|
159
|
+
context.to_proto
|
160
|
+
end
|
161
|
+
)
|
162
|
+
end
|
163
|
+
|
164
|
+
def grouped_key
|
165
|
+
contexts.map do |_, context|
|
166
|
+
context.key
|
167
|
+
end.sort.join('|')
|
168
|
+
end
|
169
|
+
|
170
|
+
include Comparable
|
171
|
+
def <=>(other)
|
172
|
+
if other.is_a?(Prefab::Context)
|
173
|
+
to_h <=> other.to_h
|
174
|
+
else
|
175
|
+
super
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Prefab
|
2
|
+
class ContextShape
|
3
|
+
MAPPING = {
|
4
|
+
Integer => 1,
|
5
|
+
String => 2,
|
6
|
+
Float => 4,
|
7
|
+
TrueClass => 5,
|
8
|
+
FalseClass => 5,
|
9
|
+
Array => 10,
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
# We default to String if the type isn't a primitive we support.
|
13
|
+
# This is because we do a `to_s` in the CriteriaEvaluator.
|
14
|
+
DEFAULT = MAPPING[String]
|
15
|
+
|
16
|
+
def self.field_type_number(value)
|
17
|
+
MAPPING.fetch(value.class, DEFAULT)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'periodic_sync'
|
4
|
+
|
5
|
+
module Prefab
|
6
|
+
class ContextShapeAggregator
|
7
|
+
include Prefab::PeriodicSync
|
8
|
+
|
9
|
+
LOG = Prefab::InternalLogger.new(ContextShapeAggregator)
|
10
|
+
|
11
|
+
attr_reader :data
|
12
|
+
|
13
|
+
def initialize(client:, max_shapes:, sync_interval:)
|
14
|
+
@max_shapes = max_shapes
|
15
|
+
@client = client
|
16
|
+
@name = 'context_shape_aggregator'
|
17
|
+
|
18
|
+
@data = Concurrent::Set.new
|
19
|
+
|
20
|
+
start_periodic_sync(sync_interval)
|
21
|
+
end
|
22
|
+
|
23
|
+
def push(context)
|
24
|
+
return if @data.size >= @max_shapes
|
25
|
+
|
26
|
+
context.contexts.each_pair do |name, name_context|
|
27
|
+
name_context.to_h.each_pair do |key, value|
|
28
|
+
@data.add [name, key, Prefab::ContextShape.field_type_number(value)]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def prepare_data
|
34
|
+
duped = @data.dup
|
35
|
+
@data.clear
|
36
|
+
|
37
|
+
duped.inject({}) do |acc, (name, key, type)|
|
38
|
+
acc[name] ||= {}
|
39
|
+
acc[name][key] = type
|
40
|
+
acc
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def flush(to_ship, _)
|
47
|
+
pool.post do
|
48
|
+
LOG.debug "Uploading context shapes for #{to_ship.values.size}"
|
49
|
+
|
50
|
+
shapes = PrefabProto::ContextShapes.new(
|
51
|
+
shapes: to_ship.map do |name, shape|
|
52
|
+
PrefabProto::ContextShape.new(
|
53
|
+
name: name,
|
54
|
+
field_types: shape
|
55
|
+
)
|
56
|
+
end
|
57
|
+
)
|
58
|
+
|
59
|
+
result = post('/api/v1/context-shapes', shapes)
|
60
|
+
|
61
|
+
LOG.debug "Uploaded #{to_ship.values.size} shapes: #{result.status}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Naming/MethodName
|
4
|
+
# We're intentionally keeping the UPCASED method names to match the protobuf
|
5
|
+
# and avoid wasting CPU cycles lowercasing things
|
6
|
+
module Prefab
|
7
|
+
# This class evaluates a config's criteria. `evaluate` returns the value of
|
8
|
+
# the first match based on the provided properties.
|
9
|
+
class CriteriaEvaluator
|
10
|
+
LOG = Prefab::InternalLogger.new(CriteriaEvaluator)
|
11
|
+
NAMESPACE_KEY = 'NAMESPACE'
|
12
|
+
NO_MATCHING_ROWS = [].freeze
|
13
|
+
|
14
|
+
def initialize(config, project_env_id:, resolver:, namespace:, base_client:)
|
15
|
+
@config = config
|
16
|
+
@project_env_id = project_env_id
|
17
|
+
@resolver = resolver
|
18
|
+
@namespace = namespace
|
19
|
+
@base_client = base_client
|
20
|
+
end
|
21
|
+
|
22
|
+
def evaluate(properties)
|
23
|
+
rtn = evaluate_for_env(@project_env_id, properties) ||
|
24
|
+
evaluate_for_env(0, properties)
|
25
|
+
LOG.debug "Eval Key #{@config.key} Result #{rtn&.reportable_value} with #{properties.to_h}" unless @config.config_type == :LOG_LEVEL
|
26
|
+
rtn
|
27
|
+
end
|
28
|
+
|
29
|
+
def all_criteria_match?(conditional_value, props)
|
30
|
+
conditional_value.criteria.all? do |criterion|
|
31
|
+
public_send(criterion.operator, criterion, props)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def IN_SEG(criterion, properties)
|
36
|
+
in_segment?(criterion, properties)
|
37
|
+
end
|
38
|
+
|
39
|
+
def NOT_IN_SEG(criterion, properties)
|
40
|
+
!in_segment?(criterion, properties)
|
41
|
+
end
|
42
|
+
|
43
|
+
def ALWAYS_TRUE(_criterion, _properties)
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def PROP_IS_ONE_OF(criterion, properties)
|
48
|
+
matches?(criterion, value_from_properties(criterion, properties), properties)
|
49
|
+
end
|
50
|
+
|
51
|
+
def PROP_IS_NOT_ONE_OF(criterion, properties)
|
52
|
+
!matches?(criterion, value_from_properties(criterion, properties), properties)
|
53
|
+
end
|
54
|
+
|
55
|
+
def PROP_ENDS_WITH_ONE_OF(criterion, properties)
|
56
|
+
prop_ends_with_one_of?(criterion, value_from_properties(criterion, properties))
|
57
|
+
end
|
58
|
+
|
59
|
+
def PROP_DOES_NOT_END_WITH_ONE_OF(criterion, properties)
|
60
|
+
!prop_ends_with_one_of?(criterion, value_from_properties(criterion, properties))
|
61
|
+
end
|
62
|
+
|
63
|
+
def HIERARCHICAL_MATCH(criterion, properties)
|
64
|
+
value = value_from_properties(criterion, properties)
|
65
|
+
value&.start_with?(criterion.value_to_match.string)
|
66
|
+
end
|
67
|
+
|
68
|
+
def IN_INT_RANGE(criterion, properties)
|
69
|
+
value = if criterion.property_name == 'prefab.current-time'
|
70
|
+
Time.now.utc.to_i * 1000
|
71
|
+
else
|
72
|
+
value_from_properties(criterion, properties)
|
73
|
+
end
|
74
|
+
|
75
|
+
value && value >= criterion.value_to_match.int_range.start && value < criterion.value_to_match.int_range.end
|
76
|
+
end
|
77
|
+
|
78
|
+
def value_from_properties(criterion, properties)
|
79
|
+
criterion.property_name == NAMESPACE_KEY ? @namespace : properties.get(criterion.property_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def evaluate_for_env(env_id, properties)
|
85
|
+
@config.rows.each_with_index do |row, index|
|
86
|
+
next unless row.project_env_id == env_id
|
87
|
+
|
88
|
+
row.values.each_with_index do |conditional_value, value_index|
|
89
|
+
next unless all_criteria_match?(conditional_value, properties)
|
90
|
+
|
91
|
+
return Prefab::Evaluation.new(
|
92
|
+
config: @config,
|
93
|
+
value: conditional_value.value,
|
94
|
+
value_index: value_index,
|
95
|
+
config_row_index: index,
|
96
|
+
context: properties,
|
97
|
+
resolver: @resolver
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def in_segment?(criterion, properties)
|
106
|
+
segment = @resolver.get(criterion.value_to_match.string, properties)
|
107
|
+
|
108
|
+
@base_client.log.info("Segment #{criterion.value_to_match.string} not found") unless segment
|
109
|
+
|
110
|
+
segment&.report_and_return(@base_client.evaluation_summary_aggregator)
|
111
|
+
end
|
112
|
+
|
113
|
+
def matches?(criterion, value, properties)
|
114
|
+
criterion_value_or_values = Prefab::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config.key,
|
115
|
+
properties, @resolver).unwrap
|
116
|
+
|
117
|
+
case criterion_value_or_values
|
118
|
+
when Google::Protobuf::RepeatedField
|
119
|
+
# we to_s the value from properties for comparison because the
|
120
|
+
# criterion_value_or_values is a list of strings
|
121
|
+
criterion_value_or_values.include?(value.to_s)
|
122
|
+
else
|
123
|
+
criterion_value_or_values == value
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def prop_ends_with_one_of?(criterion, value)
|
128
|
+
return false unless value
|
129
|
+
|
130
|
+
criterion.value_to_match.string_list.values.any? do |ending|
|
131
|
+
value.end_with?(ending)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
# rubocop:enable Naming/MethodName
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class Encryption
|
5
|
+
CIPHER_TYPE = "aes-256-gcm" # 32/12
|
6
|
+
SEPARATOR = "--"
|
7
|
+
|
8
|
+
# Hexadecimal format ensures that generated keys are representable with
|
9
|
+
# plain text
|
10
|
+
#
|
11
|
+
# To convert back to the original string with the desired length:
|
12
|
+
# [ value ].pack("H*")
|
13
|
+
def self.generate_new_hex_key
|
14
|
+
generate_random_key.unpack("H*")[0]
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(key_string_hex)
|
18
|
+
@key = [key_string_hex].pack("H*")
|
19
|
+
end
|
20
|
+
|
21
|
+
def encrypt(clear_text)
|
22
|
+
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
|
23
|
+
cipher.encrypt
|
24
|
+
iv = cipher.random_iv
|
25
|
+
|
26
|
+
# load them into the cipher
|
27
|
+
cipher.key = @key
|
28
|
+
cipher.iv = iv
|
29
|
+
cipher.auth_data = ""
|
30
|
+
|
31
|
+
# encrypt the message
|
32
|
+
encrypted = cipher.update(clear_text)
|
33
|
+
encrypted << cipher.final
|
34
|
+
tag = cipher.auth_tag
|
35
|
+
|
36
|
+
# pack and join
|
37
|
+
[encrypted, iv, tag].map { |p| p.unpack("H*")[0] }.join(SEPARATOR)
|
38
|
+
end
|
39
|
+
|
40
|
+
def decrypt(encrypted_string)
|
41
|
+
unpacked_parts = encrypted_string.split(SEPARATOR).map { |p| [p].pack("H*") }
|
42
|
+
|
43
|
+
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
|
44
|
+
cipher.decrypt
|
45
|
+
cipher.key = @key
|
46
|
+
cipher.iv = unpacked_parts[1]
|
47
|
+
cipher.auth_tag = unpacked_parts[2]
|
48
|
+
|
49
|
+
# and decrypt it
|
50
|
+
decrypted = cipher.update(unpacked_parts[0])
|
51
|
+
decrypted << cipher.final
|
52
|
+
decrypted
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def self.generate_random_key
|
58
|
+
SecureRandom.random_bytes(key_length)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.key_length
|
62
|
+
OpenSSL::Cipher.new(CIPHER_TYPE).key_len
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/prefab/error.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
module Errors
|
5
|
+
class EnvVarParseError < Prefab::Error
|
6
|
+
def initialize(env_var, config, env_var_name)
|
7
|
+
super("Evaluating #{config.key} couldn't coerce #{env_var_name} of #{env_var} to #{config.value_type}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
module Errors
|
5
|
+
class InitializationTimeoutError < Prefab::Error
|
6
|
+
def initialize(timeout_sec, key)
|
7
|
+
message = "Prefab couldn't initialize in #{timeout_sec} second timeout. Trying to fetch key `#{key}`."
|
8
|
+
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
module Errors
|
5
|
+
class InvalidApiKeyError < Prefab::Error
|
6
|
+
def initialize(key)
|
7
|
+
if key.nil? || key.empty?
|
8
|
+
message = 'No API key. Set PREFAB_API_KEY env var or use PREFAB_DATASOURCES=LOCAL_ONLY'
|
9
|
+
|
10
|
+
super(message)
|
11
|
+
else
|
12
|
+
message = "Your API key format is invalid. Expecting something like 123-development-yourapikey-SDK. You provided `#{key}`"
|
13
|
+
|
14
|
+
super(message)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
module Errors
|
5
|
+
class MissingDefaultError < Prefab::Error
|
6
|
+
def initialize(key)
|
7
|
+
message = "No value found for key '#{key}' and no default was provided.\n\nIf you'd prefer returning `nil` rather than raising when this occurs, modify the `on_no_default` value you provide in your Prefab::Options."
|
8
|
+
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|