ravioli 0.1.0 → 0.1.5
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 +4 -4
- data/README.md +45 -46
- data/lib/ravioli.rb +23 -15
- data/lib/ravioli/builder.rb +163 -42
- data/lib/ravioli/configuration.rb +10 -7
- data/lib/ravioli/engine.rb +9 -4
- data/lib/ravioli/staging_inquirer.rb +26 -0
- data/lib/ravioli/version.rb +1 -1
- metadata +68 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7531708baaf2764477fc99c00343d6ccb10873695ccd6d20c56cb38a21f6520d
|
4
|
+
data.tar.gz: 13b46317a8d6b9fa5f167cab39c3cef998889951fb99b44d0177612798c087e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79ba9026307074c13efb196b3fd3213ac76668def0064bb4c13a1abf0244c74be128bd259ec472aa09ef36b0bd26f4f5097f097206e42def11b59e04e9f01d25
|
7
|
+
data.tar.gz: ea102e1c1ad70d0fe6b6cfa9460804cf726a8bde10c404dc7eb03e598f6d6e03f7c9d4aca848c5f1f8b9642f906155af7feed4cde336d0328b4f137d1839bc4e
|
data/README.md
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# Ravioli.rb 🍝
|
2
2
|
|
3
|
-
|
4
3
|
**Grab a fork and twist your configuration spaghetti in a single, delicious dumpling!**
|
5
4
|
|
6
5
|
Ravioli combines all of your app's runtime configuration into a unified, simple interface. **It combines YAML or JSON configuration files, encrypted Rails credentials, and ENV vars into one easy-to-consume interface** so you can focus on writing code and not on where configuration comes from.
|
@@ -35,7 +34,7 @@ key = Rails.config.dig!(:thing, :api_key)
|
|
35
34
|
-->
|
36
35
|
1. Add `gem "ravioli"` to your `Gemfile`
|
37
36
|
2. Run `bundle install`
|
38
|
-
3. Add an initializer (totally optional): `rails generate ravioli:install` - Ravioli will do **everything** automatically for you if you skip this step, because I
|
37
|
+
3. Add an initializer (totally optional): `rails generate ravioli:install` - Ravioli will do **everything** automatically for you if you skip this step, because I'm here to put a little meat on your bones.
|
39
38
|
|
40
39
|
## Usage
|
41
40
|
|
@@ -207,7 +206,7 @@ mailjet:
|
|
207
206
|
# ...the contents of mailjet.json
|
208
207
|
```
|
209
208
|
|
210
|
-
**NOTE THAT APP.YML GOT LOADED INTO THE ROOT OF THE CONFIGURATION!** This is because the automatic loading system assumes you want some configuration values that aren't nested. It effectively calls [`
|
209
|
+
**NOTE THAT APP.YML GOT LOADED INTO THE ROOT OF THE CONFIGURATION!** This is because the automatic loading system assumes you want some configuration values that aren't nested. It effectively calls [`load_file(filename, key: File.basename(filename) != "app")`](#load_file), which ensures that, for example, the values in `config/mailjet.json` get loaded under `Rails.config.mailjet` while the valuaes in `config/app.yml` get loaded directly into `Rails.config`.
|
211
210
|
|
212
211
|
### 3. Loads and combines encrypted credentials
|
213
212
|
|
@@ -217,7 +216,7 @@ Ravioli will then check for [encrypted credentials](https://guides.rubyonrails.o
|
|
217
216
|
2. Then, it loads and applies `config/credentials/RAILS_ENV.yml.enc` over top of what it has already loaded
|
218
217
|
3. Finally, IF `Rails.config.staging?` IS TRUE, it loads and applies `config/credentials/staging.yml.enc`
|
219
218
|
|
220
|
-
This allows you to use your secure credentials stores without duplicating information; you can simply layer environment-specific values over top of
|
219
|
+
This allows you to use your secure credentials stores without duplicating information; you can simply layer environment-specific values over top of a "root" `config/credentials.yml.enc` file.
|
221
220
|
|
222
221
|
### All put together, it does this:
|
223
222
|
|
@@ -225,7 +224,7 @@ This allows you to use your secure credentials stores without duplicating inform
|
|
225
224
|
def Rails.config
|
226
225
|
@config ||= Ravioli.build(strict: Rails.env.production?) do |config|
|
227
226
|
config.add_staging_flag!
|
228
|
-
config.
|
227
|
+
config.auto_load_files!
|
229
228
|
config.auto_load_credentials!
|
230
229
|
end
|
231
230
|
end
|
@@ -243,16 +242,16 @@ The best way to build your own configuration is by calling `Ravioli.build`. It w
|
|
243
242
|
|
244
243
|
```ruby
|
245
244
|
configuration = Ravioli.build do |config|
|
246
|
-
config.
|
245
|
+
config.load_file("things.yml")
|
247
246
|
config.whatever = {things: true}
|
248
247
|
end
|
249
248
|
```
|
250
249
|
|
251
|
-
This will
|
250
|
+
This will return a configured instance of `Ravioli::Configuration` with structure
|
252
251
|
|
253
252
|
```yaml
|
254
|
-
|
255
|
-
# ...the contents of
|
253
|
+
things:
|
254
|
+
# ...the contents of things.yml
|
256
255
|
whatever:
|
257
256
|
things: true
|
258
257
|
```
|
@@ -303,7 +302,7 @@ end
|
|
303
302
|
### `add_staging_flag!`
|
304
303
|
|
305
304
|
|
306
|
-
### `
|
305
|
+
### `load_file`
|
307
306
|
|
308
307
|
Let's imagine we have this config file:
|
309
308
|
|
@@ -329,24 +328,24 @@ In an initializer, generate your Ravioli instance and load it up:
|
|
329
328
|
```ruby
|
330
329
|
# config/initializers/_ravioli.rb`
|
331
330
|
Config = Ravioli.build do
|
332
|
-
|
333
|
-
|
334
|
-
|
331
|
+
load_file(:mailjet) # given a symbol, it automatically assumes you meant `config/mailjet.yml`
|
332
|
+
load_file("config/mailjet") # same as above
|
333
|
+
load_file("lib/mailjet/config") # looks for `Rails.root.join("lib", "mailjet", "config.yml")
|
335
334
|
end
|
336
335
|
```
|
337
336
|
|
338
337
|
`config/initializers/_ravioli.rb`
|
339
338
|
|
340
339
|
```ruby
|
341
|
-
Config = Ravioli.build do
|
340
|
+
Config = Ravioli.build do |config|
|
342
341
|
%i[new_relic sentry google].each do |service|
|
343
|
-
|
342
|
+
config.load_file(service)
|
344
343
|
end
|
345
344
|
|
346
|
-
load_credentials # just load the base credentials file
|
347
|
-
load_credentials("credentials/production") if Rails.env.production? # add production overrides when appropriate
|
345
|
+
config.load_credentials # just load the base credentials file
|
346
|
+
config.load_credentials("credentials/production") if Rails.env.production? # add production overrides when appropriate
|
348
347
|
|
349
|
-
|
348
|
+
config.staging = File.exists?("./staging.txt") # technically you could do this ... I don't know why you would, but technically you could
|
350
349
|
end
|
351
350
|
```
|
352
351
|
|
@@ -418,47 +417,47 @@ staging:
|
|
418
417
|
|
419
418
|
### Encryption keys in ENV
|
420
419
|
|
421
|
-
|
422
|
-
|
423
|
-
<table><thead><tr><th>File</th><th>First it tries...</th><th>Then it tries...</th></tr></thead><tbody><tr><td>
|
424
|
-
|
425
|
-
`config/credentials.yml.enc`
|
426
|
-
|
427
|
-
</td><td>
|
428
|
-
|
429
|
-
`ENV["RAILS_BASE_KEY"]`
|
420
|
+
Here are a few facts about credentials in Rails and how they're deployed:
|
430
421
|
|
431
|
-
|
422
|
+
1. Rails assumes you want to use the file that matches your environment, if it exists (e.g. `RAILS_ENV=production` will look for `config/credentials/production.yml.enc`)
|
423
|
+
2. Rails does _not_ support environment-specfic keys, but it _does_ now aggressively loads credentials at boot time.
|
432
424
|
|
433
|
-
`
|
425
|
+
**This means `RAILS_MASTER_KEY` MUST be the decryption key for your environment-specific credential file, if one exists.**
|
434
426
|
|
435
|
-
|
427
|
+
But, because Ravioli merges environment-specific credentials over top of the root credentials file, you'll need to provide encryption keys for two (or, if you have a staging setup, three) different files in ENV vars. As such, Ravioli looks for decryption keys in a way that mirrors Rails' assumptions, but allows progressive layering of credentials.
|
436
428
|
|
437
|
-
|
429
|
+
Here are a few examples
|
438
430
|
|
439
|
-
</
|
431
|
+
<table><thead><tr><th>File</th><th>First it tries...</th><th>Then it tries...</th></tr></thead><tbody>
|
440
432
|
|
441
|
-
|
433
|
+
<tr>
|
434
|
+
<td><code>config/credentials.yml.enc</code></td>
|
435
|
+
<td><code>ENV["RAILS_MASTER_KEY"]</code></td>
|
436
|
+
<td><code>ENV["RAILS_ROOT_KEY"]</code></td>
|
437
|
+
</tr>
|
442
438
|
|
443
|
-
|
439
|
+
<tr>
|
440
|
+
<td><code>config/credentials/#{RAILS_ENV}.yml.enc</code></td>
|
441
|
+
<td><code>ENV["RAILS_MASTER_KEY"]</code></td>
|
442
|
+
<td><code>ENV["RAILS_#{RAILS_ENV}_KEY"]</code></td>
|
443
|
+
</tr>
|
444
444
|
|
445
|
-
`ENV["RAILS_MASTER_KEY"]`
|
446
445
|
|
447
|
-
|
446
|
+
<tr>
|
447
|
+
<td><code>config/credentials/staging.yml.enc</code></td>
|
448
|
+
<td><code>ENV["RAILS_MASTER_KEY"]</code></td>
|
449
|
+
<td><code>ENV["RAILS_STAGING_KEY"]</code></td>
|
450
|
+
</tr>
|
448
451
|
|
449
|
-
|
452
|
+
</tbody></table>
|
450
453
|
|
451
|
-
|
452
|
-
|
453
|
-
`ENV["RAILS_STAGING_KEY"]`
|
454
|
-
|
455
|
-
</td><td>
|
456
|
-
|
457
|
-
`ENV["RAILS_MASTER_KEY"]`
|
454
|
+
Credentials are loaded in that order, too, so that you can have a base setup on `config/credentials.yml.enc`, overlay that with production-specific stuff from `config/credentials/production.yml.enc`, and then short-circuit or redirect some stuff in `config/credentials/staging.yml.enc` for staging environments.
|
458
455
|
|
459
|
-
|
456
|
+
#### TLDR:
|
460
457
|
|
461
|
-
|
458
|
+
1. Set `RAILS_MASTER_KEY` to the key for your specific environment
|
459
|
+
2. Set `RAILS_STAGING_KEY` to the key for your staging credentials (if deploying to staging AND you have staging-specific credentials)
|
460
|
+
3. Set `RAILS_ROOT_KEY` to the key for your root credentials (if you have anything in `config/credentials.yml.enc`)
|
462
461
|
|
463
462
|
## License
|
464
463
|
|
data/lib/ravioli.rb
CHANGED
@@ -7,28 +7,36 @@ require_relative "ravioli/builder"
|
|
7
7
|
require_relative "ravioli/configuration"
|
8
8
|
require_relative "ravioli/version"
|
9
9
|
|
10
|
-
|
11
|
-
#
|
12
|
-
# as the Builder class for help loading configuration files and encrypted credentials
|
10
|
+
# The root namespace for all of Ravioli, and owner of two handly
|
11
|
+
# configuration-related class methods
|
13
12
|
module Ravioli
|
14
|
-
NAME = "Ravioli"
|
15
|
-
|
16
13
|
class << self
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
14
|
+
# Forwards arguments to a {Ravioli::Builder}. See
|
15
|
+
# {Ravioli::Builder#new} for complete documentation.
|
16
|
+
#
|
17
|
+
# @param namespace [String, Module, Class] the name of, or a direct reference to, the module or class your Configuration class should namespace itself within
|
18
|
+
# @param class_name [String] the name of the namespace's Configuration class
|
19
|
+
# @param strict [boolean] whether or not the Builder instance should throw errors when there are errors loading configuration files or encrypted credentials
|
20
|
+
def build(namespace: nil, class_name: "Configuration", strict: false, &block)
|
21
|
+
builder = Builder.new(
|
22
|
+
class_name: class_name,
|
23
|
+
hijack: true,
|
24
|
+
namespace: namespace,
|
25
|
+
strict: strict,
|
26
|
+
)
|
27
|
+
yield builder if block
|
28
|
+
builder.build!
|
27
29
|
end
|
28
30
|
|
31
|
+
# Returns a list of all of the configuration instances
|
29
32
|
def configurations
|
30
33
|
@configurations ||= []
|
31
34
|
end
|
35
|
+
|
36
|
+
# Returns the most-recently configured Ravioli instance that has been built with {Ravioli::build}.
|
37
|
+
def default
|
38
|
+
configurations.last
|
39
|
+
end
|
32
40
|
end
|
33
41
|
end
|
34
42
|
|
data/lib/ravioli/builder.rb
CHANGED
@@ -1,18 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_support/all"
|
4
|
+
require "erb"
|
4
5
|
require_relative "configuration"
|
5
6
|
|
6
7
|
module Ravioli
|
7
|
-
# The Builder
|
8
|
+
# The Builder class provides a simple interface for building a Ravioli configuration. It has
|
8
9
|
# methods for loading configuration files and encrypted credentials, and forwards direct
|
9
10
|
# configuration on to the configuration instance. This allows us to keep a clean separation of
|
10
11
|
# concerns (builder: loads configuration details; configuration: provides access to information
|
11
12
|
# in memory).
|
13
|
+
#
|
14
|
+
# == ENV variables and encrypted credentials keys
|
15
|
+
#
|
16
|
+
# <table><thead><tr><th>File</th><th>First it tries...</th><th>Then it tries...</th></tr></thead><tbody><tr><td>
|
17
|
+
#
|
18
|
+
# `config/credentials.yml.enc`
|
19
|
+
#
|
20
|
+
# </td><td>
|
21
|
+
#
|
22
|
+
# `ENV["RAILS_BASE_KEY"]`
|
23
|
+
#
|
24
|
+
# </td><td>
|
25
|
+
#
|
26
|
+
# `ENV["RAILS_MASTER_KEY"]`
|
27
|
+
#
|
28
|
+
# </td></tr><tr><td>
|
29
|
+
#
|
30
|
+
# `config/credentials/production.yml.enc`
|
31
|
+
|
32
|
+
# </td><td>
|
33
|
+
#
|
34
|
+
# `ENV["RAILS_PRODUCTION_KEY"]`
|
35
|
+
#
|
36
|
+
# </td><td>
|
37
|
+
#
|
38
|
+
# `ENV["RAILS_MASTER_KEY"]`
|
39
|
+
#
|
40
|
+
# </td></tr><tr><td>
|
41
|
+
#
|
42
|
+
# `config/credentials/staging.yml.enc` (only if running on staging)
|
43
|
+
#
|
44
|
+
# </td><td>
|
45
|
+
#
|
46
|
+
# `ENV["RAILS_STAGING_KEY"]`
|
47
|
+
#
|
48
|
+
# </td><td>
|
49
|
+
#
|
50
|
+
# `ENV["RAILS_MASTER_KEY"]`
|
51
|
+
#
|
52
|
+
# </td></tr></tbody></table>
|
12
53
|
class Builder
|
13
|
-
def initialize(class_name: "Configuration", namespace: nil, strict: false)
|
54
|
+
def initialize(class_name: "Configuration", hijack: false, namespace: nil, strict: false)
|
14
55
|
configuration_class = if namespace.present?
|
15
56
|
namespace.class_eval <<-EOC, __FILE__, __LINE__ + 1
|
57
|
+
# class Configuration < Ravioli::Configuration; end
|
16
58
|
class #{class_name.to_s.classify} < Ravioli::Configuration; end
|
17
59
|
EOC
|
18
60
|
namespace.const_get(class_name)
|
@@ -21,49 +63,81 @@ module Ravioli
|
|
21
63
|
end
|
22
64
|
@strict = !!strict
|
23
65
|
@configuration = configuration_class.new
|
66
|
+
@reload_credentials = Set.new
|
67
|
+
@reload_paths = Set.new
|
68
|
+
@hijack = !!hijack
|
69
|
+
|
70
|
+
if @hijack
|
71
|
+
# Put this builder on the configurations stack - it will intercept setters on the underyling
|
72
|
+
# configuration object as it loads files, and mark those files as needing a reload once
|
73
|
+
# loading is complete
|
74
|
+
Ravioli.configurations.push(self)
|
75
|
+
end
|
24
76
|
end
|
25
77
|
|
26
|
-
# Automatically infer a `staging
|
78
|
+
# Automatically infer a `staging` status from the current environment
|
79
|
+
#
|
80
|
+
# @param is_staging [boolean, #present?] whether or not the current environment is considered a staging environment
|
27
81
|
def add_staging_flag!(is_staging = Rails.env.production? && ENV["STAGING"].present?)
|
82
|
+
is_staging = is_staging.present?
|
28
83
|
configuration.staging = is_staging
|
29
|
-
Rails.env.class_eval <<-EOC, __FILE__, __LINE__ + 1
|
30
|
-
def staging?
|
31
|
-
config = Rails.try(:config)
|
32
|
-
return false unless config&.is_a?(Ravioli::Configuration)
|
33
|
-
|
34
|
-
config.staging?
|
35
|
-
end
|
36
|
-
EOC
|
37
|
-
is_staging
|
38
84
|
end
|
39
85
|
|
40
|
-
#
|
41
|
-
|
86
|
+
# Iterates through the config directory (including nested folders) and
|
87
|
+
# calls {Ravioli::Builder::load_file} on each JSON or YAML file it
|
88
|
+
# finds. Ignores `config/locales`.
|
89
|
+
def auto_load_files!
|
42
90
|
config_dir = Rails.root.join("config")
|
43
91
|
Dir[config_dir.join("{[!locales/]**/*,*}.{json,yaml,yml}")].each do |config_file|
|
44
|
-
|
92
|
+
auto_load_file(config_file)
|
45
93
|
end
|
46
94
|
end
|
47
95
|
|
48
|
-
#
|
96
|
+
# Loads Rails encrypted credentials that it can. Checks for corresponding private key files, or ENV vars based on the {Ravioli::Builder credentials preadmlogic}
|
49
97
|
def auto_load_credentials!
|
50
|
-
# Load the
|
51
|
-
load_credentials(
|
52
|
-
|
53
|
-
|
54
|
-
|
98
|
+
# Load the root config (supports using the master key or `RAILS_ROOT_KEY`)
|
99
|
+
load_credentials(
|
100
|
+
key_path: "config/master.key",
|
101
|
+
env_names: %w[master root],
|
102
|
+
)
|
103
|
+
|
104
|
+
# Load any environment-specific configuration on top of it. Since Rails will try
|
105
|
+
# `RAILS_MASTER_KEY` from the environment, we assume the same
|
106
|
+
load_credentials(
|
107
|
+
"config/credentials/#{Rails.env}",
|
108
|
+
key_path: "config/credentials/#{Rails.env}.key",
|
109
|
+
env_names: ["master"],
|
110
|
+
)
|
55
111
|
|
56
112
|
# Apply staging configuration on top of THAT, if need be
|
57
|
-
|
113
|
+
if configuration.staging?
|
114
|
+
load_credentials(
|
115
|
+
"config/credentials/staging",
|
116
|
+
env_names: %w[staging master],
|
117
|
+
key_path: "config/credentials/staging.key",
|
118
|
+
)
|
119
|
+
end
|
58
120
|
end
|
59
121
|
|
60
122
|
# When the builder is done working, lock the configuration and return it
|
61
123
|
def build!
|
124
|
+
if @hijack
|
125
|
+
# Replace this builder with the underlying configuration on the configurations stack...
|
126
|
+
Ravioli.configurations.delete(self)
|
127
|
+
Ravioli.configurations.push(configuration)
|
128
|
+
|
129
|
+
# ...and then reload any config file that referenced the configuration the first time it was
|
130
|
+
# loaded!
|
131
|
+
@reload_paths.each do |path|
|
132
|
+
auto_load_file(path)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
62
136
|
configuration.freeze
|
63
137
|
end
|
64
138
|
|
65
|
-
# Load a
|
66
|
-
def
|
139
|
+
# Load a file either with a given path or by name (e.g. `config/whatever.yml` or `:whatever`)
|
140
|
+
def load_file(path, options = {})
|
67
141
|
config = parse_config_file(path, options)
|
68
142
|
configuration.append(config) if config.present?
|
69
143
|
rescue => error
|
@@ -71,11 +145,30 @@ module Ravioli
|
|
71
145
|
end
|
72
146
|
|
73
147
|
# Load secure credentials using a key either from a file or the ENV
|
74
|
-
def load_credentials(path = "credentials", key_path: path,
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
148
|
+
def load_credentials(path = "credentials", key_path: path, env_names: path.split("/").last)
|
149
|
+
error = nil
|
150
|
+
env_names = Array(env_names).map { |env_name| parse_env_name(env_name) }
|
151
|
+
env_names.each do |env_name|
|
152
|
+
credentials = parse_credentials(path, env_name: env_name, key_path: key_path)
|
153
|
+
if credentials.present?
|
154
|
+
configuration.append(credentials)
|
155
|
+
return credentials
|
156
|
+
end
|
157
|
+
rescue => e
|
158
|
+
error = e
|
159
|
+
end
|
160
|
+
|
161
|
+
if error
|
162
|
+
attempted_names = ["key file `#{key_path}'"]
|
163
|
+
attempted_names.push(*env_names.map { |env_name| "`ENV[\"#{env_name}\"]'" })
|
164
|
+
attempted_names = attempted_names.to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
|
165
|
+
warn(
|
166
|
+
"Could not decrypt `#{path}.yml.enc' with #{attempted_names}",
|
167
|
+
error,
|
168
|
+
critical: false,
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
79
172
|
{}
|
80
173
|
end
|
81
174
|
|
@@ -86,6 +179,13 @@ module Ravioli
|
|
86
179
|
|
87
180
|
attr_reader :configuration
|
88
181
|
|
182
|
+
def auto_load_file(config_file)
|
183
|
+
basename = File.basename(config_file, File.extname(config_file))
|
184
|
+
dirname = File.dirname(config_file)
|
185
|
+
key = %w[app application].exclude?(basename) && dirname != config_dir
|
186
|
+
load_file(config_file, key: key)
|
187
|
+
end
|
188
|
+
|
89
189
|
def extract_environmental_config(config)
|
90
190
|
# Check if the config hash is keyed by environment - if not, just return it as-is. It's
|
91
191
|
# considered "keyed by environment" if it contains ONLY env-specific keys.
|
@@ -105,12 +205,23 @@ module Ravioli
|
|
105
205
|
# rubocop:disable Style/MethodMissingSuper
|
106
206
|
# rubocop:disable Style/MissingRespondToMissing
|
107
207
|
def method_missing(*args, &block)
|
208
|
+
if @current_path
|
209
|
+
@reload_paths.add(@current_path)
|
210
|
+
end
|
211
|
+
|
212
|
+
if @current_credentials
|
213
|
+
@reload_credentials.add(@current_credentials)
|
214
|
+
end
|
215
|
+
|
108
216
|
configuration.send(*args, &block)
|
109
217
|
end
|
110
218
|
# rubocop:enable Style/MissingRespondToMissing
|
111
219
|
# rubocop:enable Style/MethodMissingSuper
|
112
220
|
|
113
221
|
def parse_config_file(path, options = {})
|
222
|
+
# Stash a reference to the file we're parsing, so we can reload it later if it tries to use
|
223
|
+
# the configuration object
|
224
|
+
@current_path = path
|
114
225
|
path = path_to_config_file_path(path)
|
115
226
|
|
116
227
|
config = case path.extname.downcase
|
@@ -119,9 +230,11 @@ module Ravioli
|
|
119
230
|
when ".yml", ".yaml"
|
120
231
|
parse_yaml_config_file(path)
|
121
232
|
else
|
122
|
-
raise ParseError.new("
|
233
|
+
raise ParseError.new("Ravioli doesn't know how to parse #{path}")
|
123
234
|
end
|
124
235
|
|
236
|
+
# We are no longer loading anything
|
237
|
+
@current_path = nil
|
125
238
|
# At least expect a hash to be returned from the loaded config file
|
126
239
|
return {} unless config.is_a?(Hash)
|
127
240
|
|
@@ -144,16 +257,22 @@ module Ravioli
|
|
144
257
|
{name => config}
|
145
258
|
end
|
146
259
|
|
147
|
-
def
|
260
|
+
def parse_env_name(env_name)
|
148
261
|
env_name = env_name.to_s
|
149
|
-
env_name
|
262
|
+
env_name.match?(/^RAILS_/) ? env_name : "RAILS_#{env_name.upcase}_KEY"
|
263
|
+
end
|
264
|
+
|
265
|
+
def parse_credentials(path, key_path: path, env_name: path.split("/").last)
|
266
|
+
@current_credentials = path
|
267
|
+
env_name = parse_env_name(env_name)
|
150
268
|
key_path = path_to_config_file_path(key_path, extnames: "key", quiet: true)
|
151
|
-
options = {key_path: key_path}
|
152
|
-
options[:env_key] = ENV[env_name].present? ? env_name : SecureRandom.hex(6)
|
269
|
+
options = {key_path: key_path.to_s}
|
270
|
+
options[:env_key] = ENV[env_name].present? ? env_name : "__RAVIOLI__#{SecureRandom.hex(6)}"
|
153
271
|
|
154
272
|
path = path_to_config_file_path(path, extnames: "yml.enc")
|
155
|
-
|
156
|
-
|
273
|
+
(Rails.application.encrypted(path, **options)&.config || {}).tap do
|
274
|
+
@current_credentials = nil
|
275
|
+
end
|
157
276
|
end
|
158
277
|
|
159
278
|
def parse_json_config_file(path)
|
@@ -162,10 +281,9 @@ module Ravioli
|
|
162
281
|
end
|
163
282
|
|
164
283
|
def parse_yaml_config_file(path)
|
165
|
-
require "erb"
|
166
284
|
contents = File.read(path)
|
167
285
|
erb = ERB.new(contents).tap { |renderer| renderer.filename = path.to_s }
|
168
|
-
YAML.safe_load(erb.result, aliases: true)
|
286
|
+
YAML.safe_load(erb.result, [Symbol], aliases: true)
|
169
287
|
end
|
170
288
|
|
171
289
|
def path_to_config_file_path(path, extnames: EXTNAMES, quiet: false)
|
@@ -192,18 +310,19 @@ module Ravioli
|
|
192
310
|
path
|
193
311
|
end
|
194
312
|
|
195
|
-
def warn(message, error =
|
196
|
-
message = "[
|
313
|
+
def warn(message, error = $!, critical: true)
|
314
|
+
message = "[Ravioli] #{message}"
|
197
315
|
message = "#{message}:\n\n#{error.cause.inspect}" if error&.cause.present?
|
198
|
-
if @strict
|
316
|
+
if @strict && critical
|
199
317
|
raise BuildError.new(message, error)
|
200
318
|
else
|
201
|
-
Rails.logger.warn
|
319
|
+
Rails.logger.try(:warn, message) if defined? Rails
|
202
320
|
$stderr.write message # rubocop:disable Rails/Output
|
203
321
|
end
|
204
322
|
end
|
205
323
|
end
|
206
324
|
|
325
|
+
# Error raised when Ravioli is in strict mode. Includes the original error for context.
|
207
326
|
class BuildError < StandardError
|
208
327
|
def initialize(message, cause = nil)
|
209
328
|
super message
|
@@ -214,5 +333,7 @@ module Ravioli
|
|
214
333
|
@cause || super
|
215
334
|
end
|
216
335
|
end
|
336
|
+
|
337
|
+
# Error raised when Ravioli encounters a problem parsing a file
|
217
338
|
class ParseError < StandardError; end
|
218
339
|
end
|
@@ -5,20 +5,17 @@ require "ostruct"
|
|
5
5
|
|
6
6
|
module Ravioli
|
7
7
|
class Configuration < OpenStruct
|
8
|
-
attr_reader :key_path
|
9
|
-
|
10
8
|
def initialize(attributes = {})
|
11
9
|
super({})
|
12
10
|
@key_path = attributes.delete(:key_path)
|
13
11
|
append(attributes)
|
14
12
|
end
|
15
13
|
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# end
|
20
|
-
|
14
|
+
# Convert a hash to accessors and nested {Ravioli::Configuration} instances.
|
15
|
+
#
|
16
|
+
# @param [Hash, #each] key-value pairs to be converted to accessors
|
21
17
|
def append(attributes = {})
|
18
|
+
return unless attributes.respond_to?(:each)
|
22
19
|
attributes.each do |key, value|
|
23
20
|
self[key.to_sym] = cast(key.to_sym, value)
|
24
21
|
end
|
@@ -40,6 +37,10 @@ module Ravioli
|
|
40
37
|
fetch(*keys) { raise KeyMissingError.new("Could not find value at key path #{keys.inspect}") }
|
41
38
|
end
|
42
39
|
|
40
|
+
def delete(key)
|
41
|
+
table.delete(key.to_s)
|
42
|
+
end
|
43
|
+
|
43
44
|
def fetch(*keys)
|
44
45
|
dig(*keys) || yield
|
45
46
|
end
|
@@ -54,6 +55,8 @@ module Ravioli
|
|
54
55
|
|
55
56
|
private
|
56
57
|
|
58
|
+
attr_reader :key_path
|
59
|
+
|
57
60
|
def build(keys, attributes = {})
|
58
61
|
attributes[:key_path] = key_path_for(keys)
|
59
62
|
child = self.class.new(attributes)
|
data/lib/ravioli/engine.rb
CHANGED
@@ -1,18 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "./staging_inquirer"
|
4
|
+
Rails.env.class.prepend Ravioli::StagingInquirer
|
5
|
+
|
3
6
|
module Ravioli
|
4
7
|
class Engine < ::Rails::Engine
|
5
8
|
# Bootstrap Ravioli onto the Rails app
|
6
|
-
initializer "ravioli", before:
|
7
|
-
Rails.extend Ravioli::
|
9
|
+
initializer "ravioli", before: :load_environment_config do |app|
|
10
|
+
Rails.extend Ravioli::RailsConfig unless Rails.respond_to?(:config)
|
8
11
|
end
|
9
12
|
end
|
10
13
|
|
11
|
-
|
14
|
+
private
|
15
|
+
|
16
|
+
module RailsConfig
|
12
17
|
def config
|
13
18
|
Ravioli.default || Ravioli.build(namespace: Rails.application&.class&.module_parent, strict: Rails.env.production?) do |config|
|
14
19
|
config.add_staging_flag!
|
15
|
-
config.
|
20
|
+
config.auto_load_files!
|
16
21
|
config.auto_load_credentials!
|
17
22
|
end
|
18
23
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ravioli
|
4
|
+
# A module that we mix in to the `Rails.env` inquirer class to add some extra staging-related
|
5
|
+
# metadata
|
6
|
+
module StagingInquirer
|
7
|
+
# Add a `name` method to `Rails.env` that will return "staging" for staging environments, and
|
8
|
+
# otherwise the string's value
|
9
|
+
def name
|
10
|
+
staging? ? "staging" : to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add a `strict:` keyword to reduce `Rails.env.production && !Rails.env.staging` calls
|
14
|
+
def production?(strict: false)
|
15
|
+
is_production = super()
|
16
|
+
return is_production unless strict && is_production
|
17
|
+
|
18
|
+
is_production && !staging?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Override staging inquiries to check against the current configuration
|
22
|
+
def staging?
|
23
|
+
Rails.try(:config)&.staging?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/ravioli/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ravioli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Flip Sasser
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -30,6 +30,48 @@ dependencies:
|
|
30
30
|
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 6.0.3.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: guard
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: guard-rspec
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: guard-rubocop
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
33
75
|
- !ruby/object:Gem::Dependency
|
34
76
|
name: pry
|
35
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -90,16 +132,16 @@ dependencies:
|
|
90
132
|
name: rubocop
|
91
133
|
requirement: !ruby/object:Gem::Requirement
|
92
134
|
requirements:
|
93
|
-
- - "
|
135
|
+
- - ">="
|
94
136
|
- !ruby/object:Gem::Version
|
95
|
-
version: '0
|
137
|
+
version: '1.0'
|
96
138
|
type: :development
|
97
139
|
prerelease: false
|
98
140
|
version_requirements: !ruby/object:Gem::Requirement
|
99
141
|
requirements:
|
100
|
-
- - "
|
142
|
+
- - ">="
|
101
143
|
- !ruby/object:Gem::Version
|
102
|
-
version: '0
|
144
|
+
version: '1.0'
|
103
145
|
- !ruby/object:Gem::Dependency
|
104
146
|
name: rubocop-ordered_methods
|
105
147
|
requirement: !ruby/object:Gem::Requirement
|
@@ -118,44 +160,44 @@ dependencies:
|
|
118
160
|
name: rubocop-performance
|
119
161
|
requirement: !ruby/object:Gem::Requirement
|
120
162
|
requirements:
|
121
|
-
- - "
|
163
|
+
- - ">="
|
122
164
|
- !ruby/object:Gem::Version
|
123
|
-
version: 1.5
|
165
|
+
version: '1.5'
|
124
166
|
type: :development
|
125
167
|
prerelease: false
|
126
168
|
version_requirements: !ruby/object:Gem::Requirement
|
127
169
|
requirements:
|
128
|
-
- - "
|
170
|
+
- - ">="
|
129
171
|
- !ruby/object:Gem::Version
|
130
|
-
version: 1.5
|
172
|
+
version: '1.5'
|
131
173
|
- !ruby/object:Gem::Dependency
|
132
174
|
name: rubocop-rails
|
133
175
|
requirement: !ruby/object:Gem::Requirement
|
134
176
|
requirements:
|
135
|
-
- - "
|
177
|
+
- - ">="
|
136
178
|
- !ruby/object:Gem::Version
|
137
|
-
version: 2.5.
|
179
|
+
version: 2.5.0
|
138
180
|
type: :development
|
139
181
|
prerelease: false
|
140
182
|
version_requirements: !ruby/object:Gem::Requirement
|
141
183
|
requirements:
|
142
|
-
- - "
|
184
|
+
- - ">="
|
143
185
|
- !ruby/object:Gem::Version
|
144
|
-
version: 2.5.
|
186
|
+
version: 2.5.0
|
145
187
|
- !ruby/object:Gem::Dependency
|
146
188
|
name: rubocop-rspec
|
147
189
|
requirement: !ruby/object:Gem::Requirement
|
148
190
|
requirements:
|
149
|
-
- - "
|
191
|
+
- - ">="
|
150
192
|
- !ruby/object:Gem::Version
|
151
|
-
version: '
|
193
|
+
version: '2.0'
|
152
194
|
type: :development
|
153
195
|
prerelease: false
|
154
196
|
version_requirements: !ruby/object:Gem::Requirement
|
155
197
|
requirements:
|
156
|
-
- - "
|
198
|
+
- - ">="
|
157
199
|
- !ruby/object:Gem::Version
|
158
|
-
version: '
|
200
|
+
version: '2.0'
|
159
201
|
- !ruby/object:Gem::Dependency
|
160
202
|
name: simplecov
|
161
203
|
requirement: !ruby/object:Gem::Requirement
|
@@ -190,14 +232,14 @@ dependencies:
|
|
190
232
|
requirements:
|
191
233
|
- - "~>"
|
192
234
|
- !ruby/object:Gem::Version
|
193
|
-
version:
|
235
|
+
version: 0.13.0
|
194
236
|
type: :development
|
195
237
|
prerelease: false
|
196
238
|
version_requirements: !ruby/object:Gem::Requirement
|
197
239
|
requirements:
|
198
240
|
- - "~>"
|
199
241
|
- !ruby/object:Gem::Version
|
200
|
-
version:
|
242
|
+
version: 0.13.0
|
201
243
|
- !ruby/object:Gem::Dependency
|
202
244
|
name: yard
|
203
245
|
requirement: !ruby/object:Gem::Requirement
|
@@ -229,6 +271,7 @@ files:
|
|
229
271
|
- lib/ravioli/builder.rb
|
230
272
|
- lib/ravioli/configuration.rb
|
231
273
|
- lib/ravioli/engine.rb
|
274
|
+
- lib/ravioli/staging_inquirer.rb
|
232
275
|
- lib/ravioli/version.rb
|
233
276
|
homepage: https://github.com/flipsasser/ravioli
|
234
277
|
licenses:
|
@@ -236,7 +279,7 @@ licenses:
|
|
236
279
|
metadata:
|
237
280
|
homepage_uri: https://github.com/flipsasser/ravioli
|
238
281
|
source_code_uri: https://github.com/flipsasser/ravioli
|
239
|
-
post_install_message:
|
282
|
+
post_install_message:
|
240
283
|
rdoc_options: []
|
241
284
|
require_paths:
|
242
285
|
- lib
|
@@ -244,15 +287,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
244
287
|
requirements:
|
245
288
|
- - ">="
|
246
289
|
- !ruby/object:Gem::Version
|
247
|
-
version: 2.
|
290
|
+
version: 2.4.0
|
248
291
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
249
292
|
requirements:
|
250
293
|
- - ">="
|
251
294
|
- !ruby/object:Gem::Version
|
252
295
|
version: '0'
|
253
296
|
requirements: []
|
254
|
-
rubygems_version: 3.
|
255
|
-
signing_key:
|
297
|
+
rubygems_version: 3.2.3
|
298
|
+
signing_key:
|
256
299
|
specification_version: 4
|
257
300
|
summary: Grab a fork and twist all your configuration spaghetti into a single, delicious
|
258
301
|
bundle
|