cloud-config 0.1.1 → 0.2.1

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: 103ce286b688715803e0542a5b739d4719ff0c002113368d2db8828dcbe07be5
4
- data.tar.gz: e400a9d26f3f3e291147699f3077d4fac31d5d9d9ce82712bbe3690d75607e84
3
+ metadata.gz: 4f4463471bbbffc8cdfcda8361581f051804c106523c0fa86838f5e62de59a6e
4
+ data.tar.gz: 8c31a0844247d7e210e219099170f8bda4212fca7e2e61ab323872a8d68f8ed7
5
5
  SHA512:
6
- metadata.gz: 9317560df6ac2d68e26a189994f8090386c466ba39720874911ee79f6e8869b52ed6b2d0c703d7d9b66ec291e625306ad58a85f044b558baff540798a1229d83
7
- data.tar.gz: 6d634f9653b9df6d443524ab931ed0f79768db9db33a650e020ee43f9c15e32d13b94bbace9a2f3af231fcdf24bd26d690f1474420afde4f8d9f86ffaef77614
6
+ metadata.gz: 75a640f3415c4ae1b66e1d9260f1e02c77b5d4a692ef1de609ef3fbef5afcd6e858120a74ca15ef3a8a77259cce702bf856d60c1bac3d549cad1c32f24f2343e
7
+ data.tar.gz: 9e033c6fa3481c798ff064666f419478a5ff150e05cce1fe0bd6a44a235601b7a28f54fc7e37fa95bed89ddb38a2fb97b9e4587f1375a2531084cfbd55aad9e3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2023-01-15
3
+ ## [Released]
4
+
5
+ ## [0.2.1] - 2023-05-21
6
+
7
+ Enhancements:
8
+
9
+ - Allow options to be passed when fetching configuration from the provider
10
+
11
+
12
+ ## [0.2.0] - 2023-05-20
13
+
14
+ Bug fixes:
15
+
16
+ - Fix in memory cache expiry
17
+
18
+ Enhancements:
19
+
20
+ - Reset cache for single key
21
+ - Lookup providers by configuration key
22
+
23
+
24
+ ## [0.1.0] - 2023-05-17
4
25
 
5
26
  - Initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cloud-config (0.1.0)
4
+ cloud-config (0.2.0)
5
5
  parallel
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  A library to modernise the way applications fetch configuration. Typically an application will use environment variables and settings files to store and retrieve configurations, but there are many issues with this approach. Often environment variables create a security risk and settings files hard code infrastructure dependencies into the code. Changing configuration settings will usually require redeploying large parts of the infrastructure and perhaps even need to go through the application deployment lifecycle.
4
4
 
5
- A modern approach stores configuration remotely, often using key/value databases. Storing configurations in a single database, separately from the codebase reduces infrastructure dependency. Configuration updates can automatically sync with any application, without requiring redeployments. Security risk is greatly reduced, since the configurations can be securely stored. Another goal with this approach is creating an environmentless codebase. The application no longer needs to know which environment it's running in, since all the configuration is handled by the infrastucture.
5
+ A modern approach stores configuration remotely, often using key/value databases. Storing configurations in a single database, separately from the codebase reduces infrastructure dependency. Configuration updates can automatically sync with any application, without requiring redeployments. Security risk is greatly reduced, since configurations can be securely stored. Another goal with this approach is creating an environmentless codebase. The application no longer needs to know which environment it's running in, since all the configuration is handled by the infrastucture.
6
+
7
+ Another common problem is password rotation. A typical application will need to be restarted or even redeployed when the configuration settings change. CloudConfig can handle this elegantly, improving application uptime and resilience.
6
8
 
7
9
  ## Installation
8
10
 
@@ -16,7 +18,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
18
 
17
19
  ## Usage
18
20
 
19
- CloudConfig can be configured to fetch keys from multiple providers. Since CloudConfig will need to know which provider has the correct key, all the keys will need to be preconfigured. An example configuration may look like
21
+ CloudConfig can be configured to fetch keys from multiple providers. Since CloudConfig will need to know which provider has the correct key, all the keys will need to be preconfigured. Duplicate keys will be overriden with the latest provider. An example configuration may look like
20
22
 
21
23
  ```ruby
22
24
  CloudConfig.configure do
@@ -27,6 +29,18 @@ CloudConfig.configure do
27
29
  end
28
30
  ```
29
31
 
32
+ Fetch a setting with
33
+
34
+ ```ruby
35
+ url = CloudConfig.get(:api_url)
36
+ ```
37
+
38
+ If caching has been configured, the cache can be reset using
39
+
40
+ ```ruby
41
+ url = CloudConfig.get(:api_url, reset_cache: true)
42
+ ```
43
+
30
44
  ### Configuration Options
31
45
 
32
46
  ```ruby
@@ -61,12 +75,17 @@ CloudConfig.configure do
61
75
  provider :in_memory do
62
76
  setting :key1, cache: 60 # 60 seconds
63
77
  end
78
+
79
+ # If the datastore has configurable options
80
+ provider :aws_parameter_store do
81
+ setting :key1, with_decryption: true
82
+ end
64
83
  end
65
84
  ```
66
85
 
67
86
  ### Preloading
68
87
 
69
- CloudConfig can preload all the keys configured for preload enabled providers. A cache client must be configured, otherwise
88
+ CloudConfig can preload all the keys configured for preload enabled providers. A cache client must also be configured, otherwise
70
89
  preloading won't do anything.
71
90
 
72
91
  ```ruby
@@ -74,6 +93,7 @@ CloudConfig.configure do
74
93
  cache_client CloudConfig::Cache::InMemory.new
75
94
 
76
95
  provider :in_memory, preload: true do
96
+ setting :key1
77
97
  end
78
98
  end
79
99
  ```
@@ -84,9 +104,77 @@ Call preload to cache all the keys.
84
104
  CloudConfig.preload
85
105
  ```
86
106
 
107
+ ### Custom Providers
108
+
109
+ CloudConfig comes equipped with a set of providers for fetching configuration from remote datastores. The limit set of providers will not be sufficient for most real world applications, so creating custom providers will be necessary. There is a very simple interface consisting of 3 methods necessary for creating a custom provider.
110
+
111
+ ```ruby
112
+ class CustomProvider
113
+ # Define initialize with a params argument.
114
+ def initialize(params = {}); end
115
+
116
+ # Define `get` for fetching keys from the remote datastore
117
+ def get(key, opts = {}); end
118
+
119
+ # Define `set` for storing keys in the remote datastore
120
+ def set(key, value); end
121
+ end
122
+ ```
123
+
124
+ Specify the class when configuring the custom provider.
125
+
126
+ ```ruby
127
+ CloudConfig.configure do
128
+ provider :custom_provider, provider_class: CustomProvider do
129
+ end
130
+ end
131
+ ```
132
+
133
+ ### Custom Cache
134
+
135
+ Defining a custom cache client will require an interface of 3 methods.
136
+
137
+ ```ruby
138
+ class CustomCache
139
+ # Define `key?` for checking whether the key exists in the cache client.
140
+ # This check allows nil values to be cached.
141
+ def key?(key); end
142
+
143
+ # Define `get` for fetching keys from the cache
144
+ def get(key); end
145
+
146
+ # Define `set` for storing keys in the cache
147
+ def set(key, value); end
148
+ end
149
+ ```
150
+
151
+ Configure CloudConfig to use the cache in the usual way
152
+
153
+ ```ruby
154
+ CloudConfig.configure do
155
+ cache_client CustomCache.new
156
+ end
157
+ ```
158
+
159
+ ## Connection Pooling
160
+
161
+ CloudConfig does not do any connection pooling. It is the responsibility of the application to handle connection pooling. For example, Rails handles its own connection pools, so CloudConfig will not attempt to interfere with the pool.
162
+
163
+ ## Examples
164
+
165
+ There are some example configurations in the [examples](examples) folder. In the terminal, execute
166
+
167
+ $ examples/001_parameter_store.rb
168
+
169
+ These examples make use of AWS infrastructure, so make sure the AWS enviroment variables have been correctly configured.
170
+
171
+ $ export AWS_ACCESS_KEY_ID=
172
+ $ export AWS_SECRET_ACCESS_KEY=
173
+ $ export AWS_REGION=
174
+
87
175
  ## Development
88
176
 
89
- It's recommended to use Docker when working with the library. Assuming Docker is installed, run
177
+ It's recommended to use Docker when working with this library. Assuming Docker is installed, run
90
178
 
91
179
  $ docker-compose run config /bin/bash
92
180
 
@@ -14,12 +14,13 @@ end
14
14
  require 'benchmark'
15
15
  require 'cloud-config/providers/aws_parameter_store'
16
16
 
17
+ # @note: Make sure the `parameter_store_example` key is set in the parameter store
17
18
  CloudConfig.configure do
18
19
  provider :aws_parameter_store do
19
20
  setting :parameter_store_example
20
21
  end
21
22
  end
22
23
 
23
- puts 'Fetching key parameter_store_example'
24
+ puts 'Fetching key: parameter_store_example'
24
25
  value = CloudConfig.get(:parameter_store_example)
25
- puts "Fetched value #{value}"
26
+ puts "Fetched value: #{value}"
@@ -15,6 +15,7 @@ require 'benchmark'
15
15
  require 'cloud-config/cache/in_memory'
16
16
  require 'cloud-config/providers/aws_parameter_store'
17
17
 
18
+ # @note: Make sure the `parameter_store_example` key is set in the parameter store
18
19
  CloudConfig.configure do
19
20
  cache_client CloudConfig::Cache::InMemory.new
20
21
 
@@ -25,16 +26,20 @@ end
25
26
 
26
27
  value = nil
27
28
 
28
- puts 'Fetching key parameter_store_example'
29
+ puts 'Fetching key; parameter_store_example'
29
30
  time = Benchmark.measure { value = CloudConfig.get(:parameter_store_example) }
30
- puts "Fetched value #{value} in #{time.real.round(5)} seconds"
31
+ puts "Fetched value: #{value} in #{time.real.round(5)} seconds"
32
+
33
+ puts
31
34
 
32
- puts 'Fetching key parameter_store_example (cached)'
35
+ puts 'Fetching key: parameter_store_example (cached)'
33
36
  time = Benchmark.measure { value = CloudConfig.get(:parameter_store_example) }
34
- puts "Fetched value #{value} in #{time.real.round(5)} seconds"
37
+ puts "Fetched value: #{value} in #{time.real.round(5)} seconds"
35
38
 
36
39
  sleep 5
37
40
 
41
+ puts
42
+
38
43
  puts 'Fetching key parameter_store_example'
39
44
  time = Benchmark.measure { value = CloudConfig.get(:parameter_store_example) }
40
45
  puts "Fetched value #{value} in #{time.real.round(5)} seconds"
@@ -15,6 +15,7 @@ require 'benchmark'
15
15
  require 'cloud-config/cache/in_memory'
16
16
  require 'cloud-config/providers/aws_parameter_store'
17
17
 
18
+ # @note: Make sure the `parameter_store_example` key is set in the parameter store
18
19
  CloudConfig.configure do
19
20
  cache_client CloudConfig::Cache::InMemory.new
20
21
 
@@ -27,12 +28,14 @@ CloudConfig.preload
27
28
 
28
29
  value = nil
29
30
 
30
- puts 'Fetching key parameter_store_example (cached)'
31
+ puts 'Fetching key: parameter_store_example (cached)'
31
32
  time = Benchmark.measure { value = CloudConfig.get(:parameter_store_example) }
32
- puts "Fetched value #{value} in #{time.real.round(5)} seconds"
33
+ puts "Fetched value: #{value} in #{time.real.round(5)} seconds"
33
34
 
34
35
  sleep 5
35
36
 
36
- puts 'Fetching key parameter_store_example'
37
+ puts
38
+
39
+ puts 'Fetching key: parameter_store_example'
37
40
  time = Benchmark.measure { value = CloudConfig.get(:parameter_store_example) }
38
- puts "Fetched value #{value} in #{time.real.round(5)} seconds"
41
+ puts "Fetched value: #{value} in #{time.real.round(5)} seconds"
@@ -0,0 +1,47 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+
8
+ gem 'faraday'
9
+
10
+ gem 'cloud-config', path: '..'
11
+ end
12
+
13
+ require 'faraday'
14
+ require 'cloud-config/cache/in_memory'
15
+ require 'cloud-config/providers/in_memory'
16
+
17
+ CloudConfig.configure do
18
+ cache_client CloudConfig::Cache::InMemory.new
19
+
20
+ provider :in_memory, preload: true do
21
+ setting :url, cache: 60
22
+ end
23
+ end
24
+
25
+ CloudConfig.set(:url, 'https://httpstat.us/503')
26
+
27
+ url = CloudConfig.get(:url)
28
+
29
+ # Mimick a configuration change
30
+ CloudConfig.providers_by_key[:url].provider.set(:url, 'https://httpstat.us/200')
31
+
32
+ req = Faraday.new do |f|
33
+ f.response :raise_error
34
+ f.response :logger
35
+ end
36
+
37
+ begin
38
+ req.get(url)
39
+ rescue Faraday::ServerError
40
+ url = CloudConfig.get(:url, reset_cache: true)
41
+
42
+ sleep 1
43
+
44
+ puts
45
+
46
+ retry
47
+ end
@@ -0,0 +1,40 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+
8
+ gem 'nokogiri'
9
+ gem 'aws-sdk-ssm'
10
+
11
+ gem 'cloud-config', path: '..'
12
+ end
13
+
14
+ require 'benchmark'
15
+ require 'cloud-config/providers/aws_parameter_store'
16
+
17
+ # @note: Make sure the `encrypted_key` key is set in the parameter store
18
+ CloudConfig.configure do
19
+ provider :aws_parameter_store do
20
+ setting :encrypted_key
21
+ end
22
+ end
23
+
24
+ puts 'Fetching key: encrypted_key'
25
+ value = CloudConfig.get(:encrypted_key)
26
+ puts "Fetched value: #{value}"
27
+
28
+ puts
29
+
30
+ CloudConfig.reset!
31
+
32
+ CloudConfig.configure do
33
+ provider :aws_parameter_store do
34
+ setting :encrypted_key, with_decryption: true
35
+ end
36
+ end
37
+
38
+ puts 'Fetching key: encrypted_key (decrypt)'
39
+ value = CloudConfig.get(:encrypted_key)
40
+ puts "Fetched value: #{value}"
@@ -55,7 +55,7 @@ module CloudConfig
55
55
  # @param value [Object] Value of the key
56
56
  # @option options [Hash] :expire_in Time in seconds until key expires
57
57
  def set(key, value, options = {})
58
- expire_in = options.fetch(:expire_in) { DEFAULT_EXPIRE }
58
+ expire_in = options[:expire_in] || DEFAULT_EXPIRE
59
59
 
60
60
  expire_at = Time.now + expire_in
61
61
 
@@ -30,7 +30,7 @@ module CloudConfig
30
30
  #
31
31
  # @return [Object] Value of the key
32
32
  def with_cache(key, options = {})
33
- return cache.get(key) if cache&.key?(key)
33
+ return cache.get(key) if !options[:reset_cache] && cache&.key?(key)
34
34
 
35
35
  value = yield
36
36
 
@@ -13,18 +13,31 @@ module CloudConfig
13
13
  # @return [Aws::SSM::Client]
14
14
  attr_reader :client
15
15
 
16
+ # @!attribute [r] Whether parameters need to be decrypted
17
+ # @return [Boolean]
18
+ attr_reader :with_decryption
19
+
16
20
  # Create a new instance of {AwsParameterStore}.
17
- def initialize(_params = {})
21
+ #
22
+ # @param [Hash] opts Parameter store options
23
+ # @option opts [Boolean] :with_decryption Whether all keys need to be decrypted
24
+ def initialize(opts = {})
18
25
  @client = Aws::SSM::Client.new
26
+
27
+ @with_decryption = opts.fetch(:with_decryption, false)
19
28
  end
20
29
 
21
30
  # Fetch the value of the key
22
31
  #
23
32
  # @param key [String,Symbol] Key to fetch
33
+ # @param [Hash] opts for fetching the key
34
+ # @option opts [Boolean] :with_decryption Whether the key needs decrypting
24
35
  #
25
36
  # @return [String] Value of the key
26
- def get(key)
27
- client.get_parameter(name: key).parameter.value
37
+ def get(key, opts = {})
38
+ decrypt = opts.fetch(:with_decryption) { with_decryption }
39
+
40
+ client.get_parameter(name: key, with_decryption: decrypt).parameter.value
28
41
  end
29
42
 
30
43
  # Set the value of the key
@@ -34,10 +47,6 @@ module CloudConfig
34
47
  def set(key, value)
35
48
  client.put_parameter(name: key, value:)
36
49
  end
37
-
38
- # def client
39
- # @client ||= Aws::SSM::Client.new
40
- # end
41
50
  end
42
51
  end
43
52
  end
@@ -14,7 +14,7 @@ module CloudConfig
14
14
  attr_reader :settings
15
15
 
16
16
  # Create an instance of {InMemory}
17
- def initialize(_params = {})
17
+ def initialize(_opts = {})
18
18
  @settings = {}
19
19
  end
20
20
 
@@ -23,7 +23,7 @@ module CloudConfig
23
23
  # @param key [String,Symbol] Key to fetch
24
24
  #
25
25
  # @return [Object] Value of the key
26
- def get(key)
26
+ def get(key, _opts = {})
27
27
  settings[key]
28
28
  end
29
29
 
@@ -15,9 +15,10 @@ module CloudConfig
15
15
 
16
16
  # Create an instance of {YamlFile}
17
17
  #
18
- # @option params [Hash] :filename Name of the YAML file
19
- def initialize(params = {})
20
- @contents = YAML.load_file(params[:filename]) || {}
18
+ # @param [Hash] opts Yaml file options
19
+ # @option opts [String] :filename Name of the YAML file
20
+ def initialize(opts = {})
21
+ @contents = YAML.load_file(opts[:filename]) || {}
21
22
  end
22
23
 
23
24
  # Fetch the value of the key
@@ -25,7 +26,7 @@ module CloudConfig
25
26
  # @param key [String,Symbol] Key to fetch
26
27
  #
27
28
  # @return [Object] Value of the key
28
- def get(key)
29
+ def get(key, _opts = {})
29
30
  contents[key]
30
31
  end
31
32
 
@@ -14,6 +14,10 @@ module CloudConfig
14
14
  # @return [Hash<Symbol,ProviderConfig>]
15
15
  attr_reader :providers
16
16
 
17
+ # @!attribute [r] Fetch provider configuration by setting key
18
+ # @return [Hash<Symbol,ProviderConfig>]
19
+ attr_reader :providers_by_key
20
+
17
21
  # Add a provider to the list of provider configurations.
18
22
  #
19
23
  # @param provider_name [Symbol] Name of the provider
@@ -25,6 +29,18 @@ module CloudConfig
25
29
  provider_config.instance_eval(&blk) if blk
26
30
  @providers ||= {}
27
31
  @providers[provider_name] = provider_config
32
+
33
+ update_provider_keys(provider_config)
34
+ end
35
+
36
+ # Update {#providers_by_key} with settings from the provider config.
37
+ #
38
+ # @param provider_config [ProviderConfig] Provider config containing the keys
39
+ def update_provider_keys(provider_config)
40
+ @providers_by_key ||= {}
41
+ provider_config.settings.each_key do |key|
42
+ @providers_by_key[key] = provider_config
43
+ end
28
44
  end
29
45
  end
30
46
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module CloudConfig
4
4
  # Version of CloudConfig
5
- VERSION = '0.1.1'
5
+ VERSION = '0.2.1'
6
6
  end
data/lib/cloud-config.rb CHANGED
@@ -35,14 +35,15 @@ module CloudConfig
35
35
  # Fetch the value of a key using the appropriate provider.
36
36
  #
37
37
  # @param key [String,Symbol] Key to lookup
38
+ # @param reset_cache [Boolean] Whether the cache for the key should be reset
38
39
  #
39
40
  # @return [Object] Value of the key
40
- def get(key)
41
- provider_config = providers.values.find { |provider| provider.settings.key?(key) }
41
+ def get(key, reset_cache: false)
42
+ provider_config = providers_by_key[key]
42
43
 
43
44
  raise MissingKey, 'Key not found' if provider_config.nil?
44
45
 
45
- load_key(provider_config, key)
46
+ load_key(provider_config, key, reset_cache:)
46
47
  end
47
48
 
48
49
  # Set the value of a key with the configured provider.
@@ -50,7 +51,7 @@ module CloudConfig
50
51
  # @param key [String,Symobl] Key to update
51
52
  # @param value [Object] Value of key
52
53
  def set(key, value)
53
- provider_config = providers.values.find { |provider| provider.settings.key?(key) }
54
+ provider_config = providers_by_key[key]
54
55
 
55
56
  raise MissingKey, 'Key not found' if provider_config.nil?
56
57
 
@@ -84,17 +85,21 @@ module CloudConfig
84
85
  #
85
86
  # @param provider_config [CloudConfig::ProviderConfig] provider configuration
86
87
  # @param key [String,Symbol] Key to fetch
88
+ # @param reset_cache [Boolean] Whether the cache for the key should be reset
87
89
  #
88
90
  # @return [Object] Value of the key
89
- def load_key(provider_config, key)
90
- with_cache(key, expire_in: provider_config.settings[key][:cache]) do
91
- provider_config.provider.get(key)
91
+ def load_key(provider_config, key, reset_cache: false)
92
+ with_cache(key, reset_cache:, expire_in: provider_config.settings[key][:cache]) do
93
+ provider_config.provider.get(key, provider_config.settings[key])
92
94
  end
93
95
  end
94
96
 
95
97
  # Reset the {CloudConfig} configuration
96
98
  def reset!
97
99
  @cache = nil
98
- @provider = nil
100
+ @providers = {}
101
+ @providers_by_key = {}
99
102
  end
100
103
  end
104
+
105
+ CloudConfig.reset!
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloud-config
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - hypernova2002
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-17 00:00:00.000000000 Z
11
+ date: 2023-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parallel
@@ -230,6 +230,8 @@ files:
230
230
  - examples/001_parameter_store.rb
231
231
  - examples/002_parameter_store_with_cache.rb
232
232
  - examples/003_parameter_store_with_preload.rb
233
+ - examples/004_handling_configuration_changes.rb
234
+ - examples/005_parameter_store_encrypted_key.rb
233
235
  - lib/cloud-config.rb
234
236
  - lib/cloud-config/cache.rb
235
237
  - lib/cloud-config/cache/in_memory.rb