consul_application_settings 2.1.0 → 4.0.0.pre.alpha

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: afd23b9159a84c892c965172fd02452d93c77aeaa1391aa31e9690ed69dedd7d
4
- data.tar.gz: ec62bb128a4aa6cc773f59010a56b59ec59961e76158bf45d2dbc2b606f98e81
3
+ metadata.gz: 709d93b5794a39ed2f78eb61462cc07ff0ed77dccd66343ad22fc13abc45c267
4
+ data.tar.gz: 6daa5f0e9266fad7baee4e44efe8ed49ac5451744bd3abb60667afb1afb1a132
5
5
  SHA512:
6
- metadata.gz: 5762fe9054a80ea27e464f5778a98234540801165ff24244087e498bd7d8634a9bab3ecb69b3cfdaa8850196822ce9eb484e3eaa04d0929c5ba5810e1d619c8e
7
- data.tar.gz: 97cb89da4c846ff2c2df46e71b493a28030f3266017ff37a657e7afb628555f3fbe0a5e74fa418a64f17c8a2a1e2ad9f62455cddc5ea725d4505a532e32992f7
6
+ metadata.gz: 399c74e1bed212dea80f9b5d3f0f202c702b2e7704824214703f8003841b35792b66e3c6707ec7594d4b6860acbdd1b341c65e85312448d9fef3e2aa4a1c6c16
7
+ data.tar.gz: 4b04c272e96bc975d0cae6984dc6c44e4d442b0dc1a78a51b9d6ca772b4d5b09bcc10ce6155113a955210e9abe4d91ccb446224fd881f0a79f29da55aebb6fc7
@@ -14,12 +14,19 @@ 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'
23
+ vault:
24
+ image: vault
25
+ ports:
26
+ - "8200:8200"
27
+ env:
28
+ VAULT_DEV_ROOT_TOKEN_ID: root_token
29
+ SKIP_SETCAP: true
23
30
  steps:
24
31
  - name: Checkout
25
32
  uses: actions/checkout@v1
@@ -39,13 +46,15 @@ jobs:
39
46
  - name: Set up Dependencies
40
47
  run: bundle install --path vendor/bundle
41
48
 
42
- - name: Run Tests
43
- uses: paambaati/codeclimate-action@v2.3.0
49
+ - name: Run specs
44
50
  env:
45
51
  COVERAGE: true
46
- CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
47
- with:
48
- coverageCommand: ./bin/rspec
52
+ run: bundle exec rspec
53
+
54
+ - name: Upload coverage
55
+ env:
56
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
57
+ run: bash <(curl -s https://codecov.io/bash)
49
58
 
50
59
  release:
51
60
  runs-on: ubuntu-latest
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/.simplecov CHANGED
@@ -1 +1,6 @@
1
- SimpleCov.start if ENV["COVERAGE"]
1
+ if ENV["COVERAGE"]
2
+ require 'simplecov'
3
+ require 'codecov'
4
+ SimpleCov.start
5
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
6
+ end
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,22 +2,31 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  consul_application_settings (0.0.0)
5
- diplomat (~> 2.1.3)
5
+ deep_merge (~> 1.2)
6
+ diplomat (~> 2.5.1)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
10
11
  ast (2.4.0)
12
+ aws-eventstream (1.2.0)
13
+ aws-sigv4 (1.4.0)
14
+ aws-eventstream (~> 1, >= 1.0.2)
15
+ codecov (0.4.3)
16
+ simplecov (>= 0.15, < 0.22)
11
17
  deep_merge (1.2.1)
12
18
  diff-lcs (1.3)
13
- diplomat (2.1.3)
14
- deep_merge (~> 1.0, >= 1.0.1)
15
- faraday (~> 0.9)
19
+ diplomat (2.5.1)
20
+ deep_merge (~> 1.2)
21
+ faraday (>= 0.9)
16
22
  docile (1.3.1)
17
- faraday (0.17.3)
23
+ faraday (1.3.0)
24
+ faraday-net_http (~> 1.0)
18
25
  multipart-post (>= 1.2, < 3)
26
+ ruby2_keywords
27
+ faraday-net_http (1.0.1)
19
28
  jaro_winkler (1.5.2)
20
- json (2.2.0)
29
+ json (2.3.1)
21
30
  multipart-post (2.1.1)
22
31
  parallel (1.16.2)
23
32
  parser (2.6.2.0)
@@ -49,24 +58,29 @@ GEM
49
58
  rubocop-rspec (1.32.0)
50
59
  rubocop (>= 0.60.0)
51
60
  ruby-progressbar (1.10.0)
61
+ ruby2_keywords (0.0.4)
52
62
  simplecov (0.16.1)
53
63
  docile (~> 1.1)
54
64
  json (>= 1.8, < 3)
55
65
  simplecov-html (~> 0.10.0)
56
66
  simplecov-html (0.10.2)
57
67
  unicode-display_width (1.5.0)
68
+ vault (0.16.0)
69
+ aws-sigv4
58
70
 
59
71
  PLATFORMS
60
72
  ruby
61
73
 
62
74
  DEPENDENCIES
63
75
  bundler (~> 2.0)
76
+ codecov (~> 0.4)
64
77
  consul_application_settings!
65
78
  rake (~> 13.0)
66
79
  rspec (~> 3.0)
67
80
  rubocop (~> 0.66)
68
81
  rubocop-rspec (~> 1.32.0)
69
82
  simplecov (~> 0.16)
83
+ vault (~> 0.16)
70
84
 
71
85
  BUNDLED WITH
72
86
  2.1.2
data/README.md CHANGED
@@ -1,29 +1,18 @@
1
1
  # ConsulApplicationSettings
2
2
 
3
3
  ![Build Status](https://github.com/matic-insurance/consul_application_settings/workflows/ci/badge.svg?branch=master)
4
- [![Test Coverage](https://api.codeclimate.com/v1/badges/b0eaebcf83898535ea4e/test_coverage)](https://codeclimate.com/github/matic-insurance/consul_application_settings/test_coverage)
5
- [![Maintainability](https://api.codeclimate.com/v1/badges/b0eaebcf83898535ea4e/maintainability)](https://codeclimate.com/github/matic-insurance/consul_application_settings/maintainability)
4
+ [![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)
6
5
 
7
- Gem that simplifies usage of Consul (via [Diplomat gem](https://github.com/WeAreFarmGeek/diplomat))
8
- to host application settings. Gem provides defaults via yaml files and other utilities
9
- to simplify storage and control of application with Consul KV storage.
6
+ This gem that simplifies usage of Consul (via [Diplomat gem](https://github.com/WeAreFarmGeek/diplomat))
7
+ to host application settings.
10
8
 
11
- Gem is trying to solve a problem of distributing application settings for local development environment and provide defaults
12
- in production before custom value is set inside of consul.
9
+ Except reading value from Consul the gem also:
10
+ - Fallbacks to YAML if value is missing in consul
11
+ - Resolve actual value from other sources to facilitate overriding via ENV, storing secret values in Vault,
12
+ or executing small ERB snippets
13
13
 
14
- Example use cases:
15
-
16
- - One engineer has created a new feature that depend on consul key/value.
17
-
18
- How enginner can notify other engineers that they need to set this value in their consul environments?
19
-
20
- - DevOps team responsible to configure and maintain deployment.
21
-
22
- How do they learn (have reference) of what settings and structure application expect?
23
-
24
- Gem reads any particular setting from consul and if it is missing tries to find value in YAML defaults file
25
-
26
- **NOTE** Consul is requested every time you query the settings. Defaults YAML file is loaded in memory and is not changing.
14
+ Default values in YAML also can be considered as a way to communicate structure of settings to other engineers.
15
+ Default values also support local settings to allow override on local environment or deployment in production.
27
16
 
28
17
  ## Installation
29
18
 
@@ -40,17 +29,27 @@ gem 'consul_application_settings'
40
29
  At the load of application:
41
30
  ```ruby
42
31
  ConsulApplicationSettings.configure do |config|
43
- # Specify path to the base settings YML. Default: 'config/application_settings.yml'
44
- config.base_file_path = Rails.root.join('config/my_settings.yml')
45
- # Specify path to the local settings YML, which overrides the base file. Default: 'config/application_settings.local.yml'
46
- config.local_file_path = Rails.root.join('config/my_settings.local.yml')
47
- # Specify whether exceprion should be thrown on Consul connection errors. Default: false
32
+ # Specify path to the base settings YML. Default: 'config/app_settings.yml'
33
+ config.base_file_path = Rails.root.join('config/app_settings.yml')
34
+ # Specify path to the local settings YML, which overrides the base file. Default: 'config/app_settings.local.yml'
35
+ config.local_file_path = Rails.root.join('config/app_settings.local.yml')
36
+ # Specify whether exception should be thrown on Consul connection errors. Default: false
48
37
  config.disable_consul_connection_errors = true
38
+ # Specify setting providers. Default: [ConsulApplicationSettings::Providers::ConsulPreloaded, ConsulApplicationSettings::Providers::LocalStorage]
39
+ config.settings_providers = [
40
+ ConsulApplicationSettings::Providers::Consul,
41
+ ConsulApplicationSettings::Providers::LocalStorage
42
+ ]
43
+ # Specify how values will be additionally resolved. Default: [ConsulApplicationSettings::Resolvers::Env]
44
+ config.value_resolvers = [
45
+ ConsulApplicationSettings::Resolvers::Erb,
46
+ ConsulApplicationSettings::Resolvers::Env,
47
+ ]
49
48
  end
50
49
 
51
- APP_SETTINGS = ConsulApplicationSettings.load
52
50
  # Specify path to settings both in YML files and Consul
53
- AUTH_SETTIGNS = ConsulApplicationSettings.load('authentication')
51
+ AUTH_SETTIGNS = ConsulApplicationSettings.load('my_cool_app')
52
+ # Load at root without any prefix: APP_SETTINGS = ConsulApplicationSettings.load
54
53
  ```
55
54
 
56
55
  **NOTE** For rails you can add this code to custom initializer `console_application_settings.rb` in `app/config/initializers`
@@ -61,35 +60,32 @@ AUTH_SETTIGNS = ConsulApplicationSettings.load('authentication')
61
60
 
62
61
  Assuming your defaults file in repository `config/application_settings.yml` looks like:
63
62
  ```yaml
64
- staging:
65
- my_cool_app:
66
- app_name: 'MyCoolApp'
67
- hostname: 'http://localhost:3001'
68
-
69
- integrations:
70
- database:
71
- domain: localhost
72
- user: app
73
- password: password1234
74
- slack:
75
- enabled: false
76
- webhook_url: 'https://hooks.slack.com/services/XXXXXX/XXXXX/XXXXXXX'
63
+ my_cool_app:
64
+ app_name: 'MyCoolApp'
65
+ hostname: 'http://localhost:3001'
66
+
67
+ integrations:
68
+ database:
69
+ domain: localhost
70
+ user: app
71
+ password: password1234
72
+ slack:
73
+ enabled: false
74
+ webhook_url: 'https://hooks.slack.com/services/XXXXXX/XXXXX/XXXXXXX'
77
75
  ```
78
76
 
79
77
  And consul has following settings
80
78
  ```json
81
79
  {
82
- "staging": {
83
- "my_cool_app": {
84
- "hostname": "https://mycoolapp.com",
85
- "integrations": {
86
- "database": {
87
- "domain": "194.78.92.19",
88
- "password": "*************"
89
- },
90
- "slack": {
91
- "enabled": "true"
92
- }
80
+ "my_cool_app": {
81
+ "hostname": "https://mycoolapp.com",
82
+ "integrations": {
83
+ "database": {
84
+ "domain": "194.78.92.19",
85
+ "password": "*************"
86
+ },
87
+ "slack": {
88
+ "enabled": "true"
93
89
  }
94
90
  }
95
91
  }
@@ -116,10 +112,60 @@ gem provides interface to avoid duplicating absolute path
116
112
 
117
113
  ```ruby
118
114
  # You can load subsettings from root object
119
- db_settings = APP_SETTINGS.load('integrations/database')
115
+ db_settings = APP_SETTINGS.load('integrations/database') # ConsulApplicationSettings::Reader
120
116
  db_settings.get(:domain) # "194.78.92.19"
121
117
  db_settings['user'] # "app"
122
- ```
118
+
119
+ #if you try to get subsettings via get - error is raised
120
+ APP_SETTINGS.get('integrations/database') # raise ConsulApplicationSettings::Error
121
+ ```
122
+
123
+ ## Advanced Configurations
124
+
125
+ ### Setting Providers
126
+ Providers controls how and in which order settings are retrieved.
127
+ When application asks for specific setting - gem retrieves them from every provider in order of configuration
128
+ until one returns not nil value.
129
+
130
+ Default order for providers is:
131
+ 1. `ConsulApplicationSettings::Providers::ConsulPreloaded`
132
+ 2. `ConsulApplicationSettings::Providers::LocalStorage`
133
+
134
+ List of built in providers:
135
+ - `ConsulApplicationSettings::Providers::ConsulPreloaded` - Retrieves all settings from consul on every `.load`
136
+ - `ConsulApplicationSettings::Providers::Consul` - Retrieves setting every time `.get` method is called
137
+ - `ConsulApplicationSettings::Providers::LocalStorage` - Retrieves all settings from local files on every `.load`
138
+
139
+ Custom provider can be added as long as it support following interface:
140
+ ```ruby
141
+ class CustomProvider
142
+ #constructor
143
+ def initialize(base_path, config)
144
+ end
145
+
146
+ # get value by `base_path + '/' + path`
147
+ def get(path)
148
+ end
149
+ end
150
+ ```
151
+
152
+ ### Resolvers
153
+ Once value is retrieved - it will be additionally processed by resolvers.
154
+ This allows for additional flexibility like getting values from external sources.
155
+ While every resolver can be implemented in a form of a provider - one will be limited by the structure of settings,
156
+ while other system might not be compatible with this.
157
+
158
+ When value is retrieved - gem finds **first** provider that can resolve value and resolves it.
159
+ Resolved value is returned to application.
160
+
161
+ Default list of resolvers:
162
+ - `ConsulApplicationSettings::Resolvers::Env`
163
+
164
+ List of built in resolvers
165
+ - `ConsulApplicationSettings::Resolvers::Env` - resolves any value by looking up environment variable.
166
+ Matching any value that starts with `env://`. Value like `env://TEST_URL` will be resolved as `ENV['TEST_URL']`
167
+ - `ConsulApplicationSettings::Resolvers::Erb` - resolves value by rendering it via ERB.
168
+ Matching any value that contains `<%` and `%>` in it. Value like `<%= 2 + 2 %>` will be resolved as `4`
123
169
 
124
170
  ### Gem Configuration
125
171
  You can configure gem with block:
@@ -140,15 +186,18 @@ All Gem configurations
140
186
  | base_file_path | no | 'config/application_settings.yml' | String | Path to the file with base settings |
141
187
  | local_file_path | no | 'config/application_settings.local.yml' | String | Path to the file with local settings overriding the base settings |
142
188
  | disable_consul_connection_errors | no | true | Boolean | Do not raise exception when consul is not available (useful for development) |
189
+ | settings_providers | no | Array(Provider) | Array | Specify custom setting provider lists |
190
+ | value_resolvers | no | Array(Resolver) | Array | Specify custom value resolvers lists |
143
191
 
144
192
  ## Development
145
193
 
146
- 1. [Install Consul](https://www.consul.io/docs/install/index.html)
147
194
  1. Run `bin/setup` to install dependencies
148
- 1. Run tests `rspec`
149
- 1. Add new test
150
- 1. Add new code
151
- 1. Go to step 3
195
+ 2. Run `docker-compose up` to spin up dependencies (Consul)
196
+ 3. Run tests `rspec`
197
+ 4. Add new test
198
+ 5. Add new code
199
+ 6. Go to step 3
200
+ 7. Create PR
152
201
 
153
202
  ## Contributing
154
203
 
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,12 +29,15 @@ 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 'diplomat', '~> 2.1.3'
32
+ spec.add_dependency 'deep_merge', '~> 1.2'
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'
42
+ spec.add_development_dependency 'vault', '~> 0.16'
40
43
  end
@@ -0,0 +1,17 @@
1
+ # dependencies needed for development environment
2
+ version: '3'
3
+ services:
4
+ vault:
5
+ image: vault
6
+ ports:
7
+ - "8200:8200"
8
+ cap_add:
9
+ - IPC_LOCK
10
+ environment:
11
+ VAULT_DEV_ROOT_TOKEN_ID: root_token
12
+ consul:
13
+ image: consul
14
+ ports:
15
+ - "8500:8500"
16
+ environment:
17
+ 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
@@ -0,0 +1,24 @@
1
+ module ConsulApplicationSettings
2
+ module Resolvers
3
+ # Resolve values using vault
4
+ class Vault
5
+ IDENTIFIER = 'vault://'.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
+ # Expect value in format `vault://mount/path/to/secret?attribute_name`
14
+ def resolve(value, _path)
15
+ value = value.delete_prefix(IDENTIFIER)
16
+ mount, secret = value.split('/', 2)
17
+ secret, attribute = secret.split('?')
18
+ attribute ||= 'value'
19
+ secret = ::Vault.kv(mount).read(secret)
20
+ secret && secret.data[attribute.to_sym]
21
+ end
22
+ end
23
+ end
24
+ 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.0'.freeze
2
+ VERSION = '4.0.0-alpha'.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,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consul_application_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 4.0.0.pre.alpha
5
5
  platform: ruby
6
6
  authors:
7
7
  - Volodymyr Mykhailyk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-05-25 00:00:00.000000000 Z
11
+ date: 2021-12-06 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
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: 2.1.3
33
+ version: 2.5.1
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: 2.1.3
40
+ version: 2.5.1
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  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,6 +136,20 @@ dependencies:
108
136
  - - "~>"
109
137
  - !ruby/object:Gem::Version
110
138
  version: '0.16'
139
+ - !ruby/object:Gem::Dependency
140
+ name: vault
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.16'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.16'
111
153
  description: |-
112
154
  Gem that simplifies usage of Consul (via Diplomat gem) to host application settings.
113
155
  Gem provides defaults and utilities
@@ -117,7 +159,6 @@ executables: []
117
159
  extensions: []
118
160
  extra_rdoc_files: []
119
161
  files:
120
- - ".codeclimate.yml"
121
162
  - ".github/workflows/main.yml"
122
163
  - ".gitignore"
123
164
  - ".rspec"
@@ -130,15 +171,23 @@ files:
130
171
  - LICENSE.txt
131
172
  - README.md
132
173
  - Rakefile
174
+ - bin/benchmark
133
175
  - bin/console
134
176
  - bin/rspec
135
177
  - bin/setup
136
178
  - consul_application_settings.gemspec
179
+ - docker-compose.yml
137
180
  - lib/consul_application_settings.rb
138
181
  - lib/consul_application_settings/configuration.rb
139
- - lib/consul_application_settings/consul_provider.rb
140
- - lib/consul_application_settings/file_provider.rb
141
- - lib/consul_application_settings/settings_provider.rb
182
+ - lib/consul_application_settings/providers/abstract.rb
183
+ - lib/consul_application_settings/providers/consul.rb
184
+ - lib/consul_application_settings/providers/consul_preloaded.rb
185
+ - lib/consul_application_settings/providers/local_storage.rb
186
+ - lib/consul_application_settings/reader.rb
187
+ - lib/consul_application_settings/resolvers/abstract.rb
188
+ - lib/consul_application_settings/resolvers/env.rb
189
+ - lib/consul_application_settings/resolvers/erb.rb
190
+ - lib/consul_application_settings/resolvers/vault.rb
142
191
  - lib/consul_application_settings/utils.rb
143
192
  - lib/consul_application_settings/version.rb
144
193
  homepage: https://github.com/matic-insurance/consul_application_settings
@@ -159,11 +208,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
159
208
  version: '0'
160
209
  required_rubygems_version: !ruby/object:Gem::Requirement
161
210
  requirements:
162
- - - ">="
211
+ - - ">"
163
212
  - !ruby/object:Gem::Version
164
- version: '0'
213
+ version: 1.3.1
165
214
  requirements: []
166
- rubygems_version: 3.0.3
215
+ rubygems_version: 3.0.3.1
167
216
  signing_key:
168
217
  specification_version: 4
169
218
  summary: Application settings via Consul with yaml defaults
data/.codeclimate.yml DELETED
@@ -1,15 +0,0 @@
1
- version: 2
2
- plugins:
3
- # disabled before git-legal supports bundler 2 https://github.com/kmewhort/git.legal-codeclimate/issues/6
4
- # git-legal:
5
- # enabled: true
6
- rubocop:
7
- enabled: true
8
- channel: rubocop-0-73
9
- config:
10
- file: ".rubocop.yml"
11
- bundler-audit:
12
- enabled: true
13
- exclude_patterns:
14
- - "spec/"
15
- - "*.gemspec"
@@ -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