feature_flagger 1.2.1 → 2.0.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/README.md +4 -7
- data/lib/feature_flagger.rb +5 -1
- data/lib/feature_flagger/configuration.rb +6 -2
- data/lib/feature_flagger/control.rb +19 -30
- data/lib/feature_flagger/core_ext.rb +2 -0
- data/lib/feature_flagger/feature.rb +22 -16
- data/lib/feature_flagger/key_decomposer.rb +12 -0
- data/lib/feature_flagger/key_resolver.rb +19 -0
- data/lib/feature_flagger/manager.rb +18 -9
- data/lib/feature_flagger/model.rb +47 -43
- data/lib/feature_flagger/model_settings.rb +5 -3
- data/lib/feature_flagger/railtie.rb +2 -0
- data/lib/feature_flagger/storage/feature_keys_migration.rb +37 -40
- data/lib/feature_flagger/storage/redis.rb +49 -76
- data/lib/feature_flagger/version.rb +3 -1
- data/lib/tasks/feature_flagger.rake +30 -15
- metadata +19 -18
- data/lib/feature_flagger/storage/keys.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33983c979f6382f528796cbab4e51e80000c6855f50702602f451e2fb9cad666
|
4
|
+
data.tar.gz: 766cc624063a0a2516a2509c70f6bf6b397cc4ffcda6ee2333f36ac45d74f99a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ea819eac2381595cab508f5d8e83a2e20c3b2516ad413819875ce12392cf61d67b7a1876ffe9a5345e2dc174205b6cf3b72eba8c12fd007baa7045fffefdfc4
|
7
|
+
data.tar.gz: fadce1676c47be30ed5a4fe8845b198448395b75a4a575276fea62eb5f6565318ea9dc27770b21d43418552372c0ec19757f460fefd77a9a15cd017aa386a468
|
data/README.md
CHANGED
@@ -74,6 +74,10 @@ account.release(:email_marketing, :new_email_flow)
|
|
74
74
|
account.released?(:email_marketing, :new_email_flow)
|
75
75
|
#=> true
|
76
76
|
|
77
|
+
# Get an array with all features for a specific account id
|
78
|
+
account.releases
|
79
|
+
=> ['email_marketing:new_email_flow']
|
80
|
+
|
77
81
|
# Remove feature for given account
|
78
82
|
account.unrelease(:email_marketing, :new_email_flow)
|
79
83
|
#=> true
|
@@ -111,13 +115,6 @@ To clean it up, execute or schedule the rake:
|
|
111
115
|
|
112
116
|
$ bundle exec rake feature_flagger:cleanup_removed_rollouts
|
113
117
|
|
114
|
-
## Upgrading
|
115
|
-
|
116
|
-
When upgrading from `1.1.x` to `1.2.x` the following command must be executed
|
117
|
-
to ensure the data stored in Redis storage is right. Check [#67](https://github.com/ResultadosDigitais/feature_flagger/pull/67) and [#68](https://github.com/ResultadosDigitais/feature_flagger/pull/68) for more info.
|
118
|
-
|
119
|
-
$ bundle exec rake feature_flagger:migrate_to_resource_keys
|
120
|
-
|
121
118
|
## Contributing
|
122
119
|
|
123
120
|
Bug reports and pull requests are welcome!
|
data/lib/feature_flagger.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'yaml'
|
2
4
|
|
3
5
|
require 'feature_flagger/version'
|
4
6
|
require 'feature_flagger/storage/redis'
|
5
|
-
require 'feature_flagger/storage/feature_keys_migration'
|
6
7
|
require 'feature_flagger/control'
|
7
8
|
require 'feature_flagger/model'
|
8
9
|
require 'feature_flagger/model_settings'
|
9
10
|
require 'feature_flagger/feature'
|
11
|
+
require 'feature_flagger/key_resolver'
|
12
|
+
require 'feature_flagger/key_decomposer'
|
10
13
|
require 'feature_flagger/configuration'
|
11
14
|
require 'feature_flagger/manager'
|
15
|
+
require 'feature_flagger/storage/feature_keys_migration'
|
12
16
|
require 'feature_flagger/railtie'
|
13
17
|
|
14
18
|
module FeatureFlagger
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FeatureFlagger
|
2
4
|
class Configuration
|
3
5
|
attr_accessor :storage, :yaml_filepath
|
@@ -8,7 +10,9 @@ module FeatureFlagger
|
|
8
10
|
end
|
9
11
|
|
10
12
|
def info
|
11
|
-
|
13
|
+
raise 'Missing configuration file.' unless yaml_filepath
|
14
|
+
|
15
|
+
@info ||= YAML.load_file(yaml_filepath)
|
12
16
|
end
|
13
17
|
|
14
18
|
def mapped_feature_keys(resource_name = nil)
|
@@ -40,7 +44,7 @@ module FeatureFlagger
|
|
40
44
|
|
41
45
|
def join_key(resource_name, key)
|
42
46
|
key.unshift resource_name if resource_name
|
43
|
-
key.join(
|
47
|
+
key.join(':')
|
44
48
|
end
|
45
49
|
end
|
46
50
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FeatureFlagger
|
2
4
|
class Control
|
3
5
|
attr_reader :storage
|
@@ -8,58 +10,45 @@ module FeatureFlagger
|
|
8
10
|
@storage = storage
|
9
11
|
end
|
10
12
|
|
11
|
-
def released?(feature_key, resource_id)
|
12
|
-
@storage.has_value?(
|
13
|
+
def released?(feature_key, resource_name, resource_id)
|
14
|
+
@storage.has_value?(feature_key, resource_name, RELEASED_FEATURES) ||
|
15
|
+
@storage.has_value?(feature_key, resource_name, resource_id)
|
13
16
|
end
|
14
17
|
|
15
|
-
def release(feature_key, resource_id)
|
16
|
-
resource_name = Storage::Keys.extract_resource_name_from_feature_key(
|
17
|
-
feature_key
|
18
|
-
)
|
19
|
-
|
18
|
+
def release(feature_key, resource_name, resource_id)
|
20
19
|
@storage.add(feature_key, resource_name, resource_id)
|
21
20
|
end
|
22
21
|
|
23
|
-
def
|
24
|
-
@storage.
|
22
|
+
def release_to_all(feature_key, resource_name)
|
23
|
+
@storage.add_all(RELEASED_FEATURES, feature_key, resource_name)
|
25
24
|
end
|
26
25
|
|
27
|
-
def
|
28
|
-
@storage.
|
26
|
+
def all_feature_keys(resource_name, resource_id)
|
27
|
+
@storage.all_feature_keys(RELEASED_FEATURES, resource_name, resource_id)
|
29
28
|
end
|
30
29
|
|
31
|
-
def unrelease(feature_key, resource_id)
|
32
|
-
resource_name = Storage::Keys.extract_resource_name_from_feature_key(
|
33
|
-
feature_key
|
34
|
-
)
|
35
|
-
|
30
|
+
def unrelease(feature_key, resource_name, resource_id)
|
36
31
|
@storage.remove(feature_key, resource_name, resource_id)
|
37
32
|
end
|
38
33
|
|
39
|
-
def unrelease_to_all(feature_key)
|
40
|
-
@storage.remove_all(
|
34
|
+
def unrelease_to_all(feature_key, resource_name)
|
35
|
+
@storage.remove_all(feature_key, resource_name)
|
41
36
|
end
|
42
37
|
|
43
|
-
def resource_ids(feature_key)
|
44
|
-
@storage.all_values(feature_key)
|
38
|
+
def resource_ids(feature_key, resource_name)
|
39
|
+
@storage.all_values(feature_key, resource_name)
|
45
40
|
end
|
46
41
|
|
47
|
-
def released_features_to_all
|
48
|
-
@storage.
|
42
|
+
def released_features_to_all(resource_name)
|
43
|
+
@storage.all_feature_keys(RELEASED_FEATURES, resource_name)
|
49
44
|
end
|
50
45
|
|
51
|
-
def released_to_all?(feature_key)
|
52
|
-
@storage.has_value?(
|
46
|
+
def released_to_all?(feature_key, resource_name)
|
47
|
+
@storage.has_value?(feature_key, resource_name, RELEASED_FEATURES)
|
53
48
|
end
|
54
49
|
|
55
|
-
# DEPRECATED: this method will be removed from public api on v2.0 version.
|
56
|
-
# use instead the feature_keys method.
|
57
50
|
def search_keys(query)
|
58
51
|
@storage.search_keys(query)
|
59
52
|
end
|
60
|
-
|
61
|
-
def feature_keys
|
62
|
-
@storage.feature_keys
|
63
|
-
end
|
64
53
|
end
|
65
54
|
end
|
@@ -1,8 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FeatureFlagger
|
4
|
+
class KeyNotFoundError < StandardError; end
|
5
|
+
|
2
6
|
class Feature
|
3
|
-
def initialize(feature_key, resource_name
|
4
|
-
@
|
5
|
-
|
7
|
+
def initialize(feature_key, resource_name)
|
8
|
+
@key_resolver = KeyResolver.new(feature_key, resource_name.to_s)
|
9
|
+
|
6
10
|
fetch_data
|
7
11
|
end
|
8
12
|
|
@@ -11,33 +15,35 @@ module FeatureFlagger
|
|
11
15
|
end
|
12
16
|
|
13
17
|
def key
|
14
|
-
@
|
18
|
+
@key_resolver.normalized_key.join(':')
|
15
19
|
end
|
16
20
|
|
17
21
|
private
|
18
22
|
|
19
|
-
def
|
20
|
-
|
21
|
-
key.insert(0, resource_name) if resource_name
|
22
|
-
key.map(&:to_s)
|
23
|
+
def config_info
|
24
|
+
FeatureFlagger.config.info
|
23
25
|
end
|
24
26
|
|
25
27
|
def fetch_data
|
26
|
-
@data ||= find_value(
|
27
|
-
|
28
|
+
@data ||= find_value(config_info, *@key_resolver.normalized_key_with_name)
|
29
|
+
|
30
|
+
if @data.nil? || @data["description"].nil?
|
31
|
+
raise FeatureFlagger::KeyNotFoundError, @feature_key
|
32
|
+
end
|
33
|
+
|
28
34
|
@data
|
29
35
|
end
|
30
36
|
|
31
37
|
def find_value(hash, key, *tail)
|
38
|
+
return nil if hash.nil?
|
39
|
+
|
32
40
|
value = hash[key]
|
33
41
|
|
34
|
-
if
|
35
|
-
value
|
36
|
-
else
|
37
|
-
find_value(value, *tail)
|
42
|
+
if tail.any?
|
43
|
+
return find_value(value, *tail)
|
38
44
|
end
|
45
|
+
|
46
|
+
value
|
39
47
|
end
|
40
48
|
end
|
41
49
|
end
|
42
|
-
|
43
|
-
class FeatureFlagger::KeyNotFoundError < StandardError ; end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FeatureFlagger
|
4
|
+
class KeyDecomposer
|
5
|
+
def self.decompose(complete_feature_key)
|
6
|
+
decomposed_key = complete_feature_key.split(':')
|
7
|
+
resource_name = decomposed_key.shift
|
8
|
+
|
9
|
+
[resource_name, decomposed_key]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FeatureFlagger
|
4
|
+
class KeyResolver
|
5
|
+
def initialize(feature_key, resource_name)
|
6
|
+
@feature_key = feature_key
|
7
|
+
@resource_name = resource_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def normalized_key
|
11
|
+
@normalized_key ||= Array(@feature_key).flatten
|
12
|
+
.map(&:to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
def normalized_key_with_name
|
16
|
+
@normalized_key_with_name ||= [@resource_name] + normalized_key
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,19 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FeatureFlagger
|
2
4
|
class Manager
|
5
|
+
def self.detached_feature_keys(resource_name)
|
6
|
+
keys = FeatureFlagger.control.search_keys("#{resource_name}:*")
|
7
|
+
|
8
|
+
persisted_features = keys.flat_map do |key|
|
9
|
+
FeatureFlagger.control.all_feature_keys(resource_name, key.sub("#{resource_name}:", ''))
|
10
|
+
end.sort.uniq
|
11
|
+
|
12
|
+
mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys(resource_name).map do |feature|
|
13
|
+
feature.sub("#{resource_name}:", '')
|
14
|
+
end
|
3
15
|
|
4
|
-
def self.detached_feature_keys
|
5
|
-
persisted_features = FeatureFlagger.control.feature_keys
|
6
|
-
mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys
|
7
|
-
|
8
16
|
persisted_features - mapped_feature_keys
|
9
17
|
end
|
10
18
|
|
11
19
|
def self.cleanup_detached(resource_name, *feature_key)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
20
|
+
feature = Feature.new(feature_key, resource_name)
|
21
|
+
raise 'key is still mapped'
|
22
|
+
rescue FeatureFlagger::KeyNotFoundError => _e
|
23
|
+
# This means the keys is not present in config file anymore
|
24
|
+
key_resolver = KeyResolver.new(feature_key, resource_name)
|
25
|
+
FeatureFlagger.control.unrelease_to_all(key_resolver.normalized_key, resource_name)
|
16
26
|
end
|
17
|
-
|
18
27
|
end
|
19
28
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FeatureFlagger
|
2
4
|
# Model provides convinient methods for Rails Models
|
3
5
|
# class Account
|
@@ -20,15 +22,13 @@ module FeatureFlagger
|
|
20
22
|
self.class.release_id(feature_flagger_identifier, *feature_key)
|
21
23
|
end
|
22
24
|
|
23
|
-
def
|
24
|
-
|
25
|
-
feature = Feature.new(feature_key, resource_name)
|
26
|
-
FeatureFlagger.control.unrelease(feature.key, id)
|
25
|
+
def releases
|
26
|
+
self.class.release_keys(feature_flagger_identifier)
|
27
27
|
end
|
28
28
|
|
29
|
-
def
|
30
|
-
|
31
|
-
FeatureFlagger.control.
|
29
|
+
def unrelease(*feature_key)
|
30
|
+
feature = Feature.new(feature_key, feature_flagger_name)
|
31
|
+
FeatureFlagger.control.unrelease(feature.key, feature_flagger_name, id)
|
32
32
|
end
|
33
33
|
|
34
34
|
private
|
@@ -37,74 +37,66 @@ module FeatureFlagger
|
|
37
37
|
public_send(self.class.feature_flagger_model_settings.identifier_field)
|
38
38
|
end
|
39
39
|
|
40
|
+
def feature_flagger_name
|
41
|
+
self.class.feature_flagger_model_settings.entity_name
|
42
|
+
end
|
43
|
+
|
40
44
|
module ClassMethods
|
41
45
|
def feature_flagger
|
42
46
|
raise ArgumentError unless block_given?
|
47
|
+
|
43
48
|
yield feature_flagger_model_settings
|
44
49
|
end
|
45
50
|
|
46
51
|
def released_id?(resource_id, *feature_key)
|
47
|
-
feature = Feature.new(feature_key,
|
48
|
-
FeatureFlagger.control.released?(feature.key, resource_id)
|
52
|
+
feature = Feature.new(feature_key, feature_flagger_name)
|
53
|
+
FeatureFlagger.control.released?(feature.key, feature_flagger_name, resource_id)
|
49
54
|
end
|
50
55
|
|
51
56
|
def release_id(resource_id, *feature_key)
|
52
|
-
feature = Feature.new(feature_key,
|
53
|
-
FeatureFlagger.control.release(feature.key, resource_id)
|
57
|
+
feature = Feature.new(feature_key, feature_flagger_name)
|
58
|
+
FeatureFlagger.control.release(feature.key, feature_flagger_name, resource_id)
|
59
|
+
end
|
60
|
+
|
61
|
+
def release_keys(resource_id)
|
62
|
+
FeatureFlagger.control.all_feature_keys(feature_flagger_name, resource_id)
|
54
63
|
end
|
55
64
|
|
56
65
|
def unrelease_id(resource_id, *feature_key)
|
57
|
-
feature = Feature.new(feature_key,
|
58
|
-
FeatureFlagger.control.unrelease(feature.key, resource_id)
|
66
|
+
feature = Feature.new(feature_key, feature_flagger_name)
|
67
|
+
FeatureFlagger.control.unrelease(feature.key, feature_flagger_name, resource_id)
|
59
68
|
end
|
60
69
|
|
61
70
|
def all_released_ids_for(*feature_key)
|
62
|
-
feature_key
|
63
|
-
feature
|
64
|
-
FeatureFlagger.control.resource_ids(feature.key)
|
71
|
+
feature = Feature.new(feature_key, feature_flagger_name)
|
72
|
+
FeatureFlagger.control.resource_ids(feature.key, feature_flagger_name)
|
65
73
|
end
|
66
74
|
|
67
75
|
def release_to_all(*feature_key)
|
68
|
-
feature = Feature.new(feature_key,
|
69
|
-
FeatureFlagger.control.release_to_all(feature.key)
|
76
|
+
feature = Feature.new(feature_key, feature_flagger_name)
|
77
|
+
FeatureFlagger.control.release_to_all(feature.key, feature_flagger_name)
|
70
78
|
end
|
71
79
|
|
72
80
|
def unrelease_to_all(*feature_key)
|
73
|
-
feature = Feature.new(feature_key,
|
74
|
-
FeatureFlagger.control.unrelease_to_all(feature.key)
|
81
|
+
feature = Feature.new(feature_key, feature_flagger_name)
|
82
|
+
FeatureFlagger.control.unrelease_to_all(feature.key, feature_flagger_name)
|
75
83
|
end
|
76
84
|
|
77
85
|
def released_features_to_all
|
78
|
-
FeatureFlagger.control.released_features_to_all
|
86
|
+
FeatureFlagger.control.released_features_to_all(feature_flagger_name)
|
79
87
|
end
|
80
88
|
|
81
89
|
def released_to_all?(*feature_key)
|
82
|
-
feature = Feature.new(feature_key,
|
83
|
-
FeatureFlagger.control.released_to_all?(feature.key)
|
90
|
+
feature = Feature.new(feature_key, feature_flagger_name)
|
91
|
+
FeatureFlagger.control.released_to_all?(feature.key, feature_flagger_name)
|
84
92
|
end
|
85
93
|
|
86
94
|
def detached_feature_keys
|
87
|
-
|
88
|
-
persisted_features = FeatureFlagger.control.search_keys("#{rollout_resource_name}:*").to_a
|
89
|
-
mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys(rollout_resource_name)
|
90
|
-
(persisted_features - mapped_feature_keys).map { |key| key.sub("#{rollout_resource_name}:",'') }
|
95
|
+
Manager.detached_feature_keys(feature_flagger_name)
|
91
96
|
end
|
92
97
|
|
93
98
|
def cleanup_detached(*feature_key)
|
94
|
-
|
95
|
-
key_value = FeatureFlagger.config.info.dig(*complete_feature_key)
|
96
|
-
raise "key is still mapped" if key_value
|
97
|
-
FeatureFlagger.control.unrelease_to_all(complete_feature_key.join(':'))
|
98
|
-
end
|
99
|
-
|
100
|
-
def rollout_resource_name
|
101
|
-
klass_name = self.to_s
|
102
|
-
klass_name.gsub!(/::/, '_')
|
103
|
-
klass_name.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
104
|
-
klass_name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
105
|
-
klass_name.tr!("-", "_")
|
106
|
-
klass_name.downcase!
|
107
|
-
klass_name
|
99
|
+
Manager.cleanup_detached(feature_flagger_name, feature_key)
|
108
100
|
end
|
109
101
|
|
110
102
|
def feature_flagger_model_settings
|
@@ -114,8 +106,20 @@ module FeatureFlagger
|
|
114
106
|
)
|
115
107
|
end
|
116
108
|
|
117
|
-
|
118
|
-
|
109
|
+
private
|
110
|
+
|
111
|
+
def rollout_resource_name
|
112
|
+
klass_name = to_s
|
113
|
+
klass_name.gsub!(/::/, '_')
|
114
|
+
klass_name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
115
|
+
klass_name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
116
|
+
klass_name.tr!('-', '_')
|
117
|
+
klass_name.downcase!
|
118
|
+
klass_name
|
119
|
+
end
|
120
|
+
|
121
|
+
def feature_flagger_name
|
122
|
+
feature_flagger_model_settings.entity_name
|
119
123
|
end
|
120
124
|
end
|
121
125
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FeatureFlagger
|
2
4
|
class ModelSettings
|
3
5
|
def initialize(arguments)
|
4
6
|
arguments.each do |field, value|
|
5
|
-
|
7
|
+
public_send("#{field}=", value)
|
6
8
|
end
|
7
9
|
end
|
8
10
|
|
@@ -12,7 +14,7 @@ module FeatureFlagger
|
|
12
14
|
|
13
15
|
# Public: entity_name to which entity the model is targeting.
|
14
16
|
# Take this yaml file as example:
|
15
|
-
#
|
17
|
+
#
|
16
18
|
# account:
|
17
19
|
# email_marketing:
|
18
20
|
# whitelabel:
|
@@ -33,4 +35,4 @@ module FeatureFlagger
|
|
33
35
|
# end
|
34
36
|
attr_accessor :entity_name
|
35
37
|
end
|
36
|
-
end
|
38
|
+
end
|
@@ -1,57 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module FeatureFlagger
|
4
|
-
|
5
|
-
|
4
|
+
module Storage
|
5
|
+
class FeatureKeysMigration
|
6
|
+
def initialize(from_redis, to_control)
|
7
|
+
@from_redis = from_redis
|
8
|
+
@to_control = to_control
|
9
|
+
end
|
6
10
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
11
|
+
def call
|
12
|
+
@from_redis.keys('*').map { |key| migrate_key(key) }.flatten
|
13
|
+
end
|
11
14
|
|
12
|
-
|
13
|
-
# format.
|
14
|
-
#
|
15
|
-
# It must replicate feature keys with changes:
|
16
|
-
#
|
17
|
-
# from "avenue:traffic_lights" => 42
|
18
|
-
# to "avenue:42" => traffic_lights
|
19
|
-
def call
|
20
|
-
@from_redis.scan_each(match: "*", count: FeatureFlagger::Storage::Redis::SCAN_EACH_BATCH_SIZE) do |redis_key|
|
21
|
-
# filter out resource_keys
|
22
|
-
next if redis_key.start_with?("#{FeatureFlagger::Storage::Redis::RESOURCE_PREFIX}:")
|
23
|
-
|
24
|
-
migrate_key(redis_key)
|
25
|
-
end
|
26
|
-
end
|
15
|
+
private
|
27
16
|
|
28
|
-
|
17
|
+
def migrate_key(key)
|
18
|
+
return migrate_release_to_all(key) if feature_released_to_all?(key)
|
29
19
|
|
30
|
-
|
31
|
-
|
20
|
+
migrate_release(key)
|
21
|
+
end
|
32
22
|
|
33
|
-
|
34
|
-
|
23
|
+
def migrate_release_to_all(key)
|
24
|
+
features = @from_redis.smembers(key)
|
35
25
|
|
36
|
-
|
37
|
-
|
26
|
+
features.map do |feature|
|
27
|
+
resource_name, feature_key = KeyDecomposer.decompose(feature)
|
28
|
+
feature = Feature.new(feature_key, resource_name)
|
38
29
|
|
39
|
-
|
40
|
-
|
41
|
-
|
30
|
+
@to_control.release_to_all(feature.key, resource_name)
|
31
|
+
rescue KeyNotFoundError => _e
|
32
|
+
next
|
42
33
|
end
|
34
|
+
end
|
43
35
|
|
44
|
-
|
45
|
-
|
46
|
-
|
36
|
+
def feature_released_to_all?(key)
|
37
|
+
FeatureFlagger::Control::RELEASED_FEATURES == key
|
38
|
+
end
|
47
39
|
|
48
|
-
|
49
|
-
|
40
|
+
def migrate_release(key)
|
41
|
+
return false if key =~ /(\d+).*/
|
50
42
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
43
|
+
resource_ids = @from_redis.smembers(key)
|
44
|
+
|
45
|
+
resource_name, feature_key = KeyDecomposer.decompose(key)
|
46
|
+
feature = Feature.new(feature_key, resource_name)
|
47
|
+
|
48
|
+
@to_control.release(feature.key, resource_name, resource_ids)
|
49
|
+
rescue KeyNotFoundError => _e
|
50
|
+
return
|
55
51
|
end
|
56
52
|
end
|
57
53
|
end
|
54
|
+
end
|
@@ -1,13 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'redis'
|
2
4
|
require 'redis-namespace'
|
3
|
-
require_relative './keys'
|
4
5
|
|
5
6
|
module FeatureFlagger
|
6
7
|
module Storage
|
7
8
|
class Redis
|
8
9
|
DEFAULT_NAMESPACE = :feature_flagger
|
9
|
-
RESOURCE_PREFIX = "_r".freeze
|
10
|
-
SCAN_EACH_BATCH_SIZE = 1000.freeze
|
11
10
|
|
12
11
|
def initialize(redis)
|
13
12
|
@redis = redis
|
@@ -15,107 +14,81 @@ module FeatureFlagger
|
|
15
14
|
|
16
15
|
def self.default_client
|
17
16
|
redis = ::Redis.new(url: ENV['REDIS_URL'])
|
18
|
-
ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, :
|
17
|
+
ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, redis: redis)
|
19
18
|
new(ns)
|
20
19
|
end
|
21
20
|
|
22
|
-
def
|
23
|
-
|
24
|
-
releases = @redis.sunion(resource_key, global_key)
|
25
|
-
|
26
|
-
releases.select{ |release| release.start_with?(resource_name) }
|
27
|
-
end
|
28
|
-
|
29
|
-
def has_value?(key, value)
|
30
|
-
@redis.sismember(key, value)
|
21
|
+
def has_value?(feature_key, resource_name, resource_id)
|
22
|
+
@redis.sismember("#{resource_name}:#{resource_id}", feature_key)
|
31
23
|
end
|
32
24
|
|
33
|
-
def add(feature_key, resource_name,
|
34
|
-
resource_key = resource_key(resource_name, resource_id)
|
35
|
-
|
25
|
+
def add(feature_key, resource_name, resource_ids)
|
36
26
|
@redis.multi do |redis|
|
37
|
-
|
38
|
-
|
27
|
+
Array(resource_ids).each do |resource_id|
|
28
|
+
redis.sadd("#{resource_name}:#{resource_id}", feature_key)
|
29
|
+
end
|
39
30
|
end
|
40
31
|
end
|
41
32
|
|
42
|
-
def remove(feature_key, resource_name,
|
43
|
-
resource_key = resource_key(resource_name, resource_id)
|
44
|
-
|
33
|
+
def remove(feature_key, resource_name, resource_ids)
|
45
34
|
@redis.multi do |redis|
|
46
|
-
|
47
|
-
|
35
|
+
Array(resource_ids).each do |resource_id|
|
36
|
+
redis.srem("#{resource_name}:#{resource_id}", feature_key)
|
37
|
+
end
|
48
38
|
end
|
49
39
|
end
|
50
40
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
41
|
+
def all_feature_keys(global_features_key, resource_name, resource_id = nil)
|
42
|
+
if resource_id
|
43
|
+
return @redis.sunion(
|
44
|
+
"#{resource_name}:#{global_features_key}",
|
45
|
+
"#{resource_name}:#{resource_id}"
|
46
|
+
)
|
47
|
+
end
|
55
48
|
|
56
|
-
|
57
|
-
@redis.sadd(global_key, key)
|
58
|
-
remove_feature_key_from_resources(key)
|
49
|
+
@redis.smembers("#{resource_name}:#{global_features_key}")
|
59
50
|
end
|
60
51
|
|
61
|
-
def
|
62
|
-
|
63
|
-
end
|
52
|
+
def remove_all(feature_key, resource_name)
|
53
|
+
keys = search_keys("#{resource_name}:*")
|
64
54
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
55
|
+
@redis.multi do |redis|
|
56
|
+
keys.map do |key|
|
57
|
+
redis.srem(key, feature_key)
|
58
|
+
end
|
59
|
+
end
|
69
60
|
end
|
70
61
|
|
71
|
-
def
|
72
|
-
|
62
|
+
def add_all(global_features_key, feature_key, resource_name)
|
63
|
+
keys = search_keys("#{resource_name}:*") - ["#{resource_name}:#{global_features_key}"]
|
73
64
|
|
74
|
-
|
75
|
-
# Reject keys related to feature responsible for return
|
76
|
-
# released features for a given account.
|
77
|
-
next if key.start_with?("#{RESOURCE_PREFIX}:")
|
65
|
+
add(feature_key, resource_name, global_features_key)
|
78
66
|
|
79
|
-
|
67
|
+
@redis.multi do |redis|
|
68
|
+
keys.map do |key|
|
69
|
+
redis.srem(key.to_s, feature_key)
|
70
|
+
end
|
80
71
|
end
|
81
|
-
|
82
|
-
feature_keys
|
83
72
|
end
|
84
73
|
|
85
|
-
def
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
74
|
+
def all_values(feature_key, resource_name)
|
75
|
+
keys = search_keys("#{resource_name}:*")
|
76
|
+
query = {}
|
77
|
+
@redis.pipelined do |redis|
|
78
|
+
keys.map do |key|
|
79
|
+
query[key] = redis.sismember(key, feature_key)
|
80
|
+
end
|
81
|
+
end
|
91
82
|
|
92
|
-
|
83
|
+
query.map do |key, in_redis|
|
84
|
+
next unless in_redis.value == true
|
93
85
|
|
94
|
-
|
95
|
-
|
96
|
-
RESOURCE_PREFIX,
|
97
|
-
resource_name,
|
98
|
-
resource_id,
|
99
|
-
)
|
86
|
+
key.gsub("#{resource_name}:", '')
|
87
|
+
end.compact
|
100
88
|
end
|
101
89
|
|
102
|
-
def
|
103
|
-
|
104
|
-
resource_name = feature_key.split(":").first
|
105
|
-
|
106
|
-
loop do
|
107
|
-
cursor, resource_ids = @redis.sscan(feature_key, cursor, count: SCAN_EACH_BATCH_SIZE)
|
108
|
-
|
109
|
-
@redis.multi do |redis|
|
110
|
-
resource_ids.each do |resource_id|
|
111
|
-
key = resource_key(resource_name, resource_id)
|
112
|
-
redis.srem(key, feature_key)
|
113
|
-
redis.srem(feature_key, resource_id)
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
break if cursor == "0"
|
118
|
-
end
|
90
|
+
def search_keys(pattern)
|
91
|
+
@redis.keys(pattern)
|
119
92
|
end
|
120
93
|
end
|
121
94
|
end
|
@@ -1,40 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :feature_flagger do
|
2
|
-
desc "cleaning up keys from storage that are no longer in the rollout.yml file"
|
3
|
-
task :cleanup_removed_rollouts => :environment do
|
4
|
-
|
5
|
-
|
6
|
-
keys
|
7
|
-
|
4
|
+
desc "cleaning up keys from storage that are no longer in the rollout.yml file, Usage: `$ bundle exec rake feature_flagger:cleanup_removed_rollouts\[Account\] `"
|
5
|
+
task :cleanup_removed_rollouts, %i[entity_name] => :environment do
|
6
|
+
resource_name = args.entity_name.constantize
|
7
|
+
feature_keys = FeatureFlagger::Manager.detached_feature_keys(resource_name)
|
8
|
+
puts "Found keys to remove: #{feature_keys}"
|
9
|
+
feature_keys.each do |feature_key|
|
10
|
+
FeatureFlagger::Manager.cleanup_detached(resource_name, feature_key)
|
11
|
+
rescue RuntimeError, 'key is still mapped'
|
12
|
+
next
|
8
13
|
end
|
9
14
|
end
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
16
|
+
namespace :storage do
|
17
|
+
namespace :redis do
|
18
|
+
desc 'Migrate the old key format to the new one, Usage: `$ bundle exec rake feature_flagger:storage:redis:migrate`'
|
19
|
+
task :migrate => :environment do |_, _args|
|
20
|
+
redis = ::Redis::Namespace.new(
|
21
|
+
FeatureFlagger::Storage::Redis::DEFAULT_NAMESPACE,
|
22
|
+
redis: ::Redis.new(url: ENV['REDIS_URL'])
|
23
|
+
)
|
24
|
+
control = FeatureFlagger.control
|
25
|
+
|
26
|
+
FeatureFlagger::Storage::FeatureKeysMigration.new(redis, control).call
|
27
|
+
end
|
28
|
+
end
|
15
29
|
end
|
16
30
|
|
17
31
|
desc "Release feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:release\[Account,email_marketing:whitelabel,1,2,3,4\]`"
|
18
|
-
task :release, [
|
32
|
+
task :release, %i[entity_name feature_key] => :environment do |_, args|
|
19
33
|
entity = args.entity_name.constantize
|
20
34
|
entity_ids = args.extras
|
21
35
|
entity.release_id(entity_ids, *args.feature_key.split(':'))
|
22
36
|
end
|
23
37
|
|
24
38
|
desc "Unrelease feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:unrelease\[Account,email_marketing:whitelabel,1,2,3,4\]`"
|
25
|
-
task :unrelease, [
|
26
|
-
entity
|
39
|
+
task :unrelease, %i[entity_name feature_key] => :environment do |_, args|
|
40
|
+
entity = args.entity_name.constantize
|
41
|
+
entity_ids = args.extras
|
27
42
|
entity.unrelease_id(entity_ids, *args.feature_key.split(':'))
|
28
43
|
end
|
29
44
|
|
30
45
|
desc "Release one feature to all entity ids, Usage: `$ bundle exec rake feature_flagger:release_to_all\[Account,email_marketing:whitelabel\]`"
|
31
|
-
task :release_to_all, [
|
46
|
+
task :release_to_all, %i[entity_name feature_key] => :environment do |_, args|
|
32
47
|
entity = args.entity_name.constantize
|
33
48
|
entity.release_to_all(*args.feature_key.split(':'))
|
34
49
|
end
|
35
50
|
|
36
51
|
desc "Unrelease one feature to all entity ids, Usage: `$ bundle exec rake feature_flagger:unrelease_to_all\[Account,email_marketing:whitelabel\]`"
|
37
|
-
task :unrelease_to_all, [
|
52
|
+
task :unrelease_to_all, %i[entity_name feature_key] => :environment do |_, args|
|
38
53
|
entity = args.entity_name.constantize
|
39
54
|
entity.unrelease_to_all(*args.feature_key.split(':'))
|
40
55
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feature_flagger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nando Sousa
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-
|
12
|
+
date: 2020-06-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
@@ -53,6 +53,20 @@ dependencies:
|
|
53
53
|
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
55
|
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: fakeredis
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 0.8.0
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.8.0
|
56
70
|
- !ruby/object:Gem::Dependency
|
57
71
|
name: rake
|
58
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -95,20 +109,6 @@ dependencies:
|
|
95
109
|
- - '='
|
96
110
|
- !ruby/object:Gem::Version
|
97
111
|
version: '0.17'
|
98
|
-
- !ruby/object:Gem::Dependency
|
99
|
-
name: fakeredis
|
100
|
-
requirement: !ruby/object:Gem::Requirement
|
101
|
-
requirements:
|
102
|
-
- - '='
|
103
|
-
- !ruby/object:Gem::Version
|
104
|
-
version: 0.8.0
|
105
|
-
type: :development
|
106
|
-
prerelease: false
|
107
|
-
version_requirements: !ruby/object:Gem::Requirement
|
108
|
-
requirements:
|
109
|
-
- - '='
|
110
|
-
- !ruby/object:Gem::Version
|
111
|
-
version: 0.8.0
|
112
112
|
description: Management tool to make it easier rollouting features to customers.
|
113
113
|
email:
|
114
114
|
- nandosousafr@gmail.com
|
@@ -124,12 +124,13 @@ files:
|
|
124
124
|
- lib/feature_flagger/control.rb
|
125
125
|
- lib/feature_flagger/core_ext.rb
|
126
126
|
- lib/feature_flagger/feature.rb
|
127
|
+
- lib/feature_flagger/key_decomposer.rb
|
128
|
+
- lib/feature_flagger/key_resolver.rb
|
127
129
|
- lib/feature_flagger/manager.rb
|
128
130
|
- lib/feature_flagger/model.rb
|
129
131
|
- lib/feature_flagger/model_settings.rb
|
130
132
|
- lib/feature_flagger/railtie.rb
|
131
133
|
- lib/feature_flagger/storage/feature_keys_migration.rb
|
132
|
-
- lib/feature_flagger/storage/keys.rb
|
133
134
|
- lib/feature_flagger/storage/redis.rb
|
134
135
|
- lib/feature_flagger/version.rb
|
135
136
|
- lib/tasks/feature_flagger.rake
|
@@ -152,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
153
|
- !ruby/object:Gem::Version
|
153
154
|
version: 2.0.0
|
154
155
|
requirements: []
|
155
|
-
rubygems_version: 3.1.
|
156
|
+
rubygems_version: 3.1.2
|
156
157
|
signing_key:
|
157
158
|
specification_version: 4
|
158
159
|
summary: Partial release your features.
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module FeatureFlagger
|
2
|
-
module Storage
|
3
|
-
module Keys
|
4
|
-
MINIMUM_VALID_FEATURE_PATH = 2.freeze
|
5
|
-
|
6
|
-
def self.resource_key(prefix, resource_name, resource_id)
|
7
|
-
"#{prefix}:#{resource_name}:#{resource_id}"
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.extract_resource_name_from_feature_key(feature_key)
|
11
|
-
feature_paths = feature_key.split(':')
|
12
|
-
|
13
|
-
raise InvalidResourceNameError if feature_paths.size < MINIMUM_VALID_FEATURE_PATH
|
14
|
-
|
15
|
-
feature_paths.first
|
16
|
-
end
|
17
|
-
|
18
|
-
class InvalidResourceNameError < StandardError; end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|