legion-settings 1.3.25 → 1.3.26
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/CHANGELOG.md +16 -0
- data/README.md +22 -14
- data/legion-settings.gemspec +1 -0
- data/lib/legion/settings/agent_loader.rb +10 -2
- data/lib/legion/settings/dns_bootstrap.rb +10 -10
- data/lib/legion/settings/helper.rb +1 -1
- data/lib/legion/settings/loader.rb +39 -38
- data/lib/legion/settings/project_env.rb +11 -2
- data/lib/legion/settings/resolver.rb +77 -65
- data/lib/legion/settings/schema.rb +8 -1
- data/lib/legion/settings/version.rb +1 -1
- data/lib/legion/settings.rb +31 -24
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4d1e8b1f3f0f96ee8db9d11604d914fc253e430827e37f7e30cc244151dc75b
|
|
4
|
+
data.tar.gz: 1ed13e9f16b97fc33acc72e484671794210ac4750aef5502fc86e685ec2f9324
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47ab18a67eef8c35d86e52e9deec5716916754e5a84e0faec1ba3bbed4f24e9f4bb5c7749268eee23c96056ce85ef11c47787ac2df06fa5b918365730ddf6926
|
|
7
|
+
data.tar.gz: c76877c9e9a19f44c33a816a0580c265f721fe6dd0ef486eb06ec377ac6aedd5ae60fe52b4aaf4d8feb59bb18527f6127da40a0611d27718de3be63af5581b97
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Legion::Settings Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.26] - 2026-04-02
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Added a runtime dependency on `legion-logging >= 1.4.0` and moved `Settings`, `Loader`, `Resolver`, `ProjectEnv`, and `AgentLoader` onto component-aware logging helpers
|
|
7
|
+
- Added `Legion::Logging::Helper` integration to top-level `Settings` access and DNS bootstrap logging so component tags and per-component levels apply consistently
|
|
8
|
+
- `extensions.default_extension_settings` now defaults to `{}` instead of injecting a synthetic logger config
|
|
9
|
+
- `Legion::Settings::Helper#settings` now returns `{}` when an extension has no explicit settings
|
|
10
|
+
- `region.default_affinity` now defaults to `any`
|
|
11
|
+
- `[]`, `dig`, and no-arg `load` now share the same project-env-aware load path, and the README now documents the actual `load`/`Loader.default_directories` split
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- `validate!` now clears stale validation errors before rebuilding state, and nested schema branches now fail when a hash-shaped setting is replaced with a scalar
|
|
15
|
+
- Loader mutation paths now invalidate indifferent-access and digest caches consistently, `load_module_default` no longer overwrites existing scalar settings, and resolver traversal now handles arrays of hashes
|
|
16
|
+
- `Resolver` now treats clustered Vault connectivity from `Legion::Crypt` as available for `vault://` resolution instead of relying only on the top-level `crypt.vault.connected` flag
|
|
17
|
+
- Logger-backed settings code paths now use consistent `log.debug/info/warn/error` behavior instead of mixed direct `Legion::Logging.*` calls
|
|
18
|
+
|
|
3
19
|
## [1.3.25] - 2026-03-31
|
|
4
20
|
|
|
5
21
|
### Added
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Configuration management module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Loads settings from JSON files, directories, and environment variables. Provides a unified `Legion::Settings[:key]` accessor used by all other Legion gems.
|
|
4
4
|
|
|
5
|
-
**Version**: 1.3.
|
|
5
|
+
**Version**: 1.3.26
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -21,20 +21,28 @@ gem 'legion-settings'
|
|
|
21
21
|
```ruby
|
|
22
22
|
require 'legion/settings'
|
|
23
23
|
|
|
24
|
-
Legion::Settings.load
|
|
24
|
+
Legion::Settings.load # loads defaults, env, DNS bootstrap, and nearest .legionio.env
|
|
25
|
+
Legion::Settings.load(config_dir: './settings') # also loads all .json files in the directory
|
|
25
26
|
|
|
26
27
|
Legion::Settings[:client][:hostname]
|
|
27
|
-
Legion::Settings
|
|
28
|
+
Legion::Settings.dig(:transport, :connection, :host)
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
`[]` and `dig` will auto-load settings on first access, and implicit access follows the same overlay/project-env/base precedence as explicit `load`.
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
2. `~/.legionio/settings/`
|
|
34
|
-
3. `~/legionio/`
|
|
35
|
-
4. `./settings/`
|
|
33
|
+
### Config Loading
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
`Legion::Settings.load` only consumes the paths you pass via `config_file`, `config_dir`, or `config_dirs`.
|
|
36
|
+
|
|
37
|
+
If a caller wants the canonical Legion search directories, use `Legion::Settings::Loader.default_directories`:
|
|
38
|
+
|
|
39
|
+
1. `~/.legionio/settings`
|
|
40
|
+
2. `/etc/legionio/settings` on Unix-like systems
|
|
41
|
+
3. `%APPDATA%\\legionio\\settings` on Windows when `APPDATA` is present
|
|
42
|
+
|
|
43
|
+
`LegionIO` uses those directories during daemon boot. Library consumers can choose to pass any directory set they want.
|
|
44
|
+
|
|
45
|
+
Each Legion module registers its own defaults via `merge_settings` during startup, and the nearest `.legionio.env` file is merged on top of base settings. Request overlays applied through `with_overlay` take highest precedence.
|
|
38
46
|
|
|
39
47
|
### Secret Resolution
|
|
40
48
|
|
|
@@ -76,25 +84,25 @@ Legion::Settings.validate! # raises ValidationError if any settings are invalid
|
|
|
76
84
|
|
|
77
85
|
# In development, warn instead of raising:
|
|
78
86
|
# Set LEGION_DEV=true or Legion::Settings.set_prop(:dev, true)
|
|
79
|
-
# validate! will warn
|
|
87
|
+
# validate! will warn through the configured logger instead of raising
|
|
80
88
|
```
|
|
81
89
|
|
|
82
90
|
### Logging Defaults
|
|
83
91
|
|
|
84
|
-
The `logging` key includes a `transport` sub-section
|
|
92
|
+
The `logging` key includes a `transport` sub-section that controls whether log events are forwarded over the message bus:
|
|
85
93
|
|
|
86
94
|
```json
|
|
87
95
|
{
|
|
88
96
|
"logging": {
|
|
89
97
|
"level": "info",
|
|
90
98
|
"format": "text",
|
|
91
|
-
"log_file":
|
|
99
|
+
"log_file": "./legionio/logs/legion.log",
|
|
92
100
|
"log_stdout": true,
|
|
93
101
|
"trace": true,
|
|
94
102
|
"async": true,
|
|
95
103
|
"include_pid": false,
|
|
96
104
|
"transport": {
|
|
97
|
-
"enabled":
|
|
105
|
+
"enabled": true,
|
|
98
106
|
"forward_logs": true,
|
|
99
107
|
"forward_exceptions": true
|
|
100
108
|
}
|
|
@@ -102,7 +110,7 @@ The `logging` key includes a `transport` sub-section (new in 1.3.22) that contro
|
|
|
102
110
|
}
|
|
103
111
|
```
|
|
104
112
|
|
|
105
|
-
When `transport.enabled` is `true`, log events and unhandled exceptions are published to the AMQP bus so a central log consumer can aggregate them.
|
|
113
|
+
When `transport.enabled` is `true`, log events and unhandled exceptions are published to the AMQP bus so a central log consumer can aggregate them.
|
|
106
114
|
|
|
107
115
|
## Requirements
|
|
108
116
|
|
data/legion-settings.gemspec
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
require 'json'
|
|
5
|
+
require 'legion/logging'
|
|
5
6
|
|
|
6
7
|
module Legion
|
|
7
8
|
module Settings
|
|
8
9
|
module AgentLoader
|
|
10
|
+
extend Legion::Logging::Helper
|
|
11
|
+
|
|
9
12
|
EXTENSIONS = %w[.yaml .yml .json].freeze
|
|
10
13
|
GLOB = '*.{yaml,yml,json}'
|
|
11
14
|
|
|
@@ -44,12 +47,17 @@ module Legion
|
|
|
44
47
|
|
|
45
48
|
private
|
|
46
49
|
|
|
50
|
+
def resolve_logger_settings
|
|
51
|
+
raw_logging = Legion::Settings.loader&.settings&.dig(:logging) if Legion::Settings.respond_to?(:loader)
|
|
52
|
+
raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
|
|
53
|
+
end
|
|
54
|
+
|
|
47
55
|
def log_debug(message)
|
|
48
|
-
|
|
56
|
+
log.debug(message)
|
|
49
57
|
end
|
|
50
58
|
|
|
51
59
|
def log_warn(message)
|
|
52
|
-
|
|
60
|
+
log.warn(message)
|
|
53
61
|
end
|
|
54
62
|
end
|
|
55
63
|
end
|
|
@@ -5,10 +5,13 @@ require 'json'
|
|
|
5
5
|
require 'net/http'
|
|
6
6
|
require 'resolv'
|
|
7
7
|
require 'uri'
|
|
8
|
+
require 'legion/logging'
|
|
8
9
|
|
|
9
10
|
module Legion
|
|
10
11
|
module Settings
|
|
11
12
|
class DnsBootstrap
|
|
13
|
+
include Legion::Logging::Helper
|
|
14
|
+
|
|
12
15
|
CACHE_FILENAME = '_dns_bootstrap.json'
|
|
13
16
|
HOSTNAME_PREFIX = 'legion-bootstrap'
|
|
14
17
|
URL_PATH = '/legion/bootstrap.json'
|
|
@@ -28,7 +31,7 @@ module Legion
|
|
|
28
31
|
Resolv.getaddress(@hostname)
|
|
29
32
|
true
|
|
30
33
|
rescue Resolv::ResolvError, Resolv::ResolvTimeout => e
|
|
31
|
-
|
|
34
|
+
log.debug("Legion::Settings::DnsBootstrap#resolve? could not resolve #{@hostname}: #{e.message}")
|
|
32
35
|
false
|
|
33
36
|
end
|
|
34
37
|
|
|
@@ -45,7 +48,7 @@ module Legion
|
|
|
45
48
|
|
|
46
49
|
::JSON.parse(response.body, symbolize_names: true)
|
|
47
50
|
rescue StandardError => e
|
|
48
|
-
|
|
51
|
+
log.warn("DNS bootstrap fetch failed for #{@url}: #{e.message}")
|
|
49
52
|
nil
|
|
50
53
|
end
|
|
51
54
|
|
|
@@ -67,11 +70,11 @@ module Legion
|
|
|
67
70
|
return nil unless File.exist?(@cache_path)
|
|
68
71
|
|
|
69
72
|
raw = ::JSON.parse(File.read(@cache_path), symbolize_names: true)
|
|
70
|
-
|
|
73
|
+
log.debug("DNS bootstrap cache hit: #{@cache_path}")
|
|
71
74
|
raw.delete(:_dns_bootstrap_meta)
|
|
72
75
|
raw
|
|
73
76
|
rescue ::JSON::ParserError
|
|
74
|
-
|
|
77
|
+
log.warn("DNS bootstrap cache corrupt, deleting: #{@cache_path}")
|
|
75
78
|
FileUtils.rm_f(@cache_path)
|
|
76
79
|
nil
|
|
77
80
|
end
|
|
@@ -82,12 +85,9 @@ module Legion
|
|
|
82
85
|
|
|
83
86
|
private
|
|
84
87
|
|
|
85
|
-
def
|
|
86
|
-
Legion::
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def log_warn(message)
|
|
90
|
-
defined?(Legion::Logging) ? Legion::Logging.warn(message) : warn(message)
|
|
88
|
+
def resolve_logger_settings
|
|
89
|
+
raw_logging = Legion::Settings.loader&.settings&.dig(:logging) if Legion::Settings.respond_to?(:loader)
|
|
90
|
+
raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require 'resolv'
|
|
4
4
|
require 'socket'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
require 'legion/logging'
|
|
5
8
|
require 'legion/settings/os'
|
|
6
9
|
require_relative 'dns_bootstrap'
|
|
7
10
|
|
|
@@ -9,6 +12,7 @@ module Legion
|
|
|
9
12
|
module Settings
|
|
10
13
|
class Loader
|
|
11
14
|
include Legion::Settings::OS
|
|
15
|
+
include Legion::Logging::Helper
|
|
12
16
|
|
|
13
17
|
class Error < RuntimeError; end
|
|
14
18
|
attr_reader :warnings, :errors, :loaded_files, :settings
|
|
@@ -36,6 +40,7 @@ module Legion
|
|
|
36
40
|
@settings = default_settings
|
|
37
41
|
@indifferent_access = false
|
|
38
42
|
@loaded_files = []
|
|
43
|
+
log.debug('Initialized Legion::Settings::Loader with default settings')
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
def dns_defaults
|
|
@@ -138,16 +143,14 @@ module Legion
|
|
|
138
143
|
reload: false,
|
|
139
144
|
reloading: false,
|
|
140
145
|
auto_install_missing_lex: true,
|
|
141
|
-
default_extension_settings: {
|
|
142
|
-
logger: { level: 'info', trace: false, extended: false }
|
|
143
|
-
},
|
|
146
|
+
default_extension_settings: {},
|
|
144
147
|
logging: logging_defaults,
|
|
145
148
|
absorbers: absorbers_defaults,
|
|
146
149
|
transport: { connected: false },
|
|
147
150
|
data: { connected: false },
|
|
148
151
|
role: { profile: nil, extensions: [] },
|
|
149
152
|
region: { current: nil, primary: nil, failover: nil, peers: [],
|
|
150
|
-
default_affinity: '
|
|
153
|
+
default_affinity: 'any', data_residency: {} },
|
|
151
154
|
process: { role: 'full' },
|
|
152
155
|
dns: dns_defaults
|
|
153
156
|
}
|
|
@@ -171,7 +174,7 @@ module Legion
|
|
|
171
174
|
|
|
172
175
|
def []=(key, value)
|
|
173
176
|
@settings[key] = value
|
|
174
|
-
|
|
177
|
+
mark_dirty!
|
|
175
178
|
end
|
|
176
179
|
|
|
177
180
|
def hexdigest
|
|
@@ -220,16 +223,14 @@ module Legion
|
|
|
220
223
|
mod_name = config.keys.first
|
|
221
224
|
log_debug("Loading module settings: #{mod_name}")
|
|
222
225
|
@settings = deep_merge(config, @settings)
|
|
223
|
-
|
|
226
|
+
mark_dirty!
|
|
224
227
|
end
|
|
225
228
|
|
|
226
229
|
def load_module_default(config)
|
|
227
230
|
mod_name = config.keys.first
|
|
228
231
|
log_debug("Loading module defaults: #{mod_name}")
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
@settings = merged
|
|
232
|
-
@indifferent_access = false
|
|
232
|
+
@settings = deep_merge(config, @settings)
|
|
233
|
+
mark_dirty!
|
|
233
234
|
end
|
|
234
235
|
|
|
235
236
|
def load_file(file)
|
|
@@ -238,11 +239,10 @@ module Legion
|
|
|
238
239
|
begin
|
|
239
240
|
contents = read_config_file(file)
|
|
240
241
|
config = contents.empty? ? {} : Legion::JSON.load(contents)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
@settings = merged
|
|
244
|
-
# @indifferent_access = false
|
|
242
|
+
@settings = deep_merge(@settings, config)
|
|
243
|
+
mark_dirty!
|
|
245
244
|
@loaded_files << file
|
|
245
|
+
log.debug("Loaded settings file #{file}")
|
|
246
246
|
rescue Legion::JSON::ParseError => e
|
|
247
247
|
log_error("config file must be valid json: #{file}")
|
|
248
248
|
log_error(" parse error: #{e.message}")
|
|
@@ -255,7 +255,7 @@ module Legion
|
|
|
255
255
|
def load_directory(directory)
|
|
256
256
|
path = directory.gsub(/\\(?=\S)/, '/')
|
|
257
257
|
if File.readable?(path) && File.executable?(path)
|
|
258
|
-
files = Dir.glob(File.join(path, '**
|
|
258
|
+
files = Dir.glob(File.join(path, '**', '*.json'))
|
|
259
259
|
files.each { |file| load_file(file) }
|
|
260
260
|
log_info("Settings: loaded directory #{path} (#{files.size} files)")
|
|
261
261
|
else
|
|
@@ -268,7 +268,7 @@ module Legion
|
|
|
268
268
|
if @settings[:client][:subscriptions].is_a?(Array)
|
|
269
269
|
@settings[:client][:subscriptions] << "client:#{@settings[:client][:name]}"
|
|
270
270
|
@settings[:client][:subscriptions].uniq!
|
|
271
|
-
|
|
271
|
+
mark_dirty!
|
|
272
272
|
else
|
|
273
273
|
log_warn('unable to apply legion client overrides, reason: client subscriptions is not an array')
|
|
274
274
|
end
|
|
@@ -290,6 +290,11 @@ module Legion
|
|
|
290
290
|
|
|
291
291
|
private
|
|
292
292
|
|
|
293
|
+
def resolve_logger_settings
|
|
294
|
+
raw_logging = instance_variable_defined?(:@settings) ? @settings&.[](:logging) : nil
|
|
295
|
+
raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
|
|
296
|
+
end
|
|
297
|
+
|
|
293
298
|
def load_dns_from_cache(bootstrap)
|
|
294
299
|
config = bootstrap.read_cache
|
|
295
300
|
start_dns_background_refresh(bootstrap) if config
|
|
@@ -311,7 +316,7 @@ module Legion
|
|
|
311
316
|
hostname: bootstrap.hostname,
|
|
312
317
|
url: bootstrap.url
|
|
313
318
|
}
|
|
314
|
-
|
|
319
|
+
mark_dirty!
|
|
315
320
|
end
|
|
316
321
|
|
|
317
322
|
def start_dns_background_refresh(bootstrap)
|
|
@@ -357,14 +362,14 @@ module Legion
|
|
|
357
362
|
@settings[:api] ||= {}
|
|
358
363
|
@settings[:api][:port] = ENV['LEGION_API_PORT'].to_i
|
|
359
364
|
log_warn("using api port environment variable, api: #{@settings[:api]}")
|
|
360
|
-
|
|
365
|
+
mark_dirty!
|
|
361
366
|
end
|
|
362
367
|
|
|
363
368
|
def load_privacy_env
|
|
364
369
|
return unless ENV['LEGION_ENTERPRISE_PRIVACY'] == 'true'
|
|
365
370
|
|
|
366
371
|
@settings[:enterprise_data_privacy] = true
|
|
367
|
-
|
|
372
|
+
mark_dirty!
|
|
368
373
|
end
|
|
369
374
|
|
|
370
375
|
def read_config_file(file)
|
|
@@ -394,19 +399,6 @@ module Legion
|
|
|
394
399
|
merged
|
|
395
400
|
end
|
|
396
401
|
|
|
397
|
-
def deep_diff(hash_one, hash_two)
|
|
398
|
-
keys = hash_one.keys.concat(hash_two.keys).uniq
|
|
399
|
-
keys.each_with_object({}) do |key, diff|
|
|
400
|
-
next if hash_one[key] == hash_two[key]
|
|
401
|
-
|
|
402
|
-
diff[key] = if hash_one[key].is_a?(Hash) && hash_two[key].is_a?(Hash)
|
|
403
|
-
deep_diff(hash_one[key], hash_two[key])
|
|
404
|
-
else
|
|
405
|
-
[hash_one[key], hash_two[key]]
|
|
406
|
-
end
|
|
407
|
-
end
|
|
408
|
-
end
|
|
409
|
-
|
|
410
402
|
def create_loaded_tempfile!
|
|
411
403
|
dir = ENV['LEGION_LOADED_TEMPFILE_DIR'] || Dir.tmpdir
|
|
412
404
|
file_name = "legion_#{legion_service_name}_loaded_files"
|
|
@@ -415,6 +407,15 @@ module Legion
|
|
|
415
407
|
path
|
|
416
408
|
end
|
|
417
409
|
|
|
410
|
+
public
|
|
411
|
+
|
|
412
|
+
def mark_dirty!
|
|
413
|
+
@indifferent_access = false
|
|
414
|
+
@hexdigest = nil
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
private
|
|
418
|
+
|
|
418
419
|
def legion_service_name
|
|
419
420
|
File.basename($PROGRAM_NAME).split('-').last
|
|
420
421
|
end
|
|
@@ -422,7 +423,7 @@ module Legion
|
|
|
422
423
|
def system_hostname
|
|
423
424
|
Socket.gethostname
|
|
424
425
|
rescue StandardError => e
|
|
425
|
-
|
|
426
|
+
log_debug("Legion::Settings::Loader#system_hostname failed: #{e.message}")
|
|
426
427
|
'unknown'
|
|
427
428
|
end
|
|
428
429
|
|
|
@@ -431,7 +432,7 @@ module Legion
|
|
|
431
432
|
preferred = addresses.find { |a| rfc1918?(a.ip_address) }
|
|
432
433
|
(preferred || addresses.first)&.ip_address || 'unknown'
|
|
433
434
|
rescue StandardError => e
|
|
434
|
-
|
|
435
|
+
log_debug("Legion::Settings::Loader#system_address failed: #{e.message}")
|
|
435
436
|
'unknown'
|
|
436
437
|
end
|
|
437
438
|
|
|
@@ -442,19 +443,19 @@ module Legion
|
|
|
442
443
|
end
|
|
443
444
|
|
|
444
445
|
def log_info(message)
|
|
445
|
-
|
|
446
|
+
log.info(message)
|
|
446
447
|
end
|
|
447
448
|
|
|
448
449
|
def log_debug(message)
|
|
449
|
-
|
|
450
|
+
log.debug(message)
|
|
450
451
|
end
|
|
451
452
|
|
|
452
453
|
def log_warn(message)
|
|
453
|
-
|
|
454
|
+
log.warn(message)
|
|
454
455
|
end
|
|
455
456
|
|
|
456
457
|
def log_error(message)
|
|
457
|
-
|
|
458
|
+
log.error(message)
|
|
458
459
|
end
|
|
459
460
|
|
|
460
461
|
def warning(message, data = {})
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Settings
|
|
5
7
|
# Per-project `.legionio.env` config file loader.
|
|
@@ -19,6 +21,8 @@ module Legion
|
|
|
19
21
|
# Resolution order (lowest → highest priority):
|
|
20
22
|
# global settings < .legionio.env < request overlay (#9)
|
|
21
23
|
module ProjectEnv
|
|
24
|
+
extend Legion::Logging::Helper
|
|
25
|
+
|
|
22
26
|
ENV_FILENAME = '.legionio.env'
|
|
23
27
|
|
|
24
28
|
class << self
|
|
@@ -87,6 +91,11 @@ module Legion
|
|
|
87
91
|
|
|
88
92
|
private
|
|
89
93
|
|
|
94
|
+
def resolve_logger_settings
|
|
95
|
+
raw_logging = Legion::Settings.loader&.settings&.dig(:logging) if Legion::Settings.respond_to?(:loader)
|
|
96
|
+
raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
|
|
97
|
+
end
|
|
98
|
+
|
|
90
99
|
def set_nested(hash, keys, value)
|
|
91
100
|
*parents, leaf = keys
|
|
92
101
|
target = parents.reduce(hash) do |h, k|
|
|
@@ -108,11 +117,11 @@ module Legion
|
|
|
108
117
|
end
|
|
109
118
|
|
|
110
119
|
def log_debug(message)
|
|
111
|
-
|
|
120
|
+
log.debug(message)
|
|
112
121
|
end
|
|
113
122
|
|
|
114
123
|
def log_warn(message)
|
|
115
|
-
|
|
124
|
+
log.warn(message)
|
|
116
125
|
end
|
|
117
126
|
end
|
|
118
127
|
end
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Settings
|
|
5
7
|
module Resolver
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
VAULT_PATTERN = %r{\Avault://(.+?)#(.+)\z}
|
|
7
11
|
ENV_PATTERN = %r{\Aenv://(.+)\z}
|
|
8
12
|
LEASE_PATTERN = %r{\Alease://(.+?)#(.+)\z}
|
|
@@ -81,60 +85,79 @@ module Legion
|
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
def count_vault_refs(hash)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
hash.sum
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
when Array then value.count { |v| v.is_a?(String) && v.match?(VAULT_PATTERN) }
|
|
90
|
-
when Hash then count_vault_refs(value)
|
|
91
|
-
else 0
|
|
92
|
-
end
|
|
88
|
+
case hash
|
|
89
|
+
when String then hash.match?(VAULT_PATTERN) ? 1 : 0
|
|
90
|
+
when Array then hash.sum { |value| count_vault_refs(value) }
|
|
91
|
+
when Hash then hash.sum { |_key, value| count_vault_refs(value) }
|
|
92
|
+
else 0
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def vault_connected?
|
|
97
97
|
return false unless defined?(Legion::Crypt)
|
|
98
98
|
return false unless defined?(Legion::Settings)
|
|
99
|
+
return Legion::Crypt.vault_connected? if Legion::Crypt.respond_to?(:vault_connected?)
|
|
99
100
|
|
|
100
|
-
Legion::Settings[:crypt][:vault][:connected] == true
|
|
101
|
+
Legion::Settings[:crypt][:vault][:connected] == true ||
|
|
102
|
+
(Legion::Crypt.respond_to?(:connected_clusters) && Legion::Crypt.connected_clusters.any?)
|
|
101
103
|
rescue StandardError => e
|
|
102
104
|
log_debug("Legion::Settings::Resolver#vault_connected? failed: #{e.message}")
|
|
103
105
|
false
|
|
104
106
|
end
|
|
105
107
|
|
|
106
|
-
def walk(hash, path:, &
|
|
107
|
-
hash
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
108
|
+
def walk(hash, path:, &)
|
|
109
|
+
case hash
|
|
110
|
+
when Hash
|
|
111
|
+
hash.each do |key, value|
|
|
112
|
+
current_path = path.empty? ? key.to_s : "#{path}.#{key}"
|
|
113
|
+
walk_value(hash, key, value, current_path, &)
|
|
114
|
+
end
|
|
115
|
+
when Array
|
|
116
|
+
hash.each_with_index do |value, index|
|
|
117
|
+
current_path = "#{path}[#{index}]"
|
|
118
|
+
walk_value(hash, index, value, current_path, &)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def walk_value(container, key, value, current_path, &)
|
|
124
|
+
case value
|
|
125
|
+
when Hash
|
|
126
|
+
walk(value, path: current_path, &)
|
|
127
|
+
when String
|
|
128
|
+
handle_string_value(container, key, value, current_path) { |status| yield(status) if block_given? }
|
|
129
|
+
when Array
|
|
130
|
+
handle_array_value(container, key, value, current_path) { |status| yield(status) if block_given? }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def handle_string_value(container, key, value, current_path)
|
|
135
|
+
return unless value.match?(URI_PATTERN)
|
|
136
|
+
|
|
137
|
+
resolved = resolve_single(value)
|
|
138
|
+
if resolved.nil?
|
|
139
|
+
log_warn("Settings resolver: could not resolve #{current_path} (#{value})")
|
|
140
|
+
yield(:unresolved) if block_given?
|
|
141
|
+
else
|
|
142
|
+
container[key] = resolved
|
|
143
|
+
register_lease_ref(value, current_path) if value.match?(LEASE_PATTERN)
|
|
144
|
+
yield(:resolved) if block_given?
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def handle_array_value(container, key, value, current_path, &)
|
|
149
|
+
if resolvable_chain?(value) && value.all? { |entry| !entry.is_a?(Hash) && !entry.is_a?(Array) }
|
|
150
|
+
resolved = resolve_chain(value)
|
|
151
|
+
if resolved.nil?
|
|
152
|
+
log_warn("Settings resolver: fallback chain exhausted for #{current_path}")
|
|
153
|
+
yield(:unresolved) if block_given?
|
|
154
|
+
else
|
|
155
|
+
container[key] = resolved
|
|
156
|
+
register_lease_refs_from_chain(value, current_path)
|
|
157
|
+
yield(:resolved) if block_given?
|
|
137
158
|
end
|
|
159
|
+
else
|
|
160
|
+
walk(value, path: current_path, &)
|
|
138
161
|
end
|
|
139
162
|
end
|
|
140
163
|
|
|
@@ -207,40 +230,29 @@ module Legion
|
|
|
207
230
|
end
|
|
208
231
|
|
|
209
232
|
def count_lease_refs(hash)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
hash.sum
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
when Array then value.count { |v| v.is_a?(String) && v.match?(LEASE_PATTERN) }
|
|
216
|
-
when Hash then count_lease_refs(value)
|
|
217
|
-
else 0
|
|
218
|
-
end
|
|
233
|
+
case hash
|
|
234
|
+
when String then hash.match?(LEASE_PATTERN) ? 1 : 0
|
|
235
|
+
when Array then hash.sum { |value| count_lease_refs(value) }
|
|
236
|
+
when Hash then hash.sum { |_key, value| count_lease_refs(value) }
|
|
237
|
+
else 0
|
|
219
238
|
end
|
|
220
239
|
end
|
|
221
240
|
|
|
241
|
+
def resolve_logger_settings
|
|
242
|
+
raw_logging = Legion::Settings.loader&.settings&.dig(:logging) if Legion::Settings.respond_to?(:loader)
|
|
243
|
+
raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
|
|
244
|
+
end
|
|
245
|
+
|
|
222
246
|
def log_info(message)
|
|
223
|
-
|
|
224
|
-
Legion::Logging.info(message)
|
|
225
|
-
else
|
|
226
|
-
$stdout.puts(message)
|
|
227
|
-
end
|
|
247
|
+
log.info(message)
|
|
228
248
|
end
|
|
229
249
|
|
|
230
250
|
def log_warn(message)
|
|
231
|
-
|
|
232
|
-
Legion::Logging.warn(message)
|
|
233
|
-
else
|
|
234
|
-
warn(message)
|
|
235
|
-
end
|
|
251
|
+
log.warn(message)
|
|
236
252
|
end
|
|
237
253
|
|
|
238
254
|
def log_debug(message)
|
|
239
|
-
|
|
240
|
-
Legion::Logging.debug(message)
|
|
241
|
-
else
|
|
242
|
-
$stdout.puts(message)
|
|
243
|
-
end
|
|
255
|
+
log.debug(message)
|
|
244
256
|
end
|
|
245
257
|
end
|
|
246
258
|
end
|
|
@@ -113,7 +113,14 @@ module Legion
|
|
|
113
113
|
if constraint.is_a?(Hash) && constraint.key?(:type)
|
|
114
114
|
validate_leaf(constraint, value, mod_name, current_path, errors)
|
|
115
115
|
elsif constraint.is_a?(Hash)
|
|
116
|
-
|
|
116
|
+
next if value.nil?
|
|
117
|
+
|
|
118
|
+
if value.is_a?(Hash)
|
|
119
|
+
validate_node(constraint, value, mod_name, current_path, errors)
|
|
120
|
+
else
|
|
121
|
+
errors << { module: mod_name, path: current_path,
|
|
122
|
+
message: "expected Hash, got #{value.class} (#{value.inspect})" }
|
|
123
|
+
end
|
|
117
124
|
end
|
|
118
125
|
end
|
|
119
126
|
end
|
data/lib/legion/settings.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/json'
|
|
4
|
+
require 'legion/logging'
|
|
4
5
|
require 'legion/settings/version'
|
|
5
6
|
require 'legion/json/parse_error'
|
|
6
7
|
require 'legion/settings/loader'
|
|
@@ -17,6 +18,8 @@ module Legion
|
|
|
17
18
|
class << self
|
|
18
19
|
attr_accessor :loader
|
|
19
20
|
|
|
21
|
+
include Legion::Logging::Helper
|
|
22
|
+
|
|
20
23
|
def load(options = {})
|
|
21
24
|
has_config = options[:config_file] || options[:config_dir] || options[:config_dirs]&.any?
|
|
22
25
|
|
|
@@ -51,8 +54,8 @@ module Legion
|
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
def [](key)
|
|
54
|
-
logger.info('Legion::Settings was not
|
|
55
|
-
|
|
57
|
+
logger.info('Legion::Settings was not loaded, auto-loading now') if @loader.nil?
|
|
58
|
+
load if @loader.nil?
|
|
56
59
|
overlay_val = Overlay.overlay_for(key)
|
|
57
60
|
base_val = @loader[key]
|
|
58
61
|
if overlay_val.is_a?(Hash) && base_val.is_a?(Hash)
|
|
@@ -63,15 +66,23 @@ module Legion
|
|
|
63
66
|
base_val
|
|
64
67
|
end
|
|
65
68
|
rescue NoMethodError, TypeError => e
|
|
66
|
-
|
|
69
|
+
logger.debug("Legion::Settings#[] key=#{key} failed: #{e.message}")
|
|
67
70
|
nil
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
def dig(*keys)
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
return nil if keys.empty?
|
|
75
|
+
|
|
76
|
+
logger.info('Legion::Settings was not loaded, auto-loading now') if @loader.nil?
|
|
77
|
+
load if @loader.nil?
|
|
78
|
+
|
|
79
|
+
root = self[keys.first]
|
|
80
|
+
return root if keys.length == 1
|
|
81
|
+
return nil unless root.respond_to?(:dig)
|
|
82
|
+
|
|
83
|
+
root.dig(*keys[1..])
|
|
73
84
|
rescue NoMethodError, TypeError => e
|
|
74
|
-
|
|
85
|
+
logger.debug("Legion::Settings#dig keys=#{keys.inspect} failed: #{e.message}")
|
|
75
86
|
nil
|
|
76
87
|
end
|
|
77
88
|
|
|
@@ -116,7 +127,9 @@ module Legion
|
|
|
116
127
|
# @return [String, nil] path to the loaded file, or nil if none found
|
|
117
128
|
def load_project_env(start_dir: nil)
|
|
118
129
|
ensure_loader
|
|
119
|
-
ProjectEnv.load_into(@loader.settings, start_dir: start_dir)
|
|
130
|
+
path = ProjectEnv.load_into(@loader.settings, start_dir: start_dir)
|
|
131
|
+
@loader.mark_dirty! if path
|
|
132
|
+
path
|
|
120
133
|
end
|
|
121
134
|
|
|
122
135
|
def dev_mode?
|
|
@@ -124,7 +137,7 @@ module Legion
|
|
|
124
137
|
|
|
125
138
|
Legion::Settings[:dev] ? true : false
|
|
126
139
|
rescue StandardError => e
|
|
127
|
-
|
|
140
|
+
logger.debug("Legion::Settings#dev_mode? failed: #{e.message}")
|
|
128
141
|
false
|
|
129
142
|
end
|
|
130
143
|
|
|
@@ -133,7 +146,7 @@ module Legion
|
|
|
133
146
|
|
|
134
147
|
Legion::Settings[:enterprise_data_privacy] ? true : false
|
|
135
148
|
rescue StandardError => e
|
|
136
|
-
|
|
149
|
+
logger.debug("Legion::Settings#enterprise_privacy? failed: #{e.message}")
|
|
137
150
|
false
|
|
138
151
|
end
|
|
139
152
|
|
|
@@ -180,20 +193,16 @@ module Legion
|
|
|
180
193
|
end
|
|
181
194
|
|
|
182
195
|
def logger
|
|
183
|
-
|
|
184
|
-
::Legion::Logging
|
|
185
|
-
else
|
|
186
|
-
require 'logger'
|
|
187
|
-
l = ::Logger.new($stdout)
|
|
188
|
-
l.formatter = proc do |severity, datetime, _progname, msg|
|
|
189
|
-
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S %z')}] #{severity} #{msg}\n"
|
|
190
|
-
end
|
|
191
|
-
l
|
|
192
|
-
end
|
|
196
|
+
log
|
|
193
197
|
end
|
|
194
198
|
|
|
195
199
|
private
|
|
196
200
|
|
|
201
|
+
def resolve_logger_settings
|
|
202
|
+
raw_logging = @loader&.settings&.dig(:logging)
|
|
203
|
+
raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
|
|
204
|
+
end
|
|
205
|
+
|
|
197
206
|
def deep_merge_for_overlay(base, overlay)
|
|
198
207
|
result = base.dup
|
|
199
208
|
overlay.each do |key, value|
|
|
@@ -212,6 +221,7 @@ module Legion
|
|
|
212
221
|
|
|
213
222
|
@loader = Legion::Settings::Loader.new
|
|
214
223
|
@loader.load_env
|
|
224
|
+
logger.debug('Initialized Legion::Settings loader without config files')
|
|
215
225
|
@loader
|
|
216
226
|
end
|
|
217
227
|
|
|
@@ -224,11 +234,7 @@ module Legion
|
|
|
224
234
|
label = count == 1 ? 'error' : 'errors'
|
|
225
235
|
message = "Legion::Settings dev mode: #{count} configuration #{label} detected (not raising):\n"
|
|
226
236
|
message += errs.map { |e| " [#{e[:module]}] #{e[:path]}: #{e[:message]}" }.join("\n")
|
|
227
|
-
|
|
228
|
-
::Legion::Logging.warn(message)
|
|
229
|
-
else
|
|
230
|
-
warn(message)
|
|
231
|
-
end
|
|
237
|
+
logger.warn(message)
|
|
232
238
|
end
|
|
233
239
|
|
|
234
240
|
def validate_module_on_merge(mod_name)
|
|
@@ -240,6 +246,7 @@ module Legion
|
|
|
240
246
|
end
|
|
241
247
|
|
|
242
248
|
def revalidate_all_modules
|
|
249
|
+
@loader.errors.clear
|
|
243
250
|
schema.registered_modules.each do |mod_name|
|
|
244
251
|
values = @loader[mod_name]
|
|
245
252
|
next unless values.is_a?(Hash)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-settings
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.26
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: 1.2.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: legion-logging
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.5.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.5.0
|
|
26
40
|
description: A gem written to handle LegionIO Settings in a consistent way across
|
|
27
41
|
extensions
|
|
28
42
|
email:
|