Skip to content

Commit

Permalink
Refactor environment variable handling into new Sources::EnvSource (r…
Browse files Browse the repository at this point in the history
…ubyconfig#299)

* Extract env var handling to new EnvSource class

* Allow overriding settings for parsing env sources

By default, EnvSource will use "global" settings specified like
`Config.env_prefix`, `Config.env_separator`, `Config.env_separator`, and
`Config.env_parse_values`. Those configurations will be used when
parsing the ENV hash.

But when using EnvSource to load settings from some unrelated flat
string-keyed Hash source, we want to allow folks to override the settings.

* Update CHANGELOG

* Add AWS Secrets Manager usage to README
  • Loading branch information
cjlarose authored and ippachi committed Oct 3, 2021
1 parent 9b21992 commit 7ca6e20
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 54 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
### BREAKING CHANGES

After upgrade behaviour of `to_h` would change and match behaviour of `to_hash`. Check [#217](https://github.com/rubyconfig/config/issues/217#issuecomment-741953382) for more details.
`Config::Options#load_env!` and `Config::Options#reload_env!` have been removed. If you need to reload settings after modifying the `ENV` hash, use `Config.reload!` or `Config::Options#reload!` instead.

### Bug fixes

* Added alias `to_h` for `to_hash` ([#277](https://github.com/railsconfig/config/issues/277))

### Changes

* Add `Config::Sources::EnvSource` for loading settings from flat `Hash`es with `String` keys and `String` values ([#299](https://github.com/railsconfig/config/pull/299))

## 2.2.3

### Bug fixes
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,50 @@ Settings.section.server # => 'google.com'
Settings.section.ssl_enabled # => false
```

### Working with AWS Secrets Manager

It is possible to parse variables stored in an AWS Secrets Manager Secret as if they were environment variables by using `Config::Sources::EnvSource`.

For example, the plaintext secret might look like this:

```json
{
"Settings.foo": "hello",
"Settings.bar": "world",
}
```

In order to load those settings, fetch the settings from AWS Secrets Manager, parse the plaintext as JSON, pass the resulting `Hash` into a new `EnvSource`, load the new source, and reload.

```ruby
# fetch secrets from AWS
client = Aws::SecretsManager::Client.new
response = client.get_secret_value(secret_id: "#{ENV['ENVIRONMENT']}/my_application")
secrets = JSON.parse(response.secret_string)
# load secrets into config
secret_source = Config::Sources::EnvSource.new(secrets)
Settings.add_source!(secret_source)
Settings.reload!
```

In this case, the following settings will be available:

```ruby
Settings.foo # => "hello"
Settings.bar # => "world"
```

By default, `EnvSource` will use configuration for `env_prefix`, `env_separator`, `env_converter`, and `env_parse_values`, but any of these can be overridden in the constructor.

```ruby
secret_source = Config::Sources::EnvSource.new(secrets,
prefix: 'MyConfig',
separator: '__',
converter: nil,
parse_values: false)
```

## Contributing

You are very warmly welcome to help. Please follow our [contribution guidelines](CONTRIBUTING.md)
Expand Down
3 changes: 3 additions & 0 deletions lib/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'config/version'
require 'config/sources/yaml_source'
require 'config/sources/hash_source'
require 'config/sources/env_source'
require 'config/validation/schema'
require 'deep_merge'

Expand Down Expand Up @@ -41,6 +42,8 @@ def self.load_files(*files)
config.add_source!(file.to_s)
end

config.add_source!(Sources::EnvSource.new(ENV)) if Config.use_env

config.load!
config
end
Expand Down
54 changes: 0 additions & 54 deletions lib/config/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,47 +31,6 @@ def prepend_source!(source)
@config_sources.unshift(source)
end

def reload_env!
return self if ENV.nil? || ENV.empty?

hash = Hash.new

ENV.each do |variable, value|
separator = Config.env_separator
prefix = (Config.env_prefix || Config.const_name).to_s.split(separator)

keys = variable.to_s.split(separator)

next if keys.shift(prefix.size) != prefix

keys.map! { |key|
case Config.env_converter
when :downcase then
key.downcase.to_sym
when nil then
key.to_sym
else
raise "Invalid ENV variables name converter: #{Config.env_converter}"
end
}

leaf = keys[0...-1].inject(hash) { |h, key|
h[key] ||= {}
}

unless leaf.is_a?(Hash)
conflicting_key = (prefix + keys[0...-1]).join(separator)
raise "Environment variable #{variable} conflicts with variable #{conflicting_key}"
end

leaf[keys.last] = Config.env_parse_values ? __value(value) : value
end

merge!(hash)
end

alias :load_env! :reload_env!

# look through all our sources and rebuild the configuration
def reload!
conf = {}
Expand All @@ -96,7 +55,6 @@ def reload!
# swap out the contents of the OStruct with a hash (need to recursively convert)
marshal_load(__convert(conf).marshal_dump)

reload_env! if Config.use_env
validate!

self
Expand Down Expand Up @@ -223,17 +181,5 @@ def __convert(h) #:nodoc:
end
s
end

# Try to convert string to a correct type
def __value(v)
case v
when 'false'
false
when 'true'
true
else
Integer(v) rescue Float(v) rescue v
end
end
end
end
73 changes: 73 additions & 0 deletions lib/config/sources/env_source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
module Config
module Sources
# Allows settings to be loaded from a "flat" hash with string keys, like ENV.
class EnvSource
attr_reader :prefix
attr_reader :separator
attr_reader :converter
attr_reader :parse_values

def initialize(env,
prefix: Config.env_prefix || Config.const_name,
separator: Config.env_separator,
converter: Config.env_converter,
parse_values: Config.env_parse_values)
@env = env
@prefix = prefix.to_s.split(separator)
@separator = separator
@converter = converter
@parse_values = parse_values
end

def load
return {} if @env.nil? || @env.empty?

hash = Hash.new

@env.each do |variable, value|
keys = variable.to_s.split(separator)

next if keys.shift(prefix.size) != prefix

keys.map! { |key|
case converter
when :downcase then
key.downcase
when nil then
key
else
raise "Invalid ENV variables name converter: #{converter}"
end
}

leaf = keys[0...-1].inject(hash) { |h, key|
h[key] ||= {}
}

unless leaf.is_a?(Hash)
conflicting_key = (prefix + keys[0...-1]).join(separator)
raise "Environment variable #{variable} conflicts with variable #{conflicting_key}"
end

leaf[keys.last] = parse_values ? __value(value) : value
end

hash
end

private

# Try to convert string to a correct type
def __value(v)
case v
when 'false'
false
when 'true'
true
else
Integer(v) rescue Float(v) rescue v
end
end
end
end
end
79 changes: 79 additions & 0 deletions spec/sources/env_source_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require 'spec_helper'

module Config::Sources
describe EnvSource do
context 'configuration options' do
before :each do
Config.reset
Config.env_prefix = nil
Config.env_separator = '.'
Config.env_converter = :downcase
Config.env_parse_values = true
end

context 'default configuration' do
it 'should use global prefix configuration by default' do
Config.env_prefix = 'MY_CONFIG'

source = EnvSource.new({ 'MY_CONFIG.ACTION_MAILER' => 'enabled' })
results = source.load
expect(results['action_mailer']).to eq('enabled')
end

it 'should use global separator configuration by default' do
Config.env_separator = '__'

source = EnvSource.new({ 'Settings__ACTION_MAILER__ENABLED' => 'yes' })
results = source.load
expect(results['action_mailer']['enabled']).to eq('yes')
end

it 'should use global converter configuration by default' do
Config.env_converter = nil

source = EnvSource.new({ 'Settings.ActionMailer.Enabled' => 'yes' })
results = source.load
expect(results['ActionMailer']['Enabled']).to eq('yes')
end

it 'should use global parse_values configuration by default' do
Config.env_parse_values = false

source = EnvSource.new({ 'Settings.ACTION_MAILER.ENABLED' => 'true' })
results = source.load
expect(results['action_mailer']['enabled']).to eq('true')
end
end

context 'configuration overrides' do
it 'should allow overriding prefix configuration' do
source = EnvSource.new({ 'MY_CONFIG.ACTION_MAILER' => 'enabled' },
prefix: 'MY_CONFIG')
results = source.load
expect(results['action_mailer']).to eq('enabled')
end

it 'should allow overriding separator configuration' do
source = EnvSource.new({ 'Settings__ACTION_MAILER__ENABLED' => 'yes' },
separator: '__')
results = source.load
expect(results['action_mailer']['enabled']).to eq('yes')
end

it 'should allow overriding converter configuration' do
source = EnvSource.new({ 'Settings.ActionMailer.Enabled' => 'yes' },
converter: nil)
results = source.load
expect(results['ActionMailer']['Enabled']).to eq('yes')
end

it 'should allow overriding parse_values configuration' do
source = EnvSource.new({ 'Settings.ACTION_MAILER.ENABLED' => 'true' },
parse_values: false)
results = source.load
expect(results['action_mailer']['enabled']).to eq('true')
end
end
end
end
end

0 comments on commit 7ca6e20

Please sign in to comment.