diff --git a/README.md b/README.md index 3ea72ac..c0784a3 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,35 @@ to ensure the data stored in Redis storage is right. Check [#67](https://github. $ bundle exec rake feature_flagger:migrate_to_resource_keys +## Extra options + +There are a few options to store/retrieve your rollout manifest (a.k.a rollout.yml): + +If you have a rollout.yml file and want to use Redis to keep a backup, add the follow code to the configuration block: + +```ruby +require 'feature_flagger/manifest_sources/yaml_with_backup_to_storage' +FeatureFlagger.configure do |config| + ... + config.manifest_source = FeatureFlagger::ManifestSources::YAMLWithBackupToStorage.new(config.storage) + ... +end +``` + +If you already have your manifest on Redis and prefer not to keep a copy in your application, add the following code to the configuration block: + +```ruby +require 'feature_flagger/manifest_sources/storage_only' + +FeatureFlagger.configure do |config| + ... + config.manifest_source = FeatureFlagger::ManifestSources::StorageOnly.new(config.storage) + ... +end +``` + +If you have the YAML file and don't need a backup, it is unnecessary to do any different configuration. + ## Contributing Bug reports and pull requests are welcome! diff --git a/lib/feature_flagger.rb b/lib/feature_flagger.rb index 35c5421..cb69c8a 100644 --- a/lib/feature_flagger.rb +++ b/lib/feature_flagger.rb @@ -11,6 +11,7 @@ require 'feature_flagger/manager' require 'feature_flagger/railtie' require 'feature_flagger/notifier' +require 'feature_flagger/manifest_sources/with_yaml_file' module FeatureFlagger class << self diff --git a/lib/feature_flagger/configuration.rb b/lib/feature_flagger/configuration.rb index 1b09dcc..eef7216 100644 --- a/lib/feature_flagger/configuration.rb +++ b/lib/feature_flagger/configuration.rb @@ -1,10 +1,10 @@ module FeatureFlagger class Configuration - attr_accessor :storage, :cache_store, :yaml_filepath, :notifier_callback + attr_accessor :storage, :cache_store, :manifest_source, :notifier_callback def initialize @storage ||= Storage::Redis.default_client - @yaml_filepath ||= default_yaml_filepath + @manifest_source ||= FeatureFlagger::ManifestSources::WithYamlFile.new @notifier_callback = nil @cache_store = nil end @@ -17,7 +17,7 @@ def cache_store=(cache_store) end def info - @info ||= YAML.load_file(yaml_filepath) if yaml_filepath + @manifest_source.resolved_info end def mapped_feature_keys(resource_name = nil) @@ -29,10 +29,6 @@ def mapped_feature_keys(resource_name = nil) private - def default_yaml_filepath - "#{Rails.root}/config/rollout.yml" if defined?(Rails) - end - def make_keys_recursively(hash, keys = [], composed_key = []) unless hash.values[0].is_a?(Hash) keys.push(composed_key) diff --git a/lib/feature_flagger/manifest_sources/storage_only.rb b/lib/feature_flagger/manifest_sources/storage_only.rb new file mode 100644 index 0000000..0ada5f2 --- /dev/null +++ b/lib/feature_flagger/manifest_sources/storage_only.rb @@ -0,0 +1,13 @@ +module FeatureFlagger + module ManifestSources + class StorageOnly + def initialize(storage) + @storage = storage + end + + def resolved_info + YAML.load(@storage.read_manifest_backup) + end + end + end +end diff --git a/lib/feature_flagger/manifest_sources/with_yaml_file.rb b/lib/feature_flagger/manifest_sources/with_yaml_file.rb new file mode 100644 index 0000000..7ef739e --- /dev/null +++ b/lib/feature_flagger/manifest_sources/with_yaml_file.rb @@ -0,0 +1,14 @@ +module FeatureFlagger + module ManifestSources + class WithYamlFile + def initialize(yaml_path = nil) + @yaml_path = yaml_path + @yaml_path ||= "#{Rails.root}/config/rollout.yml" if defined?(Rails) + end + + def resolved_info + @resolved_info ||= ::YAML.load_file(@yaml_path) if @yaml_path + end + end + end +end diff --git a/lib/feature_flagger/manifest_sources/yaml_with_backup_to_storage.rb b/lib/feature_flagger/manifest_sources/yaml_with_backup_to_storage.rb new file mode 100644 index 0000000..c487224 --- /dev/null +++ b/lib/feature_flagger/manifest_sources/yaml_with_backup_to_storage.rb @@ -0,0 +1,18 @@ +module FeatureFlagger + module ManifestSources + class YAMLWithBackupToStorage + def initialize(storage, yaml_path = nil) + @yaml_path = yaml_path || ("#{Rails.root}/config/rollout.yml" if defined?(Rails)) + @storage = storage + end + + def resolved_info + @resolved_info ||= begin + yaml_data = YAML.load_file(@yaml_path) if @yaml_path + @storage.write_manifest_backup(YAML.dump(yaml_data)) + yaml_data + end + end + end + end +end diff --git a/lib/feature_flagger/storage/redis.rb b/lib/feature_flagger/storage/redis.rb index f6bbe09..f8b3fa6 100644 --- a/lib/feature_flagger/storage/redis.rb +++ b/lib/feature_flagger/storage/redis.rb @@ -5,8 +5,10 @@ module FeatureFlagger module Storage class Redis - DEFAULT_NAMESPACE = :feature_flagger - RESOURCE_PREFIX = "_r".freeze + DEFAULT_NAMESPACE = :feature_flagger + RESOURCE_PREFIX = "_r".freeze + MANIFEST_PREFIX = "_m".freeze + MANIFEST_KEY = "manifest_file".freeze SCAN_EACH_BATCH_SIZE = 1000.freeze def initialize(redis) @@ -75,6 +77,7 @@ def feature_keys # Reject keys related to feature responsible for return # released features for a given account. next if key.start_with?("#{RESOURCE_PREFIX}:") + next if key.start_with?("#{MANIFEST_PREFIX}:") feature_keys << key end @@ -89,6 +92,14 @@ def synchronize_feature_and_resource ).call end + def read_manifest_backup + @redis.get("#{MANIFEST_PREFIX}:#{MANIFEST_KEY}") + end + + def write_manifest_backup(yaml_as_string) + @redis.set("#{MANIFEST_PREFIX}:#{MANIFEST_KEY}", yaml_as_string) + end + private def resource_key(resource_name, resource_id) diff --git a/lib/feature_flagger/version.rb b/lib/feature_flagger/version.rb index c54ec69..28614dc 100644 --- a/lib/feature_flagger/version.rb +++ b/lib/feature_flagger/version.rb @@ -1,3 +1,3 @@ module FeatureFlagger - VERSION = "2.1.1" + VERSION = "2.2.0" end diff --git a/spec/feature_flagger/configuration_spec.rb b/spec/feature_flagger/configuration_spec.rb index 06f3316..1c77f78 100644 --- a/spec/feature_flagger/configuration_spec.rb +++ b/spec/feature_flagger/configuration_spec.rb @@ -55,8 +55,8 @@ module FeatureFlagger let(:configuration) { described_class.new } before do - filepath = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) - configuration.yaml_filepath = filepath + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + allow(configuration).to receive(:info).and_return(YAML.load_file(yaml_path)) end context 'without resource name' do diff --git a/spec/feature_flagger/feature_spec.rb b/spec/feature_flagger/feature_spec.rb index db82eef..f8a7911 100644 --- a/spec/feature_flagger/feature_spec.rb +++ b/spec/feature_flagger/feature_spec.rb @@ -1,12 +1,13 @@ require 'spec_helper' +require 'feature_flagger/manifest_sources/with_yaml_file' module FeatureFlagger RSpec.describe Feature do subject { Feature.new(key, :feature_flagger_dummy_class) } before do - filepath = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) - FeatureFlagger.config.yaml_filepath = filepath + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + FeatureFlagger.config.manifest_source = FeatureFlagger::ManifestSources::WithYamlFile.new(yaml_path) end describe '#initialize' do diff --git a/spec/feature_flagger/manager_spec.rb b/spec/feature_flagger/manager_spec.rb index b09a162..99efce0 100644 --- a/spec/feature_flagger/manager_spec.rb +++ b/spec/feature_flagger/manager_spec.rb @@ -11,8 +11,8 @@ module FeatureFlagger config.storage = storage end - filepath = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) - FeatureFlagger.config.yaml_filepath = filepath + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + FeatureFlagger.config.manifest_source = FeatureFlagger::ManifestSources::WithYamlFile.new(yaml_path) # All good here FeatureFlagger.control.release_to_all('feature_flagger_dummy_class:email_marketing:behavior_score') @@ -45,8 +45,8 @@ module FeatureFlagger end FeatureFlagger.control.release(feature_key, 0) - filepath = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) - FeatureFlagger.config.yaml_filepath = filepath + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + FeatureFlagger.config.manifest_source = FeatureFlagger::ManifestSources::WithYamlFile.new(yaml_path) end it 'cleanup key' do @@ -59,8 +59,8 @@ module FeatureFlagger context "mapped feature key" do before do - filepath = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) - FeatureFlagger.config.yaml_filepath = filepath + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + FeatureFlagger.config.manifest_source = FeatureFlagger::ManifestSources::WithYamlFile.new(yaml_path) end it 'do not cleanup key' do diff --git a/spec/feature_flagger/model_spec.rb b/spec/feature_flagger/model_spec.rb index a0ffe36..3aa7a55 100644 --- a/spec/feature_flagger/model_spec.rb +++ b/spec/feature_flagger/model_spec.rb @@ -14,8 +14,8 @@ def id; 14 end let(:control) { FeatureFlagger.control } before do - filepath = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) - FeatureFlagger.config.yaml_filepath = filepath + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + FeatureFlagger.config.manifest_source = FeatureFlagger::ManifestSources::WithYamlFile.new(yaml_path) end describe '#release' do @@ -128,8 +128,8 @@ def id; 14 end FeatureFlagger.control.release('feature_flagger_dummy_class:feature_a', 0) FeatureFlagger.control.release('feature_flagger_dummy_class:feature_b', 0) - filepath = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) - FeatureFlagger.config.yaml_filepath = filepath + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + FeatureFlagger.config.manifest_source = FeatureFlagger::ManifestSources::WithYamlFile.new(yaml_path) end it 'returns all detached feature keys' do @@ -149,8 +149,8 @@ def id; 14 end end FeatureFlagger.control.release(feature_key, 0) - filepath = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) - FeatureFlagger.config.yaml_filepath = filepath + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + FeatureFlagger.config.manifest_source = FeatureFlagger::ManifestSources::WithYamlFile.new(yaml_path) end it 'cleanup key' do @@ -190,6 +190,5 @@ def uuid ) end end - end end diff --git a/spec/feature_flagger/storage/feature_keys_migration_spec.rb b/spec/feature_flagger/storage/feature_keys_migration_spec.rb index f6eb6e8..14d41a6 100644 --- a/spec/feature_flagger/storage/feature_keys_migration_spec.rb +++ b/spec/feature_flagger/storage/feature_keys_migration_spec.rb @@ -12,8 +12,8 @@ let(:global_key) { FeatureFlagger::Control::RELEASED_FEATURES } before do - filepath = File.expand_path('../fixtures/rollout_example.yml', __dir__) - FeatureFlagger.config.yaml_filepath = filepath + yaml_path = File.expand_path('../fixtures/rollout_example.yml', __dir__) + FeatureFlagger.config.manifest_source = FeatureFlagger::ManifestSources::WithYamlFile.new(yaml_path) end describe '.call' do diff --git a/spec/manifests/storage_only_spec.rb b/spec/manifests/storage_only_spec.rb new file mode 100644 index 0000000..896a5f7 --- /dev/null +++ b/spec/manifests/storage_only_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' +require 'feature_flagger/manifest_sources/storage_only' + +module FeatureFlagger + module ManifestSources + RSpec.describe StorageOnly do + describe '.resolved_info' do + context 'without local yaml file' do + it 'returns a yaml file from storage' do + + storage = spy('Storage') + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + yaml_data = YAML.load_file(yaml_path) + yaml_as_string = YAML.dump(yaml_data) + allow(storage).to receive(:read_manifest_backup).and_return(yaml_as_string) + + manifest_source = StorageOnly.new(storage) + expect(manifest_source.resolved_info).to eq(yaml_data) + end + end + end + end + end +end diff --git a/spec/manifests/with_yaml_file_spec.rb b/spec/manifests/with_yaml_file_spec.rb new file mode 100644 index 0000000..3f8d081 --- /dev/null +++ b/spec/manifests/with_yaml_file_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' +require 'feature_flagger/manifest_sources/with_yaml_file' + +module FeatureFlagger + module ManifestSources + RSpec.describe WithYamlFile do + describe '.resolved_info' do + context 'whit local yaml file' do + it 'returns a yaml file from yaml_path' do + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + manifest_source = WithYamlFile.new(yaml_path) + expect(manifest_source.resolved_info).to eq(YAML.load_file(yaml_path)) + end + end + end + end + end +end diff --git a/spec/manifests/yaml_with_backup_to_storage_spec.rb b/spec/manifests/yaml_with_backup_to_storage_spec.rb new file mode 100644 index 0000000..e968a77 --- /dev/null +++ b/spec/manifests/yaml_with_backup_to_storage_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' +require 'feature_flagger/manifest_sources/yaml_with_backup_to_storage' + +module FeatureFlagger + module ManifestSources + RSpec.describe YAMLWithBackupToStorage do + describe '.resolved_info' do + context 'with local yaml file to storage' do + it 'returns a yaml file from yaml_path with backup to storage' do + storage = spy('Storage') + yaml_path = File.expand_path('../../fixtures/rollout_example.yml', __FILE__) + yaml_data = YAML.load_file(yaml_path) + yaml_as_string = YAML.dump(yaml_data) + + manifest_source = YAMLWithBackupToStorage.new(storage, yaml_path) + expect(manifest_source.resolved_info).to eq(yaml_data) + + expect(storage).to have_received(:write_manifest_backup).with(yaml_as_string) + end + end + end + end + end +end