ravioli 0.1.0 → 0.1.5

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: 10956034d9851d2c30cc00a73bbb95ece033cb8ca1d6bc66dee4ad106335b430
4
- data.tar.gz: 340b538b33f19016fbc494633ef809a79b1df518016af43de1c84f5428ce42fd
3
+ metadata.gz: 7531708baaf2764477fc99c00343d6ccb10873695ccd6d20c56cb38a21f6520d
4
+ data.tar.gz: 13b46317a8d6b9fa5f167cab39c3cef998889951fb99b44d0177612798c087e8
5
5
  SHA512:
6
- metadata.gz: e639d69ce9ccd5e373805b8e06edc8d2e5a468af695ce40dc6630cafbfe2d8ae403fe63cda38ba61b6d6b8becdbdca846566d6f5aea374a4ae9905ae66527d74
7
- data.tar.gz: f71dd8b25999a8972fc3ac9173c488ce337233f5f2496dcdb084c59067ea48056026ac65d9f1a8f176aac52c5eca9b5c71b6202998ebc40761bb6ff0064c491e
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 aim to *please*
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 [`load_configuration_file(filename, key: File.basename(filename) != "app")`](#load_configuration_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`.
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.auto_load_config_files!
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.load_configuration_file("whatever.yml")
245
+ config.load_file("things.yml")
247
246
  config.whatever = {things: true}
248
247
  end
249
248
  ```
250
249
 
251
- This will yield a configured instance of `Ravioli::Configuration` with structure
250
+ This will return a configured instance of `Ravioli::Configuration` with structure
252
251
 
253
252
  ```yaml
254
- rubocop:
255
- # ...the contents of whatever.yml
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
- ### `load_config_file`
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
- load_config_file(:mailjet) # given a symbol, it automatically assumes you meant `config/mailjet.yml`
333
- load_config_file("config/mailjet") # same as above
334
- load_config_file("lib/mailjet/config") # looks for `Rails.root.join("lib", "mailjet", "config.yml")
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
- load_config_file(service)
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
- self.staging = File.exists?("./staging.txt") # technically you could do this ... I don't know why you would, but technically you could
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
- 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 fallback-specific way. Here's where it looks for each file:
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
- </td><td>
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
- `ENV["RAILS_MASTER_KEY"]`
425
+ **This means `RAILS_MASTER_KEY` MUST be the decryption key for your environment-specific credential file, if one exists.**
434
426
 
435
- </td></tr><tr><td>
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
- `config/credentials/production.yml.enc`
429
+ Here are a few examples
438
430
 
439
- </td><td>
431
+ <table><thead><tr><th>File</th><th>First it tries...</th><th>Then it tries...</th></tr></thead><tbody>
440
432
 
441
- `ENV["RAILS_PRODUCTION_KEY"]`
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
- </td><td>
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
- </td></tr><tr><td>
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
- `config/credentials/staging.yml.enc` (only if running on staging)
452
+ </tbody></table>
450
453
 
451
- </td><td>
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
- </td></tr></tbody></table>
456
+ #### TLDR:
460
457
 
461
- 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
+ 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
- # Ravioli contains helper methods for building Configuration instances and accessing them, as well
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
- def build(class_name: "Configuration", namespace: nil, strict: false, &block)
18
- builder = Builder.new(class_name: class_name, namespace: namespace, strict: strict)
19
- yield builder if block_given?
20
- builder.build!.tap do |configuration|
21
- configurations.push(configuration)
22
- end
23
- end
24
-
25
- def default
26
- configurations.last
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
 
@@ -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 clas provides a simple interface for building a Ravioli configuration. It has
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?` status
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
- # Load YAML or JSON files in config/**/* (except for locales)
41
- def auto_load_config_files!
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
- load_config_file(config_file, key: !File.basename(config_file, File.extname(config_file)).casecmp("app").zero?)
92
+ auto_load_file(config_file)
45
93
  end
46
94
  end
47
95
 
48
- # Load config/credentials**/*.yml.enc files (assuming we can find a key)
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 base config
51
- load_credentials(key_path: "config/master.key", env_name: "base")
52
-
53
- # Load any environment-specific configuration on top of it
54
- load_credentials("config/credentials/#{Rails.env}", key_path: "config/credentials/#{Rails.env}.key", env_name: "master")
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
- load_credentials("config/credentials/staging", key_path: "config/credentials/staging.key") if configuration.staging?
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 config file either with a given path or by name (e.g. `config/whatever.yml` or `:whatever`)
66
- def load_config_file(path, options = {})
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, env_name: path.split("/").last)
75
- credentials = parse_credentials(path, env_name: env_name, key_path: key_path)
76
- configuration.append(credentials) if credentials.present?
77
- rescue => error
78
- warn "Could not decrypt `#{path}.yml.enc' with key file `#{key_path}' or `ENV[\"#{env_name}\"]'", error
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("#{Ravioli::NAME} doesn't know how to parse #{path}")
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 parse_credentials(path, key_path: path, env_name: path.split("/").last)
260
+ def parse_env_name(env_name)
148
261
  env_name = env_name.to_s
149
- env_name = "RAILS_#{env_name.upcase}_KEY" unless env_name.upcase == 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
- credentials = Rails.application.encrypted(path, options)&.config || {}
156
- credentials
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 = "[#{Ravioli::NAME}] #{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(message) if defined? Rails
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
- # def ==(other)
17
- # other = other.table if other.respond_to?(:table)
18
- # other == table
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)
@@ -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: "load_environment_config" do |app|
7
- Rails.extend Ravioli::Config unless Rails.respond_to?(:config)
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
- module Config
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.auto_load_config_files!
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ravioli
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.5"
5
5
  end
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.0
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-02-23 00:00:00.000000000 Z
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.8'
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.8'
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.2
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.2
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.2
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.2
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: '1.39'
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: '1.39'
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: '0.4'
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: '0.4'
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.3.0
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.0.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