feature_flagger 0.7.3 → 1.2.1
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 +5 -5
- data/README.md +27 -4
- data/lib/feature_flagger.rb +4 -0
- data/lib/feature_flagger/configuration.rb +26 -0
- data/lib/feature_flagger/control.rb +30 -16
- data/lib/feature_flagger/core_ext.rb +18 -0
- data/lib/feature_flagger/manager.rb +19 -0
- data/lib/feature_flagger/model.rb +61 -22
- data/lib/feature_flagger/model_settings.rb +36 -0
- data/lib/feature_flagger/railtie.rb +9 -0
- data/lib/feature_flagger/storage/feature_keys_migration.rb +57 -0
- data/lib/feature_flagger/storage/keys.rb +21 -0
- data/lib/feature_flagger/storage/redis.rb +90 -5
- data/lib/feature_flagger/version.rb +1 -1
- data/lib/tasks/feature_flagger.rake +41 -0
- metadata +40 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bb31f06b2fbb4faf49b9a560ffa6d97b774272dcd5ad6e660628b08380acf44d
|
4
|
+
data.tar.gz: 75207d7df33cc3f418a1b798d027c4ef701fbfd2aa15f17dc147e066a6f540c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b47eb06400b71e1e5fb46effbb5429f9e2299227ea5bb7f555fc31d2b4c913d938f59be84816dd0a55b281ba3a35c6d2768b2d4c0f3b33db48ece7b56cedfdf9
|
7
|
+
data.tar.gz: b3c596059e1055c2a3a003961e74091cabb888c71e1f46f1c564097428e717444735195d11b02cc5dee95b032222308b4ac9798b34e8465f1a88c623239ad5ce
|
data/README.md
CHANGED
@@ -4,6 +4,14 @@
|
|
4
4
|
|
5
5
|
Partially release your features.
|
6
6
|
|
7
|
+
## Working with Docker
|
8
|
+
|
9
|
+
Open IRB
|
10
|
+
`docker-compose run feature_flagger`
|
11
|
+
|
12
|
+
Running tests
|
13
|
+
`docker-compose run feature_flagger rspec`
|
14
|
+
|
7
15
|
## Installation
|
8
16
|
|
9
17
|
Add this line to your application's Gemfile:
|
@@ -63,7 +71,7 @@ account.release(:email_marketing, :new_email_flow)
|
|
63
71
|
#=> true
|
64
72
|
|
65
73
|
# Check feature for a given account
|
66
|
-
account.
|
74
|
+
account.released?(:email_marketing, :new_email_flow)
|
67
75
|
#=> true
|
68
76
|
|
69
77
|
# Remove feature for given account
|
@@ -71,7 +79,7 @@ account.unrelease(:email_marketing, :new_email_flow)
|
|
71
79
|
#=> true
|
72
80
|
|
73
81
|
# If you try to check an inexistent rollout key it will raise an error.
|
74
|
-
account.
|
82
|
+
account.released?(:email_marketing, :new_email_flow)
|
75
83
|
FeatureFlagger::KeyNotFoundError: ["account", "email_marketing", "new_email_flo"]
|
76
84
|
|
77
85
|
# Check feature for a specific account id
|
@@ -95,7 +103,22 @@ Account.unrelease_to_all(:email_marketing, :new_email_flow)
|
|
95
103
|
Account.released_features_to_all
|
96
104
|
```
|
97
105
|
|
106
|
+
## Clean up action
|
107
|
+
|
108
|
+
By default when a key is removed from `rollout.yml` file, its data still in the storage.
|
109
|
+
|
110
|
+
To clean it up, execute or schedule the rake:
|
111
|
+
|
112
|
+
$ bundle exec rake feature_flagger:cleanup_removed_rollouts
|
113
|
+
|
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
|
+
|
98
121
|
## Contributing
|
99
122
|
|
100
|
-
Bug reports and pull requests are welcome
|
101
|
-
|
123
|
+
Bug reports and pull requests are welcome!
|
124
|
+
Please take a look at our guidelines [here](CONTRIBUTING.md).
|
data/lib/feature_flagger.rb
CHANGED
@@ -2,10 +2,14 @@ require 'yaml'
|
|
2
2
|
|
3
3
|
require 'feature_flagger/version'
|
4
4
|
require 'feature_flagger/storage/redis'
|
5
|
+
require 'feature_flagger/storage/feature_keys_migration'
|
5
6
|
require 'feature_flagger/control'
|
6
7
|
require 'feature_flagger/model'
|
8
|
+
require 'feature_flagger/model_settings'
|
7
9
|
require 'feature_flagger/feature'
|
8
10
|
require 'feature_flagger/configuration'
|
11
|
+
require 'feature_flagger/manager'
|
12
|
+
require 'feature_flagger/railtie'
|
9
13
|
|
10
14
|
module FeatureFlagger
|
11
15
|
class << self
|
@@ -11,10 +11,36 @@ module FeatureFlagger
|
|
11
11
|
@info ||= YAML.load_file(yaml_filepath) if yaml_filepath
|
12
12
|
end
|
13
13
|
|
14
|
+
def mapped_feature_keys(resource_name = nil)
|
15
|
+
info_filtered = resource_name ? info[resource_name] : info
|
16
|
+
[].tap do |keys|
|
17
|
+
make_keys_recursively(info_filtered).each { |key| keys.push(join_key(resource_name, key)) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
14
21
|
private
|
15
22
|
|
16
23
|
def default_yaml_filepath
|
17
24
|
"#{Rails.root}/config/rollout.yml" if defined?(Rails)
|
18
25
|
end
|
26
|
+
|
27
|
+
def make_keys_recursively(hash, keys = [], composed_key = [])
|
28
|
+
unless hash.values[0].is_a?(Hash)
|
29
|
+
keys.push(composed_key)
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
hash.each do |key, value|
|
34
|
+
composed_key_cloned = composed_key.clone
|
35
|
+
composed_key_cloned.push(key.to_sym)
|
36
|
+
make_keys_recursively(value, keys, composed_key_cloned)
|
37
|
+
end
|
38
|
+
keys
|
39
|
+
end
|
40
|
+
|
41
|
+
def join_key(resource_name, key)
|
42
|
+
key.unshift resource_name if resource_name
|
43
|
+
key.join(":")
|
44
|
+
end
|
19
45
|
end
|
20
46
|
end
|
@@ -8,36 +8,36 @@ module FeatureFlagger
|
|
8
8
|
@storage = storage
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
11
|
+
def released?(feature_key, resource_id)
|
12
12
|
@storage.has_value?(RELEASED_FEATURES, feature_key) || @storage.has_value?(feature_key, resource_id)
|
13
13
|
end
|
14
14
|
|
15
15
|
def release(feature_key, resource_id)
|
16
|
-
|
16
|
+
resource_name = Storage::Keys.extract_resource_name_from_feature_key(
|
17
|
+
feature_key
|
18
|
+
)
|
19
|
+
|
20
|
+
@storage.add(feature_key, resource_name, resource_id)
|
17
21
|
end
|
18
22
|
|
19
|
-
def
|
20
|
-
@storage.
|
23
|
+
def releases(resource_name, resource_id)
|
24
|
+
@storage.fetch_releases(resource_name, resource_id, RELEASED_FEATURES)
|
21
25
|
end
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
warn "[DEPRECATION] `release!` is deprecated. Please use `release` instead."
|
26
|
-
release(feature_key, resource_id)
|
27
|
+
def release_to_all(feature_key)
|
28
|
+
@storage.add_all(RELEASED_FEATURES, feature_key)
|
27
29
|
end
|
28
30
|
|
29
31
|
def unrelease(feature_key, resource_id)
|
30
|
-
|
31
|
-
|
32
|
+
resource_name = Storage::Keys.extract_resource_name_from_feature_key(
|
33
|
+
feature_key
|
34
|
+
)
|
32
35
|
|
33
|
-
|
34
|
-
@storage.remove(RELEASED_FEATURES, feature_key)
|
36
|
+
@storage.remove(feature_key, resource_name, resource_id)
|
35
37
|
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
warn "[DEPRECATION] `unrelease!` is deprecated. Please use `unrelease` instead."
|
40
|
-
unrelease(feature_key, resource_id)
|
39
|
+
def unrelease_to_all(feature_key)
|
40
|
+
@storage.remove_all(RELEASED_FEATURES, feature_key)
|
41
41
|
end
|
42
42
|
|
43
43
|
def resource_ids(feature_key)
|
@@ -47,5 +47,19 @@ module FeatureFlagger
|
|
47
47
|
def released_features_to_all
|
48
48
|
@storage.all_values(RELEASED_FEATURES)
|
49
49
|
end
|
50
|
+
|
51
|
+
def released_to_all?(feature_key)
|
52
|
+
@storage.has_value?(RELEASED_FEATURES, feature_key)
|
53
|
+
end
|
54
|
+
|
55
|
+
# DEPRECATED: this method will be removed from public api on v2.0 version.
|
56
|
+
# use instead the feature_keys method.
|
57
|
+
def search_keys(query)
|
58
|
+
@storage.search_keys(query)
|
59
|
+
end
|
60
|
+
|
61
|
+
def feature_keys
|
62
|
+
@storage.feature_keys
|
63
|
+
end
|
50
64
|
end
|
51
65
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
begin
|
2
|
+
require 'active_support/core_ext/string/inflections'
|
3
|
+
rescue LoadError
|
4
|
+
unless ''.respond_to?(:constantize)
|
5
|
+
class String
|
6
|
+
def constantize
|
7
|
+
names = split('::')
|
8
|
+
names.shift if names.empty? || names.first.empty?
|
9
|
+
|
10
|
+
constant = Object
|
11
|
+
names.each do |name|
|
12
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
13
|
+
end
|
14
|
+
constant
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module FeatureFlagger
|
2
|
+
class Manager
|
3
|
+
|
4
|
+
def self.detached_feature_keys
|
5
|
+
persisted_features = FeatureFlagger.control.feature_keys
|
6
|
+
mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys
|
7
|
+
|
8
|
+
persisted_features - mapped_feature_keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.cleanup_detached(resource_name, *feature_key)
|
12
|
+
complete_feature_key = feature_key.map(&:to_s).insert(0, resource_name.to_s)
|
13
|
+
key_value = FeatureFlagger.config.info.dig(*complete_feature_key)
|
14
|
+
raise "key is still mapped" if key_value
|
15
|
+
FeatureFlagger.control.unrelease_to_all(complete_feature_key.join(':'))
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -12,56 +12,65 @@ module FeatureFlagger
|
|
12
12
|
base.extend ClassMethods
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
self.class.released_id?(
|
17
|
-
end
|
18
|
-
|
19
|
-
# <b>DEPRECATED:</b> Please use <tt>release</tt> instead.
|
20
|
-
def release!(*feature_key)
|
21
|
-
warn "[DEPRECATION] `release!` is deprecated. Please use `release` instead."
|
22
|
-
release(*feature_key)
|
15
|
+
def released?(*feature_key)
|
16
|
+
self.class.released_id?(feature_flagger_identifier, feature_key)
|
23
17
|
end
|
24
18
|
|
25
19
|
def release(*feature_key)
|
26
|
-
self.class.release_id(
|
27
|
-
end
|
28
|
-
|
29
|
-
# <b>DEPRECATED:</b> Please use <tt>unrelease</tt> instead.
|
30
|
-
def unrelease!(*feature_key)
|
31
|
-
warn "[DEPRECATION] `unrelease!` is deprecated. Please use `unrelease` instead."
|
32
|
-
unrelease(*feature_key)
|
20
|
+
self.class.release_id(feature_flagger_identifier, *feature_key)
|
33
21
|
end
|
34
22
|
|
35
23
|
def unrelease(*feature_key)
|
36
|
-
resource_name = self.class.
|
24
|
+
resource_name = self.class.feature_flagger_model_settings.entity_name
|
37
25
|
feature = Feature.new(feature_key, resource_name)
|
38
26
|
FeatureFlagger.control.unrelease(feature.key, id)
|
39
27
|
end
|
40
28
|
|
29
|
+
def releases
|
30
|
+
resource_name = self.class.feature_flagger_model_settings.entity_name
|
31
|
+
FeatureFlagger.control.releases(resource_name, id)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def feature_flagger_identifier
|
37
|
+
public_send(self.class.feature_flagger_model_settings.identifier_field)
|
38
|
+
end
|
39
|
+
|
41
40
|
module ClassMethods
|
41
|
+
def feature_flagger
|
42
|
+
raise ArgumentError unless block_given?
|
43
|
+
yield feature_flagger_model_settings
|
44
|
+
end
|
45
|
+
|
42
46
|
def released_id?(resource_id, *feature_key)
|
43
|
-
feature = Feature.new(feature_key,
|
44
|
-
FeatureFlagger.control.
|
47
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
48
|
+
FeatureFlagger.control.released?(feature.key, resource_id)
|
45
49
|
end
|
46
50
|
|
47
51
|
def release_id(resource_id, *feature_key)
|
48
|
-
feature = Feature.new(feature_key,
|
52
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
49
53
|
FeatureFlagger.control.release(feature.key, resource_id)
|
50
54
|
end
|
51
55
|
|
56
|
+
def unrelease_id(resource_id, *feature_key)
|
57
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
58
|
+
FeatureFlagger.control.unrelease(feature.key, resource_id)
|
59
|
+
end
|
60
|
+
|
52
61
|
def all_released_ids_for(*feature_key)
|
53
62
|
feature_key.flatten!
|
54
|
-
feature = Feature.new(feature_key,
|
63
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
55
64
|
FeatureFlagger.control.resource_ids(feature.key)
|
56
65
|
end
|
57
66
|
|
58
67
|
def release_to_all(*feature_key)
|
59
|
-
feature = Feature.new(feature_key,
|
68
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
60
69
|
FeatureFlagger.control.release_to_all(feature.key)
|
61
70
|
end
|
62
71
|
|
63
72
|
def unrelease_to_all(*feature_key)
|
64
|
-
feature = Feature.new(feature_key,
|
73
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
65
74
|
FeatureFlagger.control.unrelease_to_all(feature.key)
|
66
75
|
end
|
67
76
|
|
@@ -69,6 +78,25 @@ module FeatureFlagger
|
|
69
78
|
FeatureFlagger.control.released_features_to_all
|
70
79
|
end
|
71
80
|
|
81
|
+
def released_to_all?(*feature_key)
|
82
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
83
|
+
FeatureFlagger.control.released_to_all?(feature.key)
|
84
|
+
end
|
85
|
+
|
86
|
+
def detached_feature_keys
|
87
|
+
rollout_resource_name = feature_flagger_model_settings.entity_name
|
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}:",'') }
|
91
|
+
end
|
92
|
+
|
93
|
+
def cleanup_detached(*feature_key)
|
94
|
+
complete_feature_key = feature_key.map(&:to_s).insert(0, feature_flagger_model_settings.entity_name)
|
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
|
+
|
72
100
|
def rollout_resource_name
|
73
101
|
klass_name = self.to_s
|
74
102
|
klass_name.gsub!(/::/, '_')
|
@@ -78,6 +106,17 @@ module FeatureFlagger
|
|
78
106
|
klass_name.downcase!
|
79
107
|
klass_name
|
80
108
|
end
|
109
|
+
|
110
|
+
def feature_flagger_model_settings
|
111
|
+
@feature_flagger_model_settings ||= FeatureFlagger::ModelSettings.new(
|
112
|
+
identifier_field: :id,
|
113
|
+
entity_name: rollout_resource_name
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
def feature_flagger_identifier
|
118
|
+
public_send(feature_flagger_model_settings.identifier_field)
|
119
|
+
end
|
81
120
|
end
|
82
121
|
end
|
83
122
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module FeatureFlagger
|
2
|
+
class ModelSettings
|
3
|
+
def initialize(arguments)
|
4
|
+
arguments.each do |field, value|
|
5
|
+
self.public_send("#{field}=", value)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# Public: identifier_field Refers to which field must represent the unique model
|
10
|
+
# id.
|
11
|
+
attr_accessor :identifier_field
|
12
|
+
|
13
|
+
# Public: entity_name to which entity the model is targeting.
|
14
|
+
# Take this yaml file as example:
|
15
|
+
#
|
16
|
+
# account:
|
17
|
+
# email_marketing:
|
18
|
+
# whitelabel:
|
19
|
+
# description: a rollout
|
20
|
+
# owner: core
|
21
|
+
# account_in_migration:
|
22
|
+
# email_marketing:
|
23
|
+
# whitelabel:
|
24
|
+
# description: a rollout
|
25
|
+
# owner: core
|
26
|
+
#
|
27
|
+
# class Account < ActiveRecord::Base
|
28
|
+
# include FeatureFlagger::Model
|
29
|
+
#
|
30
|
+
# feature_flagger do |config|
|
31
|
+
# config.identifier_field = :cdp_tenant_id
|
32
|
+
# config.entity_name = :account_in_migration
|
33
|
+
# end
|
34
|
+
attr_accessor :entity_name
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FeatureFlagger
|
4
|
+
module Storage
|
5
|
+
class FeatureKeysMigration
|
6
|
+
|
7
|
+
def initialize(from_redis, to_control)
|
8
|
+
@from_redis = from_redis
|
9
|
+
@to_control = to_control
|
10
|
+
end
|
11
|
+
|
12
|
+
# call migrates features key from the old fashioned to the new
|
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
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def migrate_key(key)
|
31
|
+
return migrate_release_to_all(key) if feature_released_to_all?(key)
|
32
|
+
|
33
|
+
migrate_release(key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def migrate_release_to_all(key)
|
37
|
+
features = @from_redis.smembers(key)
|
38
|
+
|
39
|
+
features.each do |feature_key|
|
40
|
+
@to_control.release_to_all(feature_key)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def feature_released_to_all?(key)
|
45
|
+
FeatureFlagger::Control::RELEASED_FEATURES == key
|
46
|
+
end
|
47
|
+
|
48
|
+
def migrate_release(key)
|
49
|
+
resource_ids = @from_redis.smembers(key)
|
50
|
+
|
51
|
+
resource_ids.each do |id|
|
52
|
+
@to_control.release(key, id)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,21 @@
|
|
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
|
@@ -1,11 +1,13 @@
|
|
1
1
|
require 'redis'
|
2
2
|
require 'redis-namespace'
|
3
|
+
require_relative './keys'
|
3
4
|
|
4
5
|
module FeatureFlagger
|
5
6
|
module Storage
|
6
7
|
class Redis
|
7
|
-
|
8
8
|
DEFAULT_NAMESPACE = :feature_flagger
|
9
|
+
RESOURCE_PREFIX = "_r".freeze
|
10
|
+
SCAN_EACH_BATCH_SIZE = 1000.freeze
|
9
11
|
|
10
12
|
def initialize(redis)
|
11
13
|
@redis = redis
|
@@ -17,21 +19,104 @@ module FeatureFlagger
|
|
17
19
|
new(ns)
|
18
20
|
end
|
19
21
|
|
22
|
+
def fetch_releases(resource_name, resource_id, global_key)
|
23
|
+
resource_key = resource_key(resource_name, resource_id)
|
24
|
+
releases = @redis.sunion(resource_key, global_key)
|
25
|
+
|
26
|
+
releases.select{ |release| release.start_with?(resource_name) }
|
27
|
+
end
|
28
|
+
|
20
29
|
def has_value?(key, value)
|
21
30
|
@redis.sismember(key, value)
|
22
31
|
end
|
23
32
|
|
24
|
-
def add(
|
25
|
-
|
33
|
+
def add(feature_key, resource_name, resource_id)
|
34
|
+
resource_key = resource_key(resource_name, resource_id)
|
35
|
+
|
36
|
+
@redis.multi do |redis|
|
37
|
+
redis.sadd(feature_key, resource_id)
|
38
|
+
redis.sadd(resource_key, feature_key)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove(feature_key, resource_name, resource_id)
|
43
|
+
resource_key = resource_key(resource_name, resource_id)
|
44
|
+
|
45
|
+
@redis.multi do |redis|
|
46
|
+
redis.srem(feature_key, resource_id)
|
47
|
+
redis.srem(resource_key, feature_key)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def remove_all(global_key, feature_key)
|
52
|
+
@redis.srem(global_key, feature_key)
|
53
|
+
remove_feature_key_from_resources(feature_key)
|
26
54
|
end
|
27
55
|
|
28
|
-
def
|
29
|
-
@redis.
|
56
|
+
def add_all(global_key, key)
|
57
|
+
@redis.sadd(global_key, key)
|
58
|
+
remove_feature_key_from_resources(key)
|
30
59
|
end
|
31
60
|
|
32
61
|
def all_values(key)
|
33
62
|
@redis.smembers(key)
|
34
63
|
end
|
64
|
+
|
65
|
+
# DEPRECATED: this method will be removed from public api on v2.0 version.
|
66
|
+
# use instead the feature_keys method.
|
67
|
+
def search_keys(query)
|
68
|
+
@redis.scan_each(match: query)
|
69
|
+
end
|
70
|
+
|
71
|
+
def feature_keys
|
72
|
+
feature_keys = []
|
73
|
+
|
74
|
+
@redis.scan_each(match: "*") do |key|
|
75
|
+
# Reject keys related to feature responsible for return
|
76
|
+
# released features for a given account.
|
77
|
+
next if key.start_with?("#{RESOURCE_PREFIX}:")
|
78
|
+
|
79
|
+
feature_keys << key
|
80
|
+
end
|
81
|
+
|
82
|
+
feature_keys
|
83
|
+
end
|
84
|
+
|
85
|
+
def synchronize_feature_and_resource
|
86
|
+
FeatureFlagger::Storage::FeatureKeysMigration.new(
|
87
|
+
@redis,
|
88
|
+
FeatureFlagger.control,
|
89
|
+
).call
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def resource_key(resource_name, resource_id)
|
95
|
+
FeatureFlagger::Storage::Keys.resource_key(
|
96
|
+
RESOURCE_PREFIX,
|
97
|
+
resource_name,
|
98
|
+
resource_id,
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def remove_feature_key_from_resources(feature_key)
|
103
|
+
cursor = 0
|
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
|
119
|
+
end
|
35
120
|
end
|
36
121
|
end
|
37
122
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
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
|
+
keys = FeatureFlagger::Manager.detached_feature_keys
|
5
|
+
puts "Found keys to remove: #{keys}"
|
6
|
+
keys.each do |key|
|
7
|
+
FeatureFlagger::Manager.cleanup_detached key
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Synchronizes resource_keys with feature_keys, recommended to apps that installed feature flagger before v.1.2.0"
|
12
|
+
task :migrate_to_resource_keys => :environment do
|
13
|
+
storage = FeatureFlagger.config.storage
|
14
|
+
storage.synchronize_feature_and_resource
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Release feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:release\[Account,email_marketing:whitelabel,1,2,3,4\]`"
|
18
|
+
task :release, [:entity_name, :feature_key] => :environment do |_, args|
|
19
|
+
entity = args.entity_name.constantize
|
20
|
+
entity_ids = args.extras
|
21
|
+
entity.release_id(entity_ids, *args.feature_key.split(':'))
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "Unrelease feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:unrelease\[Account,email_marketing:whitelabel,1,2,3,4\]`"
|
25
|
+
task :unrelease, [:entity_name, :feature_key] => :environment do |_, args|
|
26
|
+
entity, entity_ids = args.entity_name.constantize, args.extras
|
27
|
+
entity.unrelease_id(entity_ids, *args.feature_key.split(':'))
|
28
|
+
end
|
29
|
+
|
30
|
+
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, [:entity_name, :feature_key] => :environment do |_, args|
|
32
|
+
entity = args.entity_name.constantize
|
33
|
+
entity.release_to_all(*args.feature_key.split(':'))
|
34
|
+
end
|
35
|
+
|
36
|
+
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, [:entity_name, :feature_key] => :environment do |_, args|
|
38
|
+
entity = args.entity_name.constantize
|
39
|
+
entity.unrelease_to_all(*args.feature_key.split(':'))
|
40
|
+
end
|
41
|
+
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: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nando Sousa
|
@@ -9,64 +9,64 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-08-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - "
|
18
|
+
- - ">"
|
19
19
|
- !ruby/object:Gem::Version
|
20
20
|
version: '3.2'
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
|
-
- - "
|
25
|
+
- - ">"
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: '3.2'
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: redis-namespace
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
|
-
- - "
|
32
|
+
- - ">"
|
33
33
|
- !ruby/object:Gem::Version
|
34
34
|
version: '1.3'
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
|
-
- - "
|
39
|
+
- - ">"
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '1.3'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: bundler
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
|
-
- - "
|
46
|
+
- - ">="
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version: '
|
48
|
+
version: '0'
|
49
49
|
type: :development
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
|
-
- - "
|
53
|
+
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: '
|
55
|
+
version: '0'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: rake
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
60
|
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '13.0'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
67
|
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
69
|
+
version: '13.0'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: rspec
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,20 +81,34 @@ dependencies:
|
|
81
81
|
- - "~>"
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: '3.0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: simplecov
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - '='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0.17'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - '='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0.17'
|
84
98
|
- !ruby/object:Gem::Dependency
|
85
99
|
name: fakeredis
|
86
100
|
requirement: !ruby/object:Gem::Requirement
|
87
101
|
requirements:
|
88
|
-
- -
|
102
|
+
- - '='
|
89
103
|
- !ruby/object:Gem::Version
|
90
|
-
version:
|
104
|
+
version: 0.8.0
|
91
105
|
type: :development
|
92
106
|
prerelease: false
|
93
107
|
version_requirements: !ruby/object:Gem::Requirement
|
94
108
|
requirements:
|
95
|
-
- -
|
109
|
+
- - '='
|
96
110
|
- !ruby/object:Gem::Version
|
97
|
-
version:
|
111
|
+
version: 0.8.0
|
98
112
|
description: Management tool to make it easier rollouting features to customers.
|
99
113
|
email:
|
100
114
|
- nandosousafr@gmail.com
|
@@ -108,10 +122,17 @@ files:
|
|
108
122
|
- lib/feature_flagger.rb
|
109
123
|
- lib/feature_flagger/configuration.rb
|
110
124
|
- lib/feature_flagger/control.rb
|
125
|
+
- lib/feature_flagger/core_ext.rb
|
111
126
|
- lib/feature_flagger/feature.rb
|
127
|
+
- lib/feature_flagger/manager.rb
|
112
128
|
- lib/feature_flagger/model.rb
|
129
|
+
- lib/feature_flagger/model_settings.rb
|
130
|
+
- lib/feature_flagger/railtie.rb
|
131
|
+
- lib/feature_flagger/storage/feature_keys_migration.rb
|
132
|
+
- lib/feature_flagger/storage/keys.rb
|
113
133
|
- lib/feature_flagger/storage/redis.rb
|
114
134
|
- lib/feature_flagger/version.rb
|
135
|
+
- lib/tasks/feature_flagger.rake
|
115
136
|
homepage: http://github.com/ResultadosDigitais/feature_flagger
|
116
137
|
licenses:
|
117
138
|
- MIT
|
@@ -124,15 +145,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
124
145
|
requirements:
|
125
146
|
- - ">="
|
126
147
|
- !ruby/object:Gem::Version
|
127
|
-
version: '
|
148
|
+
version: '2.5'
|
128
149
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
150
|
requirements:
|
130
151
|
- - ">="
|
131
152
|
- !ruby/object:Gem::Version
|
132
|
-
version:
|
153
|
+
version: 2.0.0
|
133
154
|
requirements: []
|
134
|
-
|
135
|
-
rubygems_version: 2.5.2.3
|
155
|
+
rubygems_version: 3.1.4
|
136
156
|
signing_key:
|
137
157
|
specification_version: 4
|
138
158
|
summary: Partial release your features.
|