consul_application_settings 2.1.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e6c1820af05af3bcc9adca21879296cbcfc8898e69ffa7136bac1d785ae1bb1
4
- data.tar.gz: 0e8b5c29bf2992a6d36e7a064b8887922b69ec5f820073157b53a21dc5905089
3
+ metadata.gz: 361bfff1ca209489842135445a5265240919a881bd37b6463a8f64bd6c7583bd
4
+ data.tar.gz: a71131c9eeb965821b20c027be2c27ebe6000d5f1fffaaf1e806fea61c1be86e
5
5
  SHA512:
6
- metadata.gz: 976fec73196a99d0a642d9f4d9243beb0a0925f44f140ff6e77091dac1919f36472b1467344e190da01b071803412d6ec612ccb31f8751a9d06d3f471a49fd1f
7
- data.tar.gz: e0d2e02025e94d2d5fc6ac4dd24a4073f9ac228969e67afbd9afb5cafa297c6a347dd68a53fac9a4ad6b4c2ce875b036372894b175e1bd36e6db138c1b31e9d9
6
+ metadata.gz: 1218d2841e41b34762d66e8eba0bc9d1db1b790114c59e7ba2fcecab34e4eb66d163fb3b21b41be96cb5f91d4a490d410b5372e5f7052157442db9b038f574b0
7
+ data.tar.gz: 864a3e31e29382c54a9cb046d62ddc85eca46a380a92214fb4b4b378de6aab9731a88981d7c46735d9cd9e8b692932ea127ba858a7864a0a3ae117d48ff7e100
@@ -14,9 +14,9 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
  strategy:
16
16
  matrix:
17
- ruby: [ '2.5.x', '2.6.x' ]
17
+ ruby: [ '2.5.x', '2.6.x', '2.7.x', '3.0.x' ]
18
18
  services:
19
- redis:
19
+ consul:
20
20
  image: consul:1.6
21
21
  ports:
22
22
  - '8500:8500'
data/.rubocop.yml CHANGED
@@ -2,6 +2,7 @@ AllCops:
2
2
  TargetRubyVersion: 2.5
3
3
  Exclude:
4
4
  - 'tmp/**/*'
5
+ - 'bin/**/*'
5
6
 
6
7
  Metrics/LineLength:
7
8
  Max: 120
@@ -10,4 +11,4 @@ Style/FrozenStringLiteralComment:
10
11
  Enabled: false
11
12
 
12
13
  Metrics/BlockLength:
13
- ExcludedMethods: ['describe', 'context']
14
+ ExcludedMethods: ['describe', 'context', 'shared_examples']
data/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [4.0.0-alpha]
4
+ ### Breaking Changes
5
+ - When using `settings.get` to retrieve object - exception is raised
6
+ ### New features
7
+ - Introduce resolvers concept
8
+ - Add environment resolver
9
+ - Add erb resolver
10
+ - Add vault alpha resolver
11
+
12
+ ## [3.0.1]
13
+ ### Fixes
14
+ - Fix exception when preloading settings without consul
15
+
16
+ ## [3.0.0]
17
+ ### Breaking Changes
18
+ - Use Preloaded Consul Settings Provider by default
19
+ ### New features
20
+ - Configurable setting providers
21
+ - Preloaded Consul Settings Provider to prioritize performance over consistency
22
+ - Performance tests in spec
23
+ - Benchmarking script
24
+ ### Fixes
25
+ - Return nil instead of empty hash when reading missing setting from file
26
+ - Return nil instead of empty string when reading missing value from Consul
27
+ - Add missing load method on Settings Reader to create object with narrow scope
28
+
29
+ ## [2.1.1]
30
+ ### Changes
31
+ - Update Diplomat to latest version
32
+
33
+ ## [2.1.0]
34
+ ### Fixes
35
+ - Return nil for unknown keys
36
+
37
+ ## [2.0.0]
38
+ ### Breaking Changes
39
+ - Change default naming for setting files
40
+
41
+ ## [1.0.0]
42
+ ### Features
43
+ - Add support for second settings file (local settings for development)
44
+
45
+ ## [0.1.4]
46
+ ### Fixes
47
+ - Clone values before returning
48
+
3
49
  ## [0.1.3]
4
50
  ### Fixes
5
51
  - Add `Diplomat::PathNotFound` to the list of caught exceptions
@@ -22,7 +68,15 @@
22
68
  - Support deep settings search
23
69
  - Support nested configs
24
70
 
25
- [Unreleased]: https://github.com/matic-insurance/consul_application_settings/compare/0.1.3...HEAD
71
+ [Unreleased]: https://github.com/matic-insurance/consul_application_settings/compare/4.0.0-alpha...HEAD
72
+ [4.0.0-alpha]: https://github.com/matic-insurance/consul_application_settings/compare/3.0.1...4.0.0-alpha
73
+ [3.0.1]: https://github.com/matic-insurance/consul_application_settings/compare/3.0.0...3.0.1
74
+ [3.0.0]: https://github.com/matic-insurance/consul_application_settings/compare/2.0.0...3.0.0
75
+ [2.1.1]: https://github.com/matic-insurance/consul_application_settings/compare/2.1.0...2.1.1
76
+ [2.1.0]: https://github.com/matic-insurance/consul_application_settings/compare/2.0.0...2.1.0
77
+ [2.0.0]: https://github.com/matic-insurance/consul_application_settings/compare/1.0.0...2.0.0
78
+ [1.0.0]: https://github.com/matic-insurance/consul_application_settings/compare/0.1.4...1.0.0
79
+ [0.1.4]: https://github.com/matic-insurance/consul_application_settings/compare/0.1.3...0.1.4
26
80
  [0.1.3]: https://github.com/matic-insurance/consul_application_settings/compare/0.1.2...0.1.3
27
81
  [0.1.2]: https://github.com/matic-insurance/consul_application_settings/compare/0.1.1...0.1.2
28
82
  [0.1.1]: https://github.com/matic-insurance/consul_application_settings/compare/0.1.0...0.1.1
data/Gemfile.lock CHANGED
@@ -2,6 +2,7 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  consul_application_settings (0.0.0)
5
+ deep_merge (~> 1.2)
5
6
  diplomat (~> 2.5.1)
6
7
 
7
8
  GEM
@@ -76,4 +77,4 @@ DEPENDENCIES
76
77
  simplecov (~> 0.16)
77
78
 
78
79
  BUNDLED WITH
79
- 2.1.2
80
+ 2.1.4
data/README.md CHANGED
@@ -1,28 +1,22 @@
1
1
  # ConsulApplicationSettings
2
2
 
3
+ ## Gem is Deprecated in favor of [SettingsReader](https://github.com/matic-insurance/settings_reader)
4
+ This gem is deprecated and all functionality has been moved to `SettingsReader`
5
+ See More: https://github.com/matic-insurance/settings_reader
6
+
3
7
  ![Build Status](https://github.com/matic-insurance/consul_application_settings/workflows/ci/badge.svg?branch=master)
4
8
  [![Test Coverage](https://codecov.io/gh/matic-insurance/consul_application_settings/branch/master/graph/badge.svg?token=5E8NA8EE8L)](https://codecov.io/gh/matic-insurance/consul_application_settings)
5
9
 
6
- Gem that simplifies usage of Consul (via [Diplomat gem](https://github.com/WeAreFarmGeek/diplomat))
7
- to host application settings. Gem provides defaults via yaml files and other utilities
8
- to simplify storage and control of application with Consul KV storage.
9
-
10
- Gem is trying to solve a problem of distributing application settings for local development environment and provide defaults
11
- in production before custom value is set inside of consul.
12
-
13
- Example use cases:
14
-
15
- - One engineer has created a new feature that depend on consul key/value.
16
-
17
- How enginner can notify other engineers that they need to set this value in their consul environments?
18
-
19
- - DevOps team responsible to configure and maintain deployment.
10
+ This gem that simplifies usage of Consul (via [Diplomat gem](https://github.com/WeAreFarmGeek/diplomat))
11
+ to host application settings.
20
12
 
21
- How do they learn (have reference) of what settings and structure application expect?
13
+ Except reading value from Consul the gem also:
14
+ - Fallbacks to YAML if value is missing in consul
15
+ - Resolve actual value from other sources to facilitate overriding via ENV, storing secret values in Vault,
16
+ or executing small ERB snippets
22
17
 
23
- Gem reads any particular setting from consul and if it is missing tries to find value in YAML defaults file
24
-
25
- **NOTE** Consul is requested every time you query the settings. Defaults YAML file is loaded in memory and is not changing.
18
+ Default values in YAML also can be considered as a way to communicate structure of settings to other engineers.
19
+ Default values also support local settings to allow override on local environment or deployment in production.
26
20
 
27
21
  ## Installation
28
22
 
@@ -39,17 +33,27 @@ gem 'consul_application_settings'
39
33
  At the load of application:
40
34
  ```ruby
41
35
  ConsulApplicationSettings.configure do |config|
42
- # Specify path to the base settings YML. Default: 'config/application_settings.yml'
43
- config.base_file_path = Rails.root.join('config/my_settings.yml')
44
- # Specify path to the local settings YML, which overrides the base file. Default: 'config/application_settings.local.yml'
45
- config.local_file_path = Rails.root.join('config/my_settings.local.yml')
46
- # Specify whether exceprion should be thrown on Consul connection errors. Default: false
36
+ # Specify path to the base settings YML. Default: 'config/app_settings.yml'
37
+ config.base_file_path = Rails.root.join('config/app_settings.yml')
38
+ # Specify path to the local settings YML, which overrides the base file. Default: 'config/app_settings.local.yml'
39
+ config.local_file_path = Rails.root.join('config/app_settings.local.yml')
40
+ # Specify whether exception should be thrown on Consul connection errors. Default: false
47
41
  config.disable_consul_connection_errors = true
42
+ # Specify setting providers. Default: [ConsulApplicationSettings::Providers::ConsulPreloaded, ConsulApplicationSettings::Providers::LocalStorage]
43
+ config.settings_providers = [
44
+ ConsulApplicationSettings::Providers::Consul,
45
+ ConsulApplicationSettings::Providers::LocalStorage
46
+ ]
47
+ # Specify how values will be additionally resolved. Default: [ConsulApplicationSettings::Resolvers::Env]
48
+ config.value_resolvers = [
49
+ ConsulApplicationSettings::Resolvers::Erb,
50
+ ConsulApplicationSettings::Resolvers::Env,
51
+ ]
48
52
  end
49
53
 
50
- APP_SETTINGS = ConsulApplicationSettings.load
51
54
  # Specify path to settings both in YML files and Consul
52
- AUTH_SETTIGNS = ConsulApplicationSettings.load('authentication')
55
+ AUTH_SETTIGNS = ConsulApplicationSettings.load('my_cool_app')
56
+ # Load at root without any prefix: APP_SETTINGS = ConsulApplicationSettings.load
53
57
  ```
54
58
 
55
59
  **NOTE** For rails you can add this code to custom initializer `console_application_settings.rb` in `app/config/initializers`
@@ -60,35 +64,32 @@ AUTH_SETTIGNS = ConsulApplicationSettings.load('authentication')
60
64
 
61
65
  Assuming your defaults file in repository `config/application_settings.yml` looks like:
62
66
  ```yaml
63
- staging:
64
- my_cool_app:
65
- app_name: 'MyCoolApp'
66
- hostname: 'http://localhost:3001'
67
-
68
- integrations:
69
- database:
70
- domain: localhost
71
- user: app
72
- password: password1234
73
- slack:
74
- enabled: false
75
- webhook_url: 'https://hooks.slack.com/services/XXXXXX/XXXXX/XXXXXXX'
67
+ my_cool_app:
68
+ app_name: 'MyCoolApp'
69
+ hostname: 'http://localhost:3001'
70
+
71
+ integrations:
72
+ database:
73
+ domain: localhost
74
+ user: app
75
+ password: password1234
76
+ slack:
77
+ enabled: false
78
+ webhook_url: 'https://hooks.slack.com/services/XXXXXX/XXXXX/XXXXXXX'
76
79
  ```
77
80
 
78
81
  And consul has following settings
79
82
  ```json
80
83
  {
81
- "staging": {
82
- "my_cool_app": {
83
- "hostname": "https://mycoolapp.com",
84
- "integrations": {
85
- "database": {
86
- "domain": "194.78.92.19",
87
- "password": "*************"
88
- },
89
- "slack": {
90
- "enabled": "true"
91
- }
84
+ "my_cool_app": {
85
+ "hostname": "https://mycoolapp.com",
86
+ "integrations": {
87
+ "database": {
88
+ "domain": "194.78.92.19",
89
+ "password": "*************"
90
+ },
91
+ "slack": {
92
+ "enabled": "true"
92
93
  }
93
94
  }
94
95
  }
@@ -115,10 +116,60 @@ gem provides interface to avoid duplicating absolute path
115
116
 
116
117
  ```ruby
117
118
  # You can load subsettings from root object
118
- db_settings = APP_SETTINGS.load('integrations/database')
119
+ db_settings = APP_SETTINGS.load('integrations/database') # ConsulApplicationSettings::Reader
119
120
  db_settings.get(:domain) # "194.78.92.19"
120
121
  db_settings['user'] # "app"
121
- ```
122
+
123
+ #if you try to get subsettings via get - error is raised
124
+ APP_SETTINGS.get('integrations/database') # raise ConsulApplicationSettings::Error
125
+ ```
126
+
127
+ ## Advanced Configurations
128
+
129
+ ### Setting Providers
130
+ Providers controls how and in which order settings are retrieved.
131
+ When application asks for specific setting - gem retrieves them from every provider in order of configuration
132
+ until one returns not nil value.
133
+
134
+ Default order for providers is:
135
+ 1. `ConsulApplicationSettings::Providers::ConsulPreloaded`
136
+ 2. `ConsulApplicationSettings::Providers::LocalStorage`
137
+
138
+ List of built in providers:
139
+ - `ConsulApplicationSettings::Providers::ConsulPreloaded` - Retrieves all settings from consul on every `.load`
140
+ - `ConsulApplicationSettings::Providers::Consul` - Retrieves setting every time `.get` method is called
141
+ - `ConsulApplicationSettings::Providers::LocalStorage` - Retrieves all settings from local files on every `.load`
142
+
143
+ Custom provider can be added as long as it support following interface:
144
+ ```ruby
145
+ class CustomProvider
146
+ #constructor
147
+ def initialize(base_path, config)
148
+ end
149
+
150
+ # get value by `base_path + '/' + path`
151
+ def get(path)
152
+ end
153
+ end
154
+ ```
155
+
156
+ ### Resolvers
157
+ Once value is retrieved - it will be additionally processed by resolvers.
158
+ This allows for additional flexibility like getting values from external sources.
159
+ While every resolver can be implemented in a form of a provider - one will be limited by the structure of settings,
160
+ while other system might not be compatible with this.
161
+
162
+ When value is retrieved - gem finds **first** provider that can resolve value and resolves it.
163
+ Resolved value is returned to application.
164
+
165
+ Default list of resolvers:
166
+ - `ConsulApplicationSettings::Resolvers::Env`
167
+
168
+ List of built in resolvers
169
+ - `ConsulApplicationSettings::Resolvers::Env` - resolves any value by looking up environment variable.
170
+ Matching any value that starts with `env://`. Value like `env://TEST_URL` will be resolved as `ENV['TEST_URL']`
171
+ - `ConsulApplicationSettings::Resolvers::Erb` - resolves value by rendering it via ERB.
172
+ Matching any value that contains `<%` and `%>` in it. Value like `<%= 2 + 2 %>` will be resolved as `4`
122
173
 
123
174
  ### Gem Configuration
124
175
  You can configure gem with block:
@@ -139,15 +190,18 @@ All Gem configurations
139
190
  | base_file_path | no | 'config/application_settings.yml' | String | Path to the file with base settings |
140
191
  | local_file_path | no | 'config/application_settings.local.yml' | String | Path to the file with local settings overriding the base settings |
141
192
  | disable_consul_connection_errors | no | true | Boolean | Do not raise exception when consul is not available (useful for development) |
193
+ | settings_providers | no | Array(Provider) | Array | Specify custom setting provider lists |
194
+ | value_resolvers | no | Array(Resolver) | Array | Specify custom value resolvers lists |
142
195
 
143
196
  ## Development
144
197
 
145
- 1. [Install Consul](https://www.consul.io/docs/install/index.html)
146
198
  1. Run `bin/setup` to install dependencies
147
- 1. Run tests `rspec`
148
- 1. Add new test
149
- 1. Add new code
150
- 1. Go to step 3
199
+ 2. Run `docker-compose up` to spin up dependencies (Consul)
200
+ 3. Run tests `rspec`
201
+ 4. Add new test
202
+ 5. Add new code
203
+ 6. Go to step 3
204
+ 7. Create PR
151
205
 
152
206
  ## Contributing
153
207
 
data/bin/benchmark ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'consul_application_settings'
5
+ require 'benchmark'
6
+
7
+ ITERATIONS = 1000
8
+
9
+ def kill_consul
10
+ `pgrep consul | xargs kill`
11
+ end
12
+
13
+ def start_consul
14
+ spawn('consul agent -dev -node machine > /dev/null 2>&1')
15
+ end
16
+
17
+ def application_settings(providers)
18
+ file = 'spec/fixtures/base_application_settings.yml'
19
+ ConsulApplicationSettings.configure do |config|
20
+ config.settings_providers = providers
21
+ end
22
+ ConsulApplicationSettings.load(file)
23
+ end
24
+
25
+ def benchmark_gem
26
+ Benchmark.bm(20) do |x|
27
+ x.report('Real Time Consul') do
28
+ ca = application_settings([
29
+ ConsulApplicationSettings::Providers::Consul,
30
+ ConsulApplicationSettings::Providers::LocalStorage
31
+ ])
32
+ ITERATIONS.times { ca.get('application/name') }
33
+ end
34
+
35
+ x.report('Preloaded Consul') do
36
+ ca = application_settings([
37
+ ConsulApplicationSettings::Providers::ConsulPreloaded,
38
+ ConsulApplicationSettings::Providers::LocalStorage
39
+ ])
40
+ ITERATIONS.times { ca.get('application/name') }
41
+ end
42
+
43
+ x.report('File Only') do
44
+ ca = application_settings([
45
+ ConsulApplicationSettings::Providers::ConsulPreloaded
46
+ ])
47
+ ITERATIONS.times { ca.get('application/name') }
48
+ end
49
+ end
50
+ end
51
+
52
+ puts '-' * 80
53
+ puts 'Benchmark without consul agent'
54
+ kill_consul
55
+ benchmark_gem
56
+
57
+ puts '-' * 80
58
+ puts 'Benchmark with consul agent running'
59
+ start_consul
60
+ benchmark_gem
61
+
62
+ kill_consul
@@ -29,13 +29,20 @@ Gem::Specification.new do |spec|
29
29
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
30
  spec.require_paths = ['lib']
31
31
 
32
+ spec.add_dependency 'deep_merge', '~> 1.2'
32
33
  spec.add_dependency 'diplomat', '~> 2.5.1'
33
34
 
34
35
  spec.add_development_dependency 'bundler', '~> 2.0'
36
+ spec.add_development_dependency 'codecov', '~> 0.4'
35
37
  spec.add_development_dependency 'rake', '~> 13.0'
36
38
  spec.add_development_dependency 'rspec', '~> 3.0'
37
39
  spec.add_development_dependency 'rubocop', '~> 0.66'
38
40
  spec.add_development_dependency 'rubocop-rspec', '~> 1.32.0'
39
41
  spec.add_development_dependency 'simplecov', '~> 0.16'
40
- spec.add_development_dependency 'codecov', '~> 0.4'
42
+
43
+ spec.post_install_message = <<~MESSAGE
44
+ ! The 'consul_application_settings' gem has been deprecated and has been replaced by 'settings_reader'.
45
+ ! See: https://rubygems.org/gems/settings_reader
46
+ ! And: https://github.com/matic-insurance/settings_reader
47
+ MESSAGE
41
48
  end
@@ -0,0 +1,9 @@
1
+ # dependencies needed for development environment
2
+ version: '3'
3
+ services:
4
+ consul:
5
+ image: consul
6
+ ports:
7
+ - "8500:8500"
8
+ environment:
9
+ CONSUL_BIND_INTERFACE: eth0
@@ -3,12 +3,22 @@ module ConsulApplicationSettings
3
3
  class Configuration
4
4
  DEFAULT_BASE_FILE_PATH = 'config/app_settings.yml'.freeze
5
5
  DEFAULT_LOCAL_FILE_PATH = 'config/app_settings.local.yml'.freeze
6
- attr_accessor :base_file_path, :local_file_path, :disable_consul_connection_errors
6
+ DEFAULT_PROVIDERS = [
7
+ ConsulApplicationSettings::Providers::ConsulPreloaded,
8
+ ConsulApplicationSettings::Providers::LocalStorage
9
+ ].freeze
10
+ DEFAULT_RESOLVERS = [
11
+ ConsulApplicationSettings::Resolvers::Env
12
+ ].freeze
13
+ attr_accessor :base_file_path, :local_file_path, :disable_consul_connection_errors,
14
+ :settings_providers, :value_resolvers
7
15
 
8
16
  def initialize
9
17
  @base_file_path = DEFAULT_BASE_FILE_PATH
10
18
  @local_file_path = DEFAULT_LOCAL_FILE_PATH
11
19
  @disable_consul_connection_errors = true
20
+ @settings_providers = DEFAULT_PROVIDERS
21
+ @value_resolvers = DEFAULT_RESOLVERS
12
22
  end
13
23
  end
14
24
  end
@@ -0,0 +1,26 @@
1
+ module ConsulApplicationSettings
2
+ module Providers
3
+ # Abstract class with basic functionality
4
+ class Abstract
5
+ def initialize(base_path, config)
6
+ @base_path = base_path
7
+ @config = config
8
+ end
9
+
10
+ def get(_path)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ protected
15
+
16
+ def absolute_key_path(path)
17
+ ConsulApplicationSettings::Utils.generate_path(@base_path, path)
18
+ end
19
+
20
+ def get_value_from_hash(path, data)
21
+ parts = ConsulApplicationSettings::Utils.decompose_path(path)
22
+ data.dig(*parts).clone
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ require 'diplomat'
2
+
3
+ module ConsulApplicationSettings
4
+ module Providers
5
+ # Provides access to settings stored in Consul
6
+ class Consul < Abstract
7
+ def get(path)
8
+ full_path = absolute_key_path(path)
9
+ value = get_from_consul(full_path)
10
+ value = resolve_tree_response(value, full_path)
11
+ ConsulApplicationSettings::Utils.cast_consul_value(value)
12
+ end
13
+
14
+ private
15
+
16
+ def get_from_consul(path)
17
+ Diplomat::Kv.get(path, recurse: true)
18
+ rescue Diplomat::KeyNotFound
19
+ nil
20
+ rescue SystemCallError, Faraday::ConnectionFailed, Diplomat::PathNotFound => e
21
+ raise e unless @config.disable_consul_connection_errors
22
+ end
23
+
24
+ def resolve_tree_response(value, full_path)
25
+ return value unless value.is_a?(Array)
26
+
27
+ value.each { |item| item[:key] = item[:key].delete_prefix("#{full_path}/") }
28
+ value
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ module ConsulApplicationSettings
2
+ module Providers
3
+ # Provides access to settings stored in Consul. Loads them once
4
+ class ConsulPreloaded < Abstract
5
+ def initialize(base_path, config)
6
+ super
7
+ @data = read_all_from_consul
8
+ end
9
+
10
+ def get(path)
11
+ value = get_value_from_hash(absolute_key_path(path), @data)
12
+ value = resolve_tree_response(value)
13
+ ConsulApplicationSettings::Utils.cast_consul_value(value)
14
+ end
15
+
16
+ protected
17
+
18
+ def read_all_from_consul
19
+ Diplomat::Kv.get_all(@base_path, convert_to_hash: true)
20
+ rescue Diplomat::KeyNotFound
21
+ {}
22
+ rescue SystemCallError, Faraday::ConnectionFailed, Diplomat::PathNotFound => e
23
+ raise e unless @config.disable_consul_connection_errors
24
+
25
+ {}
26
+ end
27
+
28
+ def resolve_tree_response(value)
29
+ return value unless value.is_a?(Hash)
30
+
31
+ deep_cast(value)
32
+ end
33
+
34
+ def deep_cast(hash)
35
+ hash.each_pair do |k, v|
36
+ if v.is_a?(Hash)
37
+ deep_cast(v)
38
+ else
39
+ hash[k] = ConsulApplicationSettings::Utils.cast_consul_value(v)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,42 @@
1
+ require 'yaml'
2
+
3
+ module ConsulApplicationSettings
4
+ module Providers
5
+ # Provides access to settings stored in file system with support of base and local files
6
+ class LocalStorage < Abstract
7
+ def initialize(base_path, config)
8
+ super
9
+ @data = load
10
+ end
11
+
12
+ def get(path)
13
+ get_value_from_hash(absolute_key_path(path), @data)
14
+ end
15
+
16
+ private
17
+
18
+ def load
19
+ base_yml = read_yml(base_file_path)
20
+ local_yml = read_yml(local_file_path)
21
+ DeepMerge.deep_merge!(local_yml, base_yml, preserve_unmergeables: false, overwrite_arrays: true,
22
+ merge_nil_values: true)
23
+ end
24
+
25
+ def base_file_path
26
+ @config.base_file_path
27
+ end
28
+
29
+ def local_file_path
30
+ @config.local_file_path
31
+ end
32
+
33
+ def read_yml(path)
34
+ return {} unless File.exist?(path)
35
+
36
+ YAML.safe_load(IO.read(path))
37
+ rescue Psych::SyntaxError, Errno::ENOENT => e
38
+ raise ConsulApplicationSettings::Error, "Cannot read settings file at #{path}: #{e.message}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,50 @@
1
+ module ConsulApplicationSettings
2
+ # Orchestrates fetching values from provider and resolving them
3
+ class Reader
4
+ def initialize(base_path, config)
5
+ @base_path = base_path
6
+ @config = config
7
+ @providers = config.settings_providers.map { |provider| provider.new(base_path, config) }
8
+ @resolvers = config.value_resolvers.map(&:new)
9
+ end
10
+
11
+ def get(path)
12
+ value = fetch_value(path)
13
+ resolve_value(value, path)
14
+ end
15
+
16
+ alias [] get
17
+
18
+ def load(sub_path)
19
+ new_path = ConsulApplicationSettings::Utils.generate_path(@base_path, sub_path)
20
+ self.class.new(new_path, @config)
21
+ end
22
+
23
+ protected
24
+
25
+ def check_deep_structure(value, path)
26
+ return unless value.is_a?(Hash)
27
+
28
+ message = "Getting value of complex object at path: '#{path}'. Use #load method to get new scoped instance"
29
+ raise ConsulApplicationSettings::Error, message if value.is_a?(Hash)
30
+ end
31
+
32
+ def fetch_value(path)
33
+ @providers.each do |provider|
34
+ value = provider.get(path)
35
+ check_deep_structure(value, path)
36
+ return value unless value.nil?
37
+ end
38
+ nil
39
+ end
40
+
41
+ def resolve_value(value, path)
42
+ resolver = @resolvers.detect { |r| r.resolvable?(value, path) }
43
+ if resolver
44
+ resolver.resolve(value, path)
45
+ else
46
+ value
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,14 @@
1
+ module ConsulApplicationSettings
2
+ module Resolvers
3
+ # Abstract resolver with basic functionality
4
+ class Abstract
5
+ def resolvable?(_value, _path)
6
+ false
7
+ end
8
+
9
+ def resolve(value, _path)
10
+ value
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module ConsulApplicationSettings
2
+ module Resolvers
3
+ # Resolve values in environment
4
+ class Env
5
+ IDENTIFIER = 'env://'.freeze
6
+
7
+ def resolvable?(value, _path)
8
+ return unless value.respond_to?(:start_with?)
9
+
10
+ value.start_with?(IDENTIFIER)
11
+ end
12
+
13
+ def resolve(value, _path)
14
+ env_path = value.to_s.delete_prefix(IDENTIFIER)
15
+ ENV[env_path]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ module ConsulApplicationSettings
2
+ module Resolvers
3
+ # Run values through ERB
4
+ class Erb
5
+ IDENTIFIER = /(<%).*(%>)/.freeze
6
+
7
+ def resolvable?(value, _path)
8
+ return unless value.is_a?(String)
9
+
10
+ IDENTIFIER.match?(value)
11
+ end
12
+
13
+ def resolve(value, _path)
14
+ ERB.new(value.to_s).result
15
+ end
16
+ end
17
+ end
18
+ end
@@ -6,8 +6,11 @@ module ConsulApplicationSettings
6
6
 
7
7
  class << self
8
8
  def cast_consul_value(value)
9
+ return nil if value.nil?
9
10
  return false if value == 'false'
10
11
  return true if value == 'true'
12
+ return value if value.is_a?(Hash)
13
+ return convert_to_hash(value) if value.is_a?(Array)
11
14
 
12
15
  cast_complex_value(value)
13
16
  end
@@ -33,6 +36,16 @@ module ConsulApplicationSettings
33
36
  end
34
37
  value.to_s
35
38
  end
39
+
40
+ def convert_to_hash(data)
41
+ data_h = data.map do |item|
42
+ value = cast_consul_value(item[:value])
43
+ item[:key].split('/').reverse.reduce(value) { |h, v| { v => h } }
44
+ end
45
+ data_h.reduce({}) do |dest, source|
46
+ DeepMerge.deep_merge!(source, dest, preserve_unmergeables: true)
47
+ end
48
+ end
36
49
  end
37
50
  end
38
51
  end
@@ -1,3 +1,3 @@
1
1
  module ConsulApplicationSettings
2
- VERSION = '2.1.1'.freeze
2
+ VERSION = '4.0.0'.freeze
3
3
  end
@@ -1,8 +1,13 @@
1
1
  require 'consul_application_settings/version'
2
+ require 'consul_application_settings/providers/abstract'
3
+ require 'consul_application_settings/providers/consul'
4
+ require 'consul_application_settings/providers/consul_preloaded'
5
+ require 'consul_application_settings/providers/local_storage'
6
+ require 'consul_application_settings/resolvers/abstract'
7
+ require 'consul_application_settings/resolvers/env'
8
+ require 'consul_application_settings/resolvers/erb'
2
9
  require 'consul_application_settings/configuration'
3
- require 'consul_application_settings/consul_provider'
4
- require 'consul_application_settings/file_provider'
5
- require 'consul_application_settings/settings_provider'
10
+ require 'consul_application_settings/reader'
6
11
  require 'consul_application_settings/utils'
7
12
 
8
13
  # The gem provides possibility to load settings from Consul and automatically fall back to data stored in file system
@@ -11,7 +16,6 @@ module ConsulApplicationSettings
11
16
 
12
17
  class << self
13
18
  attr_accessor :config
14
- attr_accessor :defaults
15
19
  end
16
20
 
17
21
  self.config ||= ConsulApplicationSettings::Configuration.new
@@ -21,6 +25,6 @@ module ConsulApplicationSettings
21
25
  end
22
26
 
23
27
  def self.load(path = '')
24
- SettingsProvider.new(path, config)
28
+ Reader.new(path, config)
25
29
  end
26
30
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consul_application_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Volodymyr Mykhailyk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-22 00:00:00.000000000 Z
11
+ date: 2022-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: deep_merge
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: diplomat
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +52,20 @@ dependencies:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
54
  version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: codecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.4'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: rake
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -108,20 +136,6 @@ dependencies:
108
136
  - - "~>"
109
137
  - !ruby/object:Gem::Version
110
138
  version: '0.16'
111
- - !ruby/object:Gem::Dependency
112
- name: codecov
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: '0.4'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: '0.4'
125
139
  description: |-
126
140
  Gem that simplifies usage of Consul (via Diplomat gem) to host application settings.
127
141
  Gem provides defaults and utilities
@@ -143,15 +157,22 @@ files:
143
157
  - LICENSE.txt
144
158
  - README.md
145
159
  - Rakefile
160
+ - bin/benchmark
146
161
  - bin/console
147
162
  - bin/rspec
148
163
  - bin/setup
149
164
  - consul_application_settings.gemspec
165
+ - docker-compose.yml
150
166
  - lib/consul_application_settings.rb
151
167
  - lib/consul_application_settings/configuration.rb
152
- - lib/consul_application_settings/consul_provider.rb
153
- - lib/consul_application_settings/file_provider.rb
154
- - lib/consul_application_settings/settings_provider.rb
168
+ - lib/consul_application_settings/providers/abstract.rb
169
+ - lib/consul_application_settings/providers/consul.rb
170
+ - lib/consul_application_settings/providers/consul_preloaded.rb
171
+ - lib/consul_application_settings/providers/local_storage.rb
172
+ - lib/consul_application_settings/reader.rb
173
+ - lib/consul_application_settings/resolvers/abstract.rb
174
+ - lib/consul_application_settings/resolvers/env.rb
175
+ - lib/consul_application_settings/resolvers/erb.rb
155
176
  - lib/consul_application_settings/utils.rb
156
177
  - lib/consul_application_settings/version.rb
157
178
  homepage: https://github.com/matic-insurance/consul_application_settings
@@ -161,7 +182,10 @@ metadata:
161
182
  homepage_uri: https://github.com/matic-insurance/consul_application_settings
162
183
  source_code_uri: https://github.com/matic-insurance/consul_application_settings
163
184
  changelog_uri: https://github.com/matic-insurance/consul_application_settings/blob/master/CHANGELOG.md
164
- post_install_message:
185
+ post_install_message: |
186
+ ! The 'consul_application_settings' gem has been deprecated and has been replaced by 'settings_reader'.
187
+ ! See: https://rubygems.org/gems/settings_reader
188
+ ! And: https://github.com/matic-insurance/settings_reader
165
189
  rdoc_options: []
166
190
  require_paths:
167
191
  - lib
@@ -176,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
200
  - !ruby/object:Gem::Version
177
201
  version: '0'
178
202
  requirements: []
179
- rubygems_version: 3.0.3
203
+ rubygems_version: 3.0.3.1
180
204
  signing_key:
181
205
  specification_version: 4
182
206
  summary: Application settings via Consul with yaml defaults
@@ -1,33 +0,0 @@
1
- require 'diplomat'
2
-
3
- module ConsulApplicationSettings
4
- # Provides access to settings stored in Consul
5
- class ConsulProvider
6
- def initialize(base_path, config)
7
- @base_path = base_path
8
- @config = config
9
- end
10
-
11
- def get(path)
12
- value = fetch_value(path)
13
- ConsulApplicationSettings::Utils.cast_consul_value(value)
14
- end
15
-
16
- private
17
-
18
- def fetch_value(path)
19
- full_path = generate_full_path(path)
20
- Diplomat::Kv.get(full_path, {}, :return)
21
- rescue SystemCallError, Faraday::ConnectionFailed, Diplomat::PathNotFound => e
22
- raise e unless disable_consul_connection_errors?
23
- end
24
-
25
- def generate_full_path(path)
26
- ConsulApplicationSettings::Utils.generate_path(@base_path, path)
27
- end
28
-
29
- def disable_consul_connection_errors?
30
- @config.disable_consul_connection_errors
31
- end
32
- end
33
- end
@@ -1,54 +0,0 @@
1
- require 'yaml'
2
-
3
- module ConsulApplicationSettings
4
- # Provides access to settings stored in file system with support of base and local files
5
- class FileProvider
6
- def initialize(base_path, config)
7
- @base_path = base_path
8
- @config = config
9
- load
10
- end
11
-
12
- def get(path)
13
- read_path(path).clone
14
- end
15
-
16
- private
17
-
18
- def load
19
- base_yml = read_yml(base_file_path)
20
- local_yml = read_yml(local_file_path)
21
- @data = DeepMerge.deep_merge!(local_yml, base_yml, preserve_unmergeables: false, overwrite_arrays: true,
22
- merge_nil_values: true)
23
- end
24
-
25
- def base_file_path
26
- @config.base_file_path
27
- end
28
-
29
- def local_file_path
30
- @config.local_file_path
31
- end
32
-
33
- def read_yml(path)
34
- return {} unless File.exist?(path)
35
-
36
- YAML.safe_load(IO.read(path))
37
- rescue Psych::SyntaxError, Errno::ENOENT => e
38
- raise ConsulApplicationSettings::Error, "Cannot read settings file at #{path}: #{e.message}"
39
- end
40
-
41
- def read_path(path)
42
- full_path = ConsulApplicationSettings::Utils.generate_path(@base_path, path)
43
- parts = ConsulApplicationSettings::Utils.decompose_path(full_path)
44
- parts.reduce(@data, &method(:read_value))
45
- end
46
-
47
- def read_value(hash, key)
48
- raise ConsulApplicationSettings::Error, 'reading arrays not implemented' if hash.is_a? Array
49
- return {} if hash.nil?
50
-
51
- hash.fetch(key.to_s, nil)
52
- end
53
- end
54
- end
@@ -1,16 +0,0 @@
1
- module ConsulApplicationSettings
2
- # Provides access to settings stored in Consul or in file system
3
- class SettingsProvider
4
- def initialize(base_path, config)
5
- @consul_provider = ConsulProvider.new(base_path, config)
6
- @file_provider = FileProvider.new(base_path, config)
7
- end
8
-
9
- def get(path)
10
- consul_value = @consul_provider.get(path)
11
- !consul_value.nil? && consul_value != '' ? consul_value : @file_provider.get(path)
12
- end
13
-
14
- alias [] get
15
- end
16
- end