consul_application_settings 2.1.1 → 4.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 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