invar 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +26 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +324 -0
- data/RELEASE_NOTES.md +75 -0
- data/Rakefile +15 -0
- data/invar.gemspec +41 -0
- data/lib/invar/file_locator.rb +86 -0
- data/lib/invar/rake.rb +47 -0
- data/lib/invar/rake_tasks.rb +178 -0
- data/lib/invar/scope.rb +58 -0
- data/lib/invar/test.rb +16 -0
- data/lib/invar/version.rb +6 -0
- data/lib/invar.rb +273 -0
- metadata +176 -0
data/lib/invar/rake.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'io/console'
|
5
|
+
require 'tempfile'
|
6
|
+
require_relative 'rake_tasks'
|
7
|
+
|
8
|
+
namespace :invar do
|
9
|
+
namespace :configs do
|
10
|
+
desc 'Create a new configuration file'
|
11
|
+
task :create, [:namespace] do |task, args|
|
12
|
+
Invar::RakeTasks::ConfigTask.new(args[:namespace], task).create
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Edit the config in your default editor'
|
16
|
+
task :edit, [:namespace] do |task, args|
|
17
|
+
Invar::RakeTasks::ConfigTask.new(args[:namespace], task).edit
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
namespace :secrets do
|
22
|
+
desc 'Create a new encrypted secrets file'
|
23
|
+
task :create, [:namespace] do |task, args|
|
24
|
+
Invar::RakeTasks::SecretTask.new(args[:namespace], task).create
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'Edit the encrypted secrets file in your default editor'
|
28
|
+
task :edit, [:namespace] do |task, args|
|
29
|
+
Invar::RakeTasks::SecretTask.new(args[:namespace], task).edit
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# aliasing
|
34
|
+
namespace :config do
|
35
|
+
task :create, [:namespace] => ['configs:create']
|
36
|
+
task :edit, [:namespace] => ['configs:edit']
|
37
|
+
end
|
38
|
+
namespace :secret do
|
39
|
+
task :create, [:namespace] => ['secrets:create']
|
40
|
+
task :edit, [:namespace] => ['secrets:edit']
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'Show directories to be searched for the given namespace'
|
44
|
+
task :paths, [:namespace] do |task, args|
|
45
|
+
Invar::RakeTasks::StateTask.new(args[:namespace], task).show_paths
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'invar'
|
4
|
+
|
5
|
+
module Invar
|
6
|
+
# Rake task implementation.
|
7
|
+
#
|
8
|
+
# The actual rake tasks themselves are thinly defined in invar/rake.rb (so that the external include
|
9
|
+
# path is nice and short)
|
10
|
+
module RakeTasks
|
11
|
+
# Template config YAML file
|
12
|
+
CONFIG_TEMPLATE = SECRETS_TEMPLATE = <<~YML
|
13
|
+
---
|
14
|
+
YML
|
15
|
+
|
16
|
+
# Tasks that use a namespace
|
17
|
+
class NamespacedTask
|
18
|
+
def initialize(namespace, task)
|
19
|
+
if namespace.nil?
|
20
|
+
raise NamespaceMissingError,
|
21
|
+
"Namespace argument required. Run with: bundle exec rake #{ task.name }[namespace_here]"
|
22
|
+
end
|
23
|
+
|
24
|
+
@namespace = namespace
|
25
|
+
@locator = FileLocator.new(@namespace)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Configuration file actions.
|
30
|
+
class ConfigTask < NamespacedTask
|
31
|
+
# Creates a config file in the appropriate location
|
32
|
+
def create
|
33
|
+
config_dir = @locator.search_paths.first
|
34
|
+
config_dir.mkpath
|
35
|
+
|
36
|
+
file = config_dir / 'config.yml'
|
37
|
+
if file.exist?
|
38
|
+
warn <<~MSG
|
39
|
+
Abort: File exists. (#{ file })
|
40
|
+
Maybe you meant to edit the file with bundle exec rake invar:secrets:edit[#{ @namespace }]?
|
41
|
+
MSG
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
|
45
|
+
file.write CONFIG_TEMPLATE
|
46
|
+
|
47
|
+
warn "Created file: #{ file }"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Edits the existing config file in the appropriate location
|
51
|
+
def edit
|
52
|
+
configs_file = begin
|
53
|
+
@locator.find('config.yml')
|
54
|
+
rescue ::Invar::FileLocator::FileNotFoundError => e
|
55
|
+
warn <<~ERR
|
56
|
+
Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
|
57
|
+
Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:configs:create?
|
58
|
+
ERR
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
|
62
|
+
system(ENV.fetch('EDITOR', 'editor'), configs_file.to_s, exception: true)
|
63
|
+
|
64
|
+
warn "File saved to: #{ configs_file }"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Secrets file actions.
|
69
|
+
class SecretTask < NamespacedTask
|
70
|
+
# Instructions hint for how to handle secret keys.
|
71
|
+
SECRETS_INSTRUCTIONS = <<~INST
|
72
|
+
Save this key to a secure password manager. You will need it to edit the secrets.yml file.
|
73
|
+
INST
|
74
|
+
|
75
|
+
# Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
|
76
|
+
def create
|
77
|
+
config_dir = ::Invar::FileLocator.new(@namespace).search_paths.first
|
78
|
+
config_dir.mkpath
|
79
|
+
|
80
|
+
file = config_dir / 'secrets.yml'
|
81
|
+
|
82
|
+
if file.exist?
|
83
|
+
warn <<~ERR
|
84
|
+
Abort: File exists. (#{ file })
|
85
|
+
Maybe you meant to edit the file with bundle exec rake invar:secrets:edit[#{ @namespace }]?
|
86
|
+
ERR
|
87
|
+
exit 1
|
88
|
+
end
|
89
|
+
|
90
|
+
encryption_key = Lockbox.generate_key
|
91
|
+
|
92
|
+
write_encrypted_file(file, encryption_key, SECRETS_TEMPLATE)
|
93
|
+
|
94
|
+
warn "Created file #{ file }"
|
95
|
+
|
96
|
+
warn SECRETS_INSTRUCTIONS
|
97
|
+
warn 'Generated key is:'
|
98
|
+
puts encryption_key
|
99
|
+
end
|
100
|
+
|
101
|
+
# Opens an editor for the decrypted contents of the secrets file. After closing the editor, the file will be
|
102
|
+
# updated with the new encrypted contents.
|
103
|
+
def edit
|
104
|
+
secrets_file = begin
|
105
|
+
@locator.find('secrets.yml')
|
106
|
+
rescue ::Invar::FileLocator::FileNotFoundError => e
|
107
|
+
warn <<~ERR
|
108
|
+
Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
|
109
|
+
Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:secrets:create?
|
110
|
+
ERR
|
111
|
+
exit 1
|
112
|
+
end
|
113
|
+
|
114
|
+
edit_encrypted_file(secrets_file)
|
115
|
+
|
116
|
+
warn "File saved to #{ secrets_file }"
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def write_encrypted_file(file_path, encryption_key, content)
|
122
|
+
lockbox = Lockbox.new(key: encryption_key)
|
123
|
+
|
124
|
+
encrypted_data = lockbox.encrypt(content)
|
125
|
+
|
126
|
+
# TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
|
127
|
+
File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
|
128
|
+
end
|
129
|
+
|
130
|
+
def edit_encrypted_file(file_path)
|
131
|
+
encryption_key = determine_key(file_path)
|
132
|
+
|
133
|
+
lockbox = build_lockbox(encryption_key)
|
134
|
+
|
135
|
+
file_str = Tempfile.create(file_path.basename.to_s) do |tmp_file|
|
136
|
+
decrypted = lockbox.decrypt(file_path.binread)
|
137
|
+
|
138
|
+
tmp_file.write(decrypted)
|
139
|
+
tmp_file.rewind # rewind needed because file does not get closed after write
|
140
|
+
system(ENV.fetch('EDITOR', 'editor'), tmp_file.path, exception: true)
|
141
|
+
tmp_file.read
|
142
|
+
end
|
143
|
+
|
144
|
+
write_encrypted_file(file_path, encryption_key, file_str)
|
145
|
+
end
|
146
|
+
|
147
|
+
def determine_key(file_path)
|
148
|
+
encryption_key = Lockbox.master_key
|
149
|
+
|
150
|
+
if encryption_key.nil? && $stdin.respond_to?(:noecho)
|
151
|
+
warn "Enter master key to decrypt #{ file_path }:"
|
152
|
+
encryption_key = $stdin.noecho(&:gets).strip
|
153
|
+
end
|
154
|
+
|
155
|
+
encryption_key
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_lockbox(encryption_key)
|
159
|
+
Lockbox.new(key: encryption_key)
|
160
|
+
rescue ArgumentError => e
|
161
|
+
raise SecretsFileEncryptionError, e
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# General status tasks
|
166
|
+
class StateTask < NamespacedTask
|
167
|
+
# Prints the current paths to be searched in
|
168
|
+
def show_paths
|
169
|
+
warn @locator.search_paths.join("\n")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Raised when the namespace Rake task parameter is missing. Add it in square brackets after the task name when
|
174
|
+
# running Rake.
|
175
|
+
class NamespaceMissingError < RuntimeError
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/invar/scope.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Invar
|
4
|
+
# A set of configurations
|
5
|
+
class Scope
|
6
|
+
def initialize(data = nil)
|
7
|
+
@data = convert(data)
|
8
|
+
|
9
|
+
@data.freeze
|
10
|
+
@data_override = {}
|
11
|
+
freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
# Retrieve the value for the given key
|
15
|
+
#
|
16
|
+
# @param [symbol] key
|
17
|
+
# @raise KeyError if no such key exists.
|
18
|
+
# @see #override
|
19
|
+
def fetch(key)
|
20
|
+
key = key.downcase.to_sym
|
21
|
+
@data_override.fetch(key, @data.fetch(key))
|
22
|
+
rescue KeyError => e
|
23
|
+
raise KeyError, "#{ e.message }. Known keys are #{ @data.keys.sort.collect { |k| ":#{ k }" }.join(', ') }"
|
24
|
+
end
|
25
|
+
|
26
|
+
alias / fetch
|
27
|
+
alias [] fetch
|
28
|
+
|
29
|
+
def pretend(**)
|
30
|
+
raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::MSG
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_h
|
34
|
+
@data.merge(@data_override).to_h.transform_values do |value|
|
35
|
+
case value
|
36
|
+
when Scope
|
37
|
+
value.to_h
|
38
|
+
else
|
39
|
+
value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def key?(key_name)
|
45
|
+
to_h.key?(key_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def convert(data)
|
51
|
+
(data || {}).dup.each_with_object({}) do |pair, agg|
|
52
|
+
key, value = pair
|
53
|
+
|
54
|
+
agg[key] = value.is_a?(Hash) ? Scope.new(value) : value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/invar/test.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'invar'
|
4
|
+
|
5
|
+
module Invar
|
6
|
+
# Extension to the standard class
|
7
|
+
class Scope
|
8
|
+
# Overrides the given set of key-value pairs. This is intended to only be used in testing environments,
|
9
|
+
# where you may need contextual adjustments to suit the test situation.
|
10
|
+
#
|
11
|
+
# @param [Hash] pairs the hash of pairs to override.
|
12
|
+
def pretend(pairs)
|
13
|
+
@data_override.merge!(pairs.transform_keys(&:to_sym))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/invar.rb
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'invar/version'
|
4
|
+
require 'invar/file_locator'
|
5
|
+
require 'invar/scope'
|
6
|
+
|
7
|
+
require 'yaml'
|
8
|
+
require 'lockbox'
|
9
|
+
require 'pathname'
|
10
|
+
require 'dry/schema'
|
11
|
+
|
12
|
+
# Invar is a Ruby Gem that provides a single source of truth for application configuration and environment.
|
13
|
+
module Invar
|
14
|
+
# Alias for Invar::Invar.new
|
15
|
+
#
|
16
|
+
# @see Invar.new
|
17
|
+
def self.new(**args)
|
18
|
+
Invar.new(**args)
|
19
|
+
end
|
20
|
+
|
21
|
+
# A wrapper for config and ENV variable data. It endeavours to limit your situation to a single source of truth.
|
22
|
+
class Invar
|
23
|
+
# Allowed permissions modes for lockfile. Readable or read-writable by the current user only
|
24
|
+
ALLOWED_LOCKFILE_MODES = [0o600, 0o400].freeze
|
25
|
+
|
26
|
+
# Name of the default key file to be searched for within config directories
|
27
|
+
DEFAULT_KEY_FILE_NAME = 'master_key'
|
28
|
+
|
29
|
+
# Constructs a new Invar.
|
30
|
+
#
|
31
|
+
# It will search for config, secrets, and decryption key files using the XDG specification.
|
32
|
+
#
|
33
|
+
# The secrets file is decrypted using Lockbox. The key will be requested from these locations in order:
|
34
|
+
#
|
35
|
+
# 1. the Lockbox.master_key variable
|
36
|
+
# 2. the LOCKBOX_MASTER_KEY environment variable.
|
37
|
+
# 3. saved in a secure key file (recommended)
|
38
|
+
# 4. manual terminal prompt entry (recommended)
|
39
|
+
#
|
40
|
+
# The :decryption_keyfile argument specifies the filename to read for option 3. It will be searched for in
|
41
|
+
# the same XDG locations as the secrets file. The decryption keyfile will be checked for safe permission modes.
|
42
|
+
#
|
43
|
+
# NEVER hardcode your encryption key. This class intentionally does not accept a raw string of your decryption
|
44
|
+
# key to discourage hardcoding your encryption key and committing it to version control.
|
45
|
+
#
|
46
|
+
# @param [String] namespace name of the subdirectory within XDG locations
|
47
|
+
# @param [#read] decryption_keyfile Any #read capable object referring to the decryption key file
|
48
|
+
def initialize(namespace:, decryption_keyfile: nil, configs_schema: nil, secrets_schema: nil)
|
49
|
+
locator = FileLocator.new(namespace)
|
50
|
+
search_paths = locator.search_paths.join(', ')
|
51
|
+
|
52
|
+
begin
|
53
|
+
@configs = Scope.new(load_configs(locator))
|
54
|
+
rescue FileLocator::FileNotFoundError
|
55
|
+
raise MissingConfigFileError,
|
56
|
+
"No config file found. Create config.yml in one of these locations: #{ search_paths }"
|
57
|
+
end
|
58
|
+
|
59
|
+
begin
|
60
|
+
@secrets = Scope.new(load_secrets(locator, decryption_keyfile))
|
61
|
+
rescue FileLocator::FileNotFoundError
|
62
|
+
raise MissingSecretsFileError,
|
63
|
+
"No secrets file found. Create encrypted secrets.yml in one of these locations: #{ search_paths }"
|
64
|
+
end
|
65
|
+
|
66
|
+
freeze
|
67
|
+
# instance_eval(&self.class.__override_block__)
|
68
|
+
self.class.__override_block__&.call(self)
|
69
|
+
|
70
|
+
validate_with configs_schema, secrets_schema
|
71
|
+
end
|
72
|
+
|
73
|
+
class << self
|
74
|
+
attr_accessor :__override_block__
|
75
|
+
end
|
76
|
+
|
77
|
+
# Fetch from one of the two base scopes: :config or :secret.
|
78
|
+
# Plural names are also accepted (ie. :configs and :secrets).
|
79
|
+
#
|
80
|
+
# @param base_scope [Symbol, String]
|
81
|
+
def fetch(base_scope)
|
82
|
+
case base_scope
|
83
|
+
when /configs?/i
|
84
|
+
@configs
|
85
|
+
when /secrets?/i
|
86
|
+
@secrets
|
87
|
+
else
|
88
|
+
raise ArgumentError, 'The root scope name must be either :config or :secret.'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
alias / fetch
|
93
|
+
alias [] fetch
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def load_configs(locator)
|
98
|
+
file = locator.find('config', EXT::YAML)
|
99
|
+
|
100
|
+
configs = parse(file.read)
|
101
|
+
|
102
|
+
collision_key = configs.keys.collect(&:downcase).find { |key| env_hash.key? key }
|
103
|
+
if collision_key
|
104
|
+
hint = EnvConfigCollisionError::HINT
|
105
|
+
raise EnvConfigCollisionError,
|
106
|
+
"Both the environment and your config file have key #{ collision_key }. #{ hint }"
|
107
|
+
end
|
108
|
+
|
109
|
+
configs.merge(env_hash)
|
110
|
+
end
|
111
|
+
|
112
|
+
def env_hash
|
113
|
+
ENV.to_hash.transform_keys(&:downcase).transform_keys(&:to_sym)
|
114
|
+
end
|
115
|
+
|
116
|
+
def load_secrets(locator, decryption_keyfile)
|
117
|
+
file = locator.find('secrets', EXT::YAML)
|
118
|
+
|
119
|
+
lockbox = begin
|
120
|
+
decryption_key = Lockbox.master_key || resolve_key(decryption_keyfile, locator,
|
121
|
+
"Enter master key to decrypt #{ file }:")
|
122
|
+
|
123
|
+
Lockbox.new(key: decryption_key)
|
124
|
+
rescue Lockbox::Error => e
|
125
|
+
raise SecretsFileDecryptionError, e
|
126
|
+
end
|
127
|
+
|
128
|
+
bytes = file.binread
|
129
|
+
file_data = begin
|
130
|
+
lockbox.decrypt bytes
|
131
|
+
rescue Lockbox::DecryptionError => e
|
132
|
+
hint = 'Perhaps you used the wrong file decryption key?'
|
133
|
+
raise SecretsFileDecryptionError, "Failed to open #{ file } (#{ e }). #{ hint }"
|
134
|
+
end
|
135
|
+
|
136
|
+
parse(file_data)
|
137
|
+
end
|
138
|
+
|
139
|
+
def parse(file_data)
|
140
|
+
YAML.safe_load(file_data, symbolize_names: true) || {}
|
141
|
+
end
|
142
|
+
|
143
|
+
def resolve_key(pathname, locator, prompt)
|
144
|
+
key_file = locator.find(pathname || DEFAULT_KEY_FILE_NAME)
|
145
|
+
|
146
|
+
read_keyfile(key_file)
|
147
|
+
rescue FileLocator::FileNotFoundError
|
148
|
+
if $stdin.respond_to?(:noecho)
|
149
|
+
warn prompt
|
150
|
+
$stdin.noecho(&:gets).strip
|
151
|
+
else
|
152
|
+
raise SecretsFileDecryptionError,
|
153
|
+
"Could not find file '#{ pathname }'. Searched in: #{ locator.search_paths }"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def read_keyfile(key_file)
|
158
|
+
permissions_mask = 0o777 # only the lowest three digits are perms, so masking
|
159
|
+
stat = key_file.stat
|
160
|
+
file_mode = stat.mode & permissions_mask
|
161
|
+
# TODO: use stat.world_readable? etc instead
|
162
|
+
unless ALLOWED_LOCKFILE_MODES.include? file_mode
|
163
|
+
hint = "Try: chmod 600 #{ key_file }"
|
164
|
+
raise SecretsFileDecryptionError,
|
165
|
+
format("File '%<path>s' has improper permissions (%<mode>04o). %<hint>s",
|
166
|
+
path: key_file,
|
167
|
+
mode: file_mode,
|
168
|
+
hint: hint)
|
169
|
+
end
|
170
|
+
|
171
|
+
key_file.read.strip
|
172
|
+
end
|
173
|
+
|
174
|
+
def validate_with(configs_schema, secrets_schema)
|
175
|
+
env_keys = env_hash.keys
|
176
|
+
|
177
|
+
configs_schema ||= Dry::Schema.define
|
178
|
+
|
179
|
+
# Special schema for just the env variables, listing them explicitly allows for using validate_keys
|
180
|
+
env_schema = Dry::Schema.define do
|
181
|
+
env_keys.each do |key|
|
182
|
+
optional(key)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
schema = Dry::Schema.define do
|
187
|
+
config.validate_keys = true
|
188
|
+
|
189
|
+
required(:configs).hash(configs_schema & env_schema)
|
190
|
+
|
191
|
+
if secrets_schema
|
192
|
+
required(:secrets).hash(secrets_schema)
|
193
|
+
else
|
194
|
+
required(:secrets)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
validation = schema.call(configs: @configs.to_h,
|
199
|
+
secrets: @secrets.to_h)
|
200
|
+
|
201
|
+
return true if validation.success?
|
202
|
+
|
203
|
+
errs = validation.errors.messages.collect do |message|
|
204
|
+
[message.path.collect do |p|
|
205
|
+
":#{ p }"
|
206
|
+
end.join(' / '), message.text].join(' ')
|
207
|
+
end
|
208
|
+
|
209
|
+
raise SchemaValidationError, <<~ERR
|
210
|
+
Validation errors:
|
211
|
+
#{ errs.join("\n ") }
|
212
|
+
ERR
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
class << self
|
217
|
+
# Block that will be run after loading from config files and prior to freezing. It is intended to allow
|
218
|
+
# for test suites to tweak configurations without having to duplicate the entire config file.
|
219
|
+
#
|
220
|
+
# @yieldparam the configs from the Invar
|
221
|
+
# @return [void]
|
222
|
+
def after_load(&block)
|
223
|
+
::Invar::Invar.__override_block__ = block
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Raised when no config file can be found within the search paths.
|
228
|
+
class MissingConfigFileError < RuntimeError
|
229
|
+
end
|
230
|
+
|
231
|
+
# Raised when no secrets file can be found within the search paths.
|
232
|
+
class MissingSecretsFileError < RuntimeError
|
233
|
+
end
|
234
|
+
|
235
|
+
# Raised when an error is encountered during secrets file encryption
|
236
|
+
class SecretsFileEncryptionError < RuntimeError
|
237
|
+
end
|
238
|
+
|
239
|
+
# Raised when an error is encountered during secrets file decryption
|
240
|
+
class SecretsFileDecryptionError < RuntimeError
|
241
|
+
end
|
242
|
+
|
243
|
+
# Raised when a key is defined in both the environment and the configuration file.
|
244
|
+
class EnvConfigCollisionError < RuntimeError
|
245
|
+
# Message hinting at possible solution
|
246
|
+
HINT = 'Either rename your config entry or remove the environment variable.'
|
247
|
+
end
|
248
|
+
|
249
|
+
# Raised when there are config or secrets files found at multiple locations. You can resolve this by deciding on
|
250
|
+
# one correct location and removing the alternate file(s).
|
251
|
+
class AmbiguousSourceError < RuntimeError
|
252
|
+
# Message hinting at possible solution
|
253
|
+
HINT = 'Choose 1 correct one and delete the others.'
|
254
|
+
end
|
255
|
+
|
256
|
+
# Raised when #pretend is called but the testing extension has not been loaded.
|
257
|
+
#
|
258
|
+
# When raised during normal operation, it may mean the application is calling #pretend directly, which is strongly
|
259
|
+
# discouraged. The feature is meant for testing.
|
260
|
+
#
|
261
|
+
# @see Invar#pretend
|
262
|
+
class ImmutableRealityError < NoMethodError
|
263
|
+
# Message and hint for a possible solution
|
264
|
+
MSG = <<~MSG
|
265
|
+
Method 'pretend' is defined in the testing extension. Try adding this to your test suite config file:
|
266
|
+
require 'invar/test'
|
267
|
+
MSG
|
268
|
+
end
|
269
|
+
|
270
|
+
# Raised when schema validation fails
|
271
|
+
class SchemaValidationError < RuntimeError
|
272
|
+
end
|
273
|
+
end
|