invar 0.4.0
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 +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
|