diff --git a/CHANGELOG.md b/CHANGELOG.md index dd71d14..631036a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.20.0] - Unreleased +### Added +- Added regular expressions capabilities to targeting. See [README.md](README.md#Regexp-Targeting) for usage examples. +### Fixed +- Use `Pysch.safe_load_file` over `Pysch.load_file` with `Regexp` as only permitted class. + ## [0.19.0] - 2021-07-09 ### Fixed - Fixed a bug where accessing a setting that is a `Hash` defined across multiple file entries diff --git a/Gemfile.lock b/Gemfile.lock index 517af42..2193d1f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - process_settings (0.19.0) + process_settings (0.20.0.pre.2) activesupport (>= 4.2, < 7) json listen (~> 3.0) @@ -26,11 +26,11 @@ GEM concurrent-ruby (1.1.7) diff-lcs (1.3) docile (1.4.0) - ffi (1.15.3) + ffi (1.15.5) i18n (1.8.5) concurrent-ruby (~> 1.0) - json (2.1.0) - listen (3.5.1) + json (2.6.1) + listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) method_source (0.9.0) @@ -45,7 +45,7 @@ GEM psych (3.3.2) rainbow (3.0.0) rake (12.3.3) - rb-fsevent (0.11.0) + rb-fsevent (0.11.1) rb-inotify (0.10.1) ffi (~> 1.0) rspec (3.8.0) diff --git a/README.md b/README.md index 843c6fd..fb154cb 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,14 @@ target: ``` This will be applied in any process that has (`service_name == "frontend"` OR `service_name == "auth"`) AND `datacenter == "AWS-US-EAST-1"`. +### Regexp Targeting +To provide a regular expression for targeting, use the ruby regex keyword `!ruby/regexp` followed by your regular expression. For example: +``` +target: + service_name: !ruby/regexp /frontend-\d/ +``` +This will be applied in any process that has `service_name` that has `frontend-` followed with a number. As an example this will match `"frontend-1"` + ### Precedence The settings YAML files are always combined in alphabetical order by file path. Later settings take precedence over the earlier ones. diff --git a/lib/process_settings/target.rb b/lib/process_settings/target.rb index 3ea46ab..79b3097 100644 --- a/lib/process_settings/target.rb +++ b/lib/process_settings/target.rb @@ -38,6 +38,12 @@ def with_static_context(target_value, static_context_hash) with_static_context_hash(target_value, static_context_hash) when true, false !target_value == !static_context_hash + when Regexp + if static_context_hash.is_a?(String) + static_context_hash.match?(target_value) + else + static_context_hash == target_value + end else target_value == static_context_hash end @@ -90,6 +96,12 @@ def target_key_matches?(target_value, context_hash) end when true, false target_value + when Regexp + if context_hash.is_a?(String) + context_hash.match?(target_value) + else + context_hash == target_value + end else target_value == context_hash end diff --git a/lib/process_settings/targeted_settings.rb b/lib/process_settings/targeted_settings.rb index 5032fe6..f489902 100644 --- a/lib/process_settings/targeted_settings.rb +++ b/lib/process_settings/targeted_settings.rb @@ -73,7 +73,7 @@ def from_array(settings_array, only_meta: false) end def from_file(file_path, only_meta: false) - json_doc = Psych.load_file(file_path) + json_doc = Psych.safe_load_file(file_path, permitted_classes: [Regexp]) from_array(json_doc, only_meta: only_meta) end end diff --git a/lib/process_settings/version.rb b/lib/process_settings/version.rb index 1aed95d..759ccbf 100644 --- a/lib/process_settings/version.rb +++ b/lib/process_settings/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ProcessSettings - VERSION = '0.19.0' + VERSION = '0.20.0.pre.2' end diff --git a/spec/bin/combine_process_settings_spec.rb b/spec/bin/combine_process_settings_spec.rb index 091e17f..ae1d113 100644 --- a/spec/bin/combine_process_settings_spec.rb +++ b/spec/bin/combine_process_settings_spec.rb @@ -32,6 +32,11 @@ max_recording_seconds: 600 answer_odds: 100 status_change_min_days: + - filename: regex_target.yml + target: + service: !ruby/regexp /frontend/ + settings: + test_setting: 100 - filename: stop_incoming_requests.yml target: region: east diff --git a/spec/fixtures/production/combined_process_settings-20.yml b/spec/fixtures/production/combined_process_settings-20.yml new file mode 100644 index 0000000..01d6333 --- /dev/null +++ b/spec/fixtures/production/combined_process_settings-20.yml @@ -0,0 +1,47 @@ +--- +# +# Don't edit this file directly! It was generated by combine_process_settings from the files in staging/settings/. +# +- filename: honeypot.yml + settings: + honeypot: + max_recording_seconds: 600 + answer_odds: 100 + status_change_min_days: +- filename: regex_target.yml + target: + service: !ruby/regexp /frontend/ + settings: + test_setting: 100 +- filename: telecom/log_level.yml + target: + app: telecom + settings: + logging: + level: debug +- filename: telecom/stop_incoming_requests.yml + target: + app: telecom + region: west + settings: + incoming_requests: 0 +- filename: telecom/debug_sip_private_caller_id.yml + target: + app: telecom + region: west + caller_id: + - "+18053334444" + - "+12755554321" + - "+18052223344" + settings: + log_stream: + sip: caller_id_privacy +- filename: cca/tech-1234_call_counts_drift_investigation.yml + target: + app: ccn + settings: + call_counts: + complete_sync_seconds: 60 +- meta: + version: 20 + END: true diff --git a/spec/fixtures/production/settings/regex_target.yml b/spec/fixtures/production/settings/regex_target.yml new file mode 100644 index 0000000..153cc9b --- /dev/null +++ b/spec/fixtures/production/settings/regex_target.yml @@ -0,0 +1,5 @@ +--- +target: + service: !ruby/regexp /frontend/ +settings: + test_setting: 100 diff --git a/spec/lib/process_settings/target_spec.rb b/spec/lib/process_settings/target_spec.rb index 31e27b7..40fb122 100644 --- a/spec/lib/process_settings/target_spec.rb +++ b/spec/lib/process_settings/target_spec.rb @@ -134,6 +134,81 @@ expect(process_target.target_key_matches?(context_hash)).to be_truthy expect(process_target.target_key_matches?({})).to be_falsey end + + describe "for regex matching" do + subject { process_target.target_key_matches?(context_hash) } + let(:process_target) { described_class.new(target_hash) } + let(:context_hash) do + { + 'service' => service, + 'region' => 'east', + 'cdr' => { 'caller' => '+18056807000' } + } + end + + context "with target value is a regular expression" do + let(:target_hash) { { 'service' => /telecom/ } } + + context "when context hash value matches" do + let(:service) { 'telecom-1' } + + it { is_expected.to be_truthy } + end + + context "with an empty context hash" do + let(:context_hash) { {} } + it { is_expected.to be_falsey } + end + + context "with a non-matching context value" do + let(:service) { 'tele' } + it { is_expected.to be_falsey } + end + end + + context "when target value is a regex as a string" do + let(:target_hash) { { 'service' => '/telecom/' } } + + context "when context hash value has the string but not the slash" do + let(:service) { 'telecom' } + + it { is_expected.to be_falsey } + end + + context "when context hash value exactly matches the string target value" do + let(:service) { '/telecom/' } + it { is_expected.to be_truthy } + end + end + + context "when target value is valid and also has embedded slashes" do + let(:target_hash) { { 'service' => /tmp\/dir\/log/ } } + + context "when context hash value matches the target" do + let(:service) { 'tmp/dir/log/service-log.txt' } + it { is_expected.to be_truthy } + end + + context "when context hash only has whats between the embedded slashes" do + let(:service) { 'dir' } + it { is_expected.to be_falsey } + end + end + + context "when context hash and target hash don't match keys" do + let(:context_hash) do + { + 'service' => { 'name' => service }, + 'region' => 'east', + 'cdr' => { 'caller' => '+18056807000' } + } + end + let(:target_hash) { { 'service' => /telecom/ } } + let(:service) { 'telecom-1' } + + it { is_expected.to be_falsey } + end + end end describe "#with_static_context" do @@ -333,6 +408,81 @@ it { should eq(target_hash) } end end + + describe "for regex matching" do + subject { process_target.with_static_context(context_hash).json_doc } + let(:process_target) { described_class.new(target_hash) } + let(:context_hash) do + { + 'service' => service, + 'region' => 'east', + 'cdr' => { 'caller' => '+18056807000' } + } + end + + context "with target value is a regular expression" do + let(:target_hash) { { 'service' => /telecom/ } } + + context "when context hash value matches" do + let(:service) { 'telecom-1' } + + it { is_expected.to be_truthy } + end + + context "with an empty context hash" do + let(:context_hash) { {} } + it { is_expected.to eq(target_hash) } + end + + context "with a non-matching context value" do + let(:service) { 'tele' } + it { is_expected.to be_falsey } + end + end + + context "when target value is a regex as a string" do + let(:target_hash) { { 'service' => '/telecom/' } } + + context "when context hash value has the string but not the slash" do + let(:service) { 'telecom' } + + it { is_expected.to be_falsey } + end + + context "when context hash value exactly matches the string target value" do + let(:service) { '/telecom/' } + it { is_expected.to be_truthy } + end + end + + context "when target value is valid and also has embedded slashes" do + let(:target_hash) { { 'service' => /tmp\/dir\/log/ } } + + context "when context hash value matches the target" do + let(:service) { 'tmp/dir/log/service-log.txt' } + it { is_expected.to be_truthy } + end + + context "when context hash only has whats between the embedded slashes" do + let(:service) { 'dir' } + it { is_expected.to be_falsey } + end + end + + context "when context hash and target hash don't match keys" do + let(:context_hash) do + { + 'service' => { 'name' => service }, + 'region' => 'east', + 'cdr' => { 'caller' => '+18056807000' } + } + end + let(:target_hash) { { 'service' => /telecom/ } } + let(:service) { 'telecom-1' } + + it { is_expected.to be_falsey } + end + end end describe "class methods" do diff --git a/spec/lib/process_settings/targeted_settings_spec.rb b/spec/lib/process_settings/targeted_settings_spec.rb index eb1d835..085b972 100644 --- a/spec/lib/process_settings/targeted_settings_spec.rb +++ b/spec/lib/process_settings/targeted_settings_spec.rb @@ -78,6 +78,21 @@ end end + context "with ruby's special regular expression keyword" do + let(:file_path) { File.expand_path("../../fixtures/production/combined_process_settings-20.yml", __dir__) } + + describe ".from_file" do + it "reads from yaml file and properly converts the special keyword and expression in the file into a ruby Regexp object" do + targeted_settings = described_class.from_file(file_path) + + regex_target_setting = targeted_settings.targeted_settings_array[1] + expect(regex_target_setting.filename).to eq("regex_target.yml") + expect(regex_target_setting.target.json_doc).to eq({ "service" => /frontend/ }) + expect(regex_target_setting.settings.json_doc).to eq({ "test_setting" => 100 }) + end + end + end + describe ".from_array" do it "delegates" do target_and_settings = described_class.from_array(TARGETED_SETTINGS)