legion-settings 1.3.25 → 1.3.27
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/.gitignore +2 -1
- data/.pre-commit-config.yaml +29 -0
- data/CHANGELOG.md +30 -0
- data/README.md +67 -14
- data/legion-settings.gemspec +1 -0
- data/lib/legion/settings/agent_loader.rb +8 -8
- data/lib/legion/settings/dns_bootstrap.rb +10 -10
- data/lib/legion/settings/helper.rb +1 -1
- data/lib/legion/settings/loader.rb +54 -67
- data/lib/legion/settings/project_env.rb +12 -11
- data/lib/legion/settings/resolver.rb +85 -85
- data/lib/legion/settings/schema.rb +8 -1
- data/lib/legion/settings/version.rb +1 -1
- data/lib/legion/settings.rb +166 -26
- data/scripts/pre-commit-rubocop.sh +39 -0
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a096c46e4b074a4e77c104f21b1dc89b953ad15370e514ccd7face8cf8a7b8f
|
|
4
|
+
data.tar.gz: 06475bdc9bf41c6219e2c61fef17006346bb57b833a9a6c992a02f1d1178047a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 30cc986020bd6c2f4c783193f65fb07b935e08ddadd1a00608a90001a8312da19e88612e99449bc283ae4d716d119f22517c37809670f50e854a8658502c645f
|
|
7
|
+
data.tar.gz: 7d41547be02a86b3afc065e8865dbe16c02ebde7b592bd7f9a83585b22d7f07cfc1fe078eb171b754f2398d9fce9942410e8b9b999e744a73ee6abf378ada1bb
|
data/.gitignore
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Standard LegionIO pre-commit configuration
|
|
2
|
+
# Install: pre-commit install
|
|
3
|
+
# Manual: pre-commit run --all-files
|
|
4
|
+
repos:
|
|
5
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
6
|
+
rev: v5.0.0
|
|
7
|
+
hooks:
|
|
8
|
+
- id: trailing-whitespace
|
|
9
|
+
- id: end-of-file-fixer
|
|
10
|
+
- id: check-yaml
|
|
11
|
+
- id: check-json
|
|
12
|
+
exclude: Gemfile\.lock
|
|
13
|
+
- id: check-merge-conflict
|
|
14
|
+
|
|
15
|
+
- repo: local
|
|
16
|
+
hooks:
|
|
17
|
+
- id: rubocop
|
|
18
|
+
name: RuboCop (autofix)
|
|
19
|
+
entry: scripts/pre-commit-rubocop.sh
|
|
20
|
+
language: script
|
|
21
|
+
types: [ruby]
|
|
22
|
+
pass_filenames: true
|
|
23
|
+
|
|
24
|
+
- id: ruby-syntax
|
|
25
|
+
name: Ruby syntax check
|
|
26
|
+
entry: ruby -c
|
|
27
|
+
language: system
|
|
28
|
+
types: [ruby]
|
|
29
|
+
pass_filenames: true
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Legion::Settings Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.27] - 2026-04-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Settings.reload!` — re-reads all previously loaded config files and re-resolves vault://, env://, and lease:// references; returns a hash of changed keys with old/new values; thread-safe via internal mutex
|
|
7
|
+
- `Settings.watch!` — registers a SIGHUP handler that triggers `reload!` in a background thread; optionally accepts a block for change notification
|
|
8
|
+
- `Settings.on_reload(&block)` — register callbacks invoked after `reload!` detects changes; multiple callbacks supported, called in order, rescue-safe
|
|
9
|
+
- Private `diff_settings` helper for deep comparison of old vs new config hashes
|
|
10
|
+
- Private `fire_reload_callbacks` for executing registered change callbacks
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `reload!` preserves programmatic module merges and reapplies `.legionio.env` overrides to the reloaded settings loader
|
|
14
|
+
- `watch!` no-ops when SIGHUP is unavailable and coalesces repeated SIGHUP events through a single reload worker
|
|
15
|
+
- Replaced deprecated helper logging method calls with direct `log.debug/info/warn/error` usage
|
|
16
|
+
|
|
17
|
+
## [1.3.26] - 2026-04-02
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Added a runtime dependency on `legion-logging >= 1.4.0` and moved `Settings`, `Loader`, `Resolver`, `ProjectEnv`, and `AgentLoader` onto component-aware logging helpers
|
|
21
|
+
- Added `Legion::Logging::Helper` integration to top-level `Settings` access and DNS bootstrap logging so component tags and per-component levels apply consistently
|
|
22
|
+
- `extensions.default_extension_settings` now defaults to `{}` instead of injecting a synthetic logger config
|
|
23
|
+
- `Legion::Settings::Helper#settings` now returns `{}` when an extension has no explicit settings
|
|
24
|
+
- `region.default_affinity` now defaults to `any`
|
|
25
|
+
- `[]`, `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
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- `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
|
|
29
|
+
- 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
|
|
30
|
+
- `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
|
|
31
|
+
- Logger-backed settings code paths now use consistent `log.debug/info/warn/error` behavior instead of mixed direct `Legion::Logging.*` calls
|
|
32
|
+
|
|
3
33
|
## [1.3.25] - 2026-03-31
|
|
4
34
|
|
|
5
35
|
### 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.27
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -21,20 +21,73 @@ 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.
|
|
46
|
+
|
|
47
|
+
### Hot Reload
|
|
48
|
+
|
|
49
|
+
`Legion::Settings.reload!` re-reads the config files that were previously loaded, reapplies module defaults and the nearest `.legionio.env`, re-resolves secret references, and returns a hash describing the changed keys.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
changes = Legion::Settings.reload!
|
|
53
|
+
|
|
54
|
+
changes
|
|
55
|
+
# {
|
|
56
|
+
# "llm.default_model" => { old: "old-model", new: "new-model" }
|
|
57
|
+
# }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Callbacks run only when changes are detected:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
Legion::Settings.on_reload do |changes|
|
|
64
|
+
Legion::Settings.logger.info("Settings changed: #{changes.keys.join(', ')}")
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`watch!` installs a SIGHUP handler when the platform supports it. Repeated signals are coalesced through one background reload worker, so rapid SIGHUP bursts do not create unbounded reload threads.
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
Legion::Settings.watch! do |changes|
|
|
72
|
+
Legion::Settings.logger.info("Reloaded #{changes.size} setting(s)")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Later, from a shell:
|
|
76
|
+
# kill -HUP <daemon_pid>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
On platforms without `HUP`, `watch!` logs and returns without raising. Direct `reload!` remains available for API endpoints, tests, or environments that use a different process-control mechanism.
|
|
80
|
+
|
|
81
|
+
### Project Environment Overrides
|
|
82
|
+
|
|
83
|
+
When present, the nearest `.legionio.env` file is loaded after base settings and module defaults. Dot notation maps to nested settings:
|
|
84
|
+
|
|
85
|
+
```dotenv
|
|
86
|
+
llm.default_model=claude-sonnet
|
|
87
|
+
cache.driver=redis
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Hot reload picks up changes to this file as part of the same `reload!` flow.
|
|
38
91
|
|
|
39
92
|
### Secret Resolution
|
|
40
93
|
|
|
@@ -76,25 +129,25 @@ Legion::Settings.validate! # raises ValidationError if any settings are invalid
|
|
|
76
129
|
|
|
77
130
|
# In development, warn instead of raising:
|
|
78
131
|
# Set LEGION_DEV=true or Legion::Settings.set_prop(:dev, true)
|
|
79
|
-
# validate! will warn
|
|
132
|
+
# validate! will warn through the configured logger instead of raising
|
|
80
133
|
```
|
|
81
134
|
|
|
82
135
|
### Logging Defaults
|
|
83
136
|
|
|
84
|
-
The `logging` key includes a `transport` sub-section
|
|
137
|
+
The `logging` key includes a `transport` sub-section that controls whether log events are forwarded over the message bus:
|
|
85
138
|
|
|
86
139
|
```json
|
|
87
140
|
{
|
|
88
141
|
"logging": {
|
|
89
142
|
"level": "info",
|
|
90
143
|
"format": "text",
|
|
91
|
-
"log_file":
|
|
144
|
+
"log_file": "./legionio/logs/legion.log",
|
|
92
145
|
"log_stdout": true,
|
|
93
146
|
"trace": true,
|
|
94
147
|
"async": true,
|
|
95
148
|
"include_pid": false,
|
|
96
149
|
"transport": {
|
|
97
|
-
"enabled":
|
|
150
|
+
"enabled": true,
|
|
98
151
|
"forward_logs": true,
|
|
99
152
|
"forward_exceptions": true
|
|
100
153
|
}
|
|
@@ -102,7 +155,7 @@ The `logging` key includes a `transport` sub-section (new in 1.3.22) that contro
|
|
|
102
155
|
}
|
|
103
156
|
```
|
|
104
157
|
|
|
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.
|
|
158
|
+
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
159
|
|
|
107
160
|
## Requirements
|
|
108
161
|
|
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
|
|
|
@@ -17,7 +20,7 @@ module Legion
|
|
|
17
20
|
definition = load_file(path)
|
|
18
21
|
next unless definition && valid?(definition)
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
log.debug("Agent loaded: #{definition[:name]} (#{path})")
|
|
21
24
|
definition.merge(_source_path: path, _source_mtime: File.mtime(path))
|
|
22
25
|
end
|
|
23
26
|
end
|
|
@@ -29,7 +32,7 @@ module Legion
|
|
|
29
32
|
when '.json' then ::JSON.parse(content, symbolize_names: true)
|
|
30
33
|
end
|
|
31
34
|
rescue StandardError => e
|
|
32
|
-
|
|
35
|
+
log.warn("Failed to parse agent file #{path}: #{e.message}")
|
|
33
36
|
nil
|
|
34
37
|
end
|
|
35
38
|
|
|
@@ -44,12 +47,9 @@ module Legion
|
|
|
44
47
|
|
|
45
48
|
private
|
|
46
49
|
|
|
47
|
-
def
|
|
48
|
-
Legion::
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def log_warn(message)
|
|
52
|
-
defined?(Legion::Logging) ? Legion::Logging.warn(message) : warn(message)
|
|
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
53
|
end
|
|
54
54
|
end
|
|
55
55
|
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,9 +12,10 @@ 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
|
-
attr_reader :warnings, :errors, :loaded_files, :settings
|
|
18
|
+
attr_reader :warnings, :errors, :loaded_files, :settings, :merged_modules
|
|
15
19
|
|
|
16
20
|
def self.default_directories
|
|
17
21
|
env_dirs = ENV.fetch('LEGION_SETTINGS_DIRS', nil)
|
|
@@ -36,6 +40,8 @@ module Legion
|
|
|
36
40
|
@settings = default_settings
|
|
37
41
|
@indifferent_access = false
|
|
38
42
|
@loaded_files = []
|
|
43
|
+
@merged_modules = {}
|
|
44
|
+
log.debug('Initialized Legion::Settings::Loader with default settings')
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def dns_defaults
|
|
@@ -138,16 +144,14 @@ module Legion
|
|
|
138
144
|
reload: false,
|
|
139
145
|
reloading: false,
|
|
140
146
|
auto_install_missing_lex: true,
|
|
141
|
-
default_extension_settings: {
|
|
142
|
-
logger: { level: 'info', trace: false, extended: false }
|
|
143
|
-
},
|
|
147
|
+
default_extension_settings: {},
|
|
144
148
|
logging: logging_defaults,
|
|
145
149
|
absorbers: absorbers_defaults,
|
|
146
150
|
transport: { connected: false },
|
|
147
151
|
data: { connected: false },
|
|
148
152
|
role: { profile: nil, extensions: [] },
|
|
149
153
|
region: { current: nil, primary: nil, failover: nil, peers: [],
|
|
150
|
-
default_affinity: '
|
|
154
|
+
default_affinity: 'any', data_residency: {} },
|
|
151
155
|
process: { role: 'full' },
|
|
152
156
|
dns: dns_defaults
|
|
153
157
|
}
|
|
@@ -171,7 +175,7 @@ module Legion
|
|
|
171
175
|
|
|
172
176
|
def []=(key, value)
|
|
173
177
|
@settings[key] = value
|
|
174
|
-
|
|
178
|
+
mark_dirty!
|
|
175
179
|
end
|
|
176
180
|
|
|
177
181
|
def hexdigest
|
|
@@ -218,46 +222,44 @@ module Legion
|
|
|
218
222
|
|
|
219
223
|
def load_module_settings(config)
|
|
220
224
|
mod_name = config.keys.first
|
|
221
|
-
|
|
225
|
+
log.debug("Loading module settings: #{mod_name}")
|
|
226
|
+
@merged_modules = deep_merge(@merged_modules, config)
|
|
222
227
|
@settings = deep_merge(config, @settings)
|
|
223
|
-
|
|
228
|
+
mark_dirty!
|
|
224
229
|
end
|
|
225
230
|
|
|
226
231
|
def load_module_default(config)
|
|
227
232
|
mod_name = config.keys.first
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
@settings = merged
|
|
232
|
-
@indifferent_access = false
|
|
233
|
+
log.debug("Loading module defaults: #{mod_name}")
|
|
234
|
+
@settings = deep_merge(config, @settings)
|
|
235
|
+
mark_dirty!
|
|
233
236
|
end
|
|
234
237
|
|
|
235
238
|
def load_file(file)
|
|
236
|
-
|
|
239
|
+
log.debug("Trying to load file #{file}")
|
|
237
240
|
if File.file?(file) && File.readable?(file)
|
|
238
241
|
begin
|
|
239
242
|
contents = read_config_file(file)
|
|
240
243
|
config = contents.empty? ? {} : Legion::JSON.load(contents)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
@settings = merged
|
|
244
|
-
# @indifferent_access = false
|
|
244
|
+
@settings = deep_merge(@settings, config)
|
|
245
|
+
mark_dirty!
|
|
245
246
|
@loaded_files << file
|
|
247
|
+
log.debug("Loaded settings file #{file}")
|
|
246
248
|
rescue Legion::JSON::ParseError => e
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
log.error("config file must be valid json: #{file}")
|
|
250
|
+
log.error(" parse error: #{e.message}")
|
|
249
251
|
end
|
|
250
252
|
else
|
|
251
|
-
|
|
253
|
+
log.warn("Config file does not exist or is not readable file:#{file}")
|
|
252
254
|
end
|
|
253
255
|
end
|
|
254
256
|
|
|
255
257
|
def load_directory(directory)
|
|
256
258
|
path = directory.gsub(/\\(?=\S)/, '/')
|
|
257
259
|
if File.readable?(path) && File.executable?(path)
|
|
258
|
-
files = Dir.glob(File.join(path, '**
|
|
260
|
+
files = Dir.glob(File.join(path, '**', '*.json'))
|
|
259
261
|
files.each { |file| load_file(file) }
|
|
260
|
-
|
|
262
|
+
log.info("Settings: loaded directory #{path} (#{files.size} files)")
|
|
261
263
|
else
|
|
262
264
|
load_error('insufficient permissions for loading', directory: directory)
|
|
263
265
|
end
|
|
@@ -268,9 +270,9 @@ module Legion
|
|
|
268
270
|
if @settings[:client][:subscriptions].is_a?(Array)
|
|
269
271
|
@settings[:client][:subscriptions] << "client:#{@settings[:client][:name]}"
|
|
270
272
|
@settings[:client][:subscriptions].uniq!
|
|
271
|
-
|
|
273
|
+
mark_dirty!
|
|
272
274
|
else
|
|
273
|
-
|
|
275
|
+
log.warn('unable to apply legion client overrides, reason: client subscriptions is not an array')
|
|
274
276
|
end
|
|
275
277
|
end
|
|
276
278
|
|
|
@@ -290,6 +292,11 @@ module Legion
|
|
|
290
292
|
|
|
291
293
|
private
|
|
292
294
|
|
|
295
|
+
def resolve_logger_settings
|
|
296
|
+
raw_logging = instance_variable_defined?(:@settings) ? @settings&.[](:logging) : nil
|
|
297
|
+
raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
|
|
298
|
+
end
|
|
299
|
+
|
|
293
300
|
def load_dns_from_cache(bootstrap)
|
|
294
301
|
config = bootstrap.read_cache
|
|
295
302
|
start_dns_background_refresh(bootstrap) if config
|
|
@@ -297,7 +304,7 @@ module Legion
|
|
|
297
304
|
end
|
|
298
305
|
|
|
299
306
|
def load_dns_first_boot(bootstrap)
|
|
300
|
-
|
|
307
|
+
log.debug("DNS bootstrap: first boot, fetching from #{bootstrap.url}")
|
|
301
308
|
config = bootstrap.fetch
|
|
302
309
|
bootstrap.write_cache(config) if config
|
|
303
310
|
config
|
|
@@ -311,7 +318,7 @@ module Legion
|
|
|
311
318
|
hostname: bootstrap.hostname,
|
|
312
319
|
url: bootstrap.url
|
|
313
320
|
}
|
|
314
|
-
|
|
321
|
+
mark_dirty!
|
|
315
322
|
end
|
|
316
323
|
|
|
317
324
|
def start_dns_background_refresh(bootstrap)
|
|
@@ -319,7 +326,7 @@ module Legion
|
|
|
319
326
|
fresh = bootstrap.fetch
|
|
320
327
|
bootstrap.write_cache(fresh) if fresh
|
|
321
328
|
rescue StandardError => e
|
|
322
|
-
|
|
329
|
+
log.warn("DNS background refresh failed: #{e.message}")
|
|
323
330
|
end
|
|
324
331
|
end
|
|
325
332
|
|
|
@@ -356,15 +363,15 @@ module Legion
|
|
|
356
363
|
|
|
357
364
|
@settings[:api] ||= {}
|
|
358
365
|
@settings[:api][:port] = ENV['LEGION_API_PORT'].to_i
|
|
359
|
-
|
|
360
|
-
|
|
366
|
+
log.warn("using api port environment variable, api: #{@settings[:api]}")
|
|
367
|
+
mark_dirty!
|
|
361
368
|
end
|
|
362
369
|
|
|
363
370
|
def load_privacy_env
|
|
364
371
|
return unless ENV['LEGION_ENTERPRISE_PRIVACY'] == 'true'
|
|
365
372
|
|
|
366
373
|
@settings[:enterprise_data_privacy] = true
|
|
367
|
-
|
|
374
|
+
mark_dirty!
|
|
368
375
|
end
|
|
369
376
|
|
|
370
377
|
def read_config_file(file)
|
|
@@ -394,19 +401,6 @@ module Legion
|
|
|
394
401
|
merged
|
|
395
402
|
end
|
|
396
403
|
|
|
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
404
|
def create_loaded_tempfile!
|
|
411
405
|
dir = ENV['LEGION_LOADED_TEMPFILE_DIR'] || Dir.tmpdir
|
|
412
406
|
file_name = "legion_#{legion_service_name}_loaded_files"
|
|
@@ -415,6 +409,15 @@ module Legion
|
|
|
415
409
|
path
|
|
416
410
|
end
|
|
417
411
|
|
|
412
|
+
public
|
|
413
|
+
|
|
414
|
+
def mark_dirty!
|
|
415
|
+
@indifferent_access = false
|
|
416
|
+
@hexdigest = nil
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
private
|
|
420
|
+
|
|
418
421
|
def legion_service_name
|
|
419
422
|
File.basename($PROGRAM_NAME).split('-').last
|
|
420
423
|
end
|
|
@@ -422,7 +425,7 @@ module Legion
|
|
|
422
425
|
def system_hostname
|
|
423
426
|
Socket.gethostname
|
|
424
427
|
rescue StandardError => e
|
|
425
|
-
|
|
428
|
+
log.debug("Legion::Settings::Loader#system_hostname failed: #{e.message}")
|
|
426
429
|
'unknown'
|
|
427
430
|
end
|
|
428
431
|
|
|
@@ -431,7 +434,7 @@ module Legion
|
|
|
431
434
|
preferred = addresses.find { |a| rfc1918?(a.ip_address) }
|
|
432
435
|
(preferred || addresses.first)&.ip_address || 'unknown'
|
|
433
436
|
rescue StandardError => e
|
|
434
|
-
|
|
437
|
+
log.debug("Legion::Settings::Loader#system_address failed: #{e.message}")
|
|
435
438
|
'unknown'
|
|
436
439
|
end
|
|
437
440
|
|
|
@@ -441,34 +444,18 @@ module Legion
|
|
|
441
444
|
ip.start_with?('192.168.')
|
|
442
445
|
end
|
|
443
446
|
|
|
444
|
-
def log_info(message)
|
|
445
|
-
defined?(Legion::Logging) ? Legion::Logging.info(message) : $stdout.puts(message)
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def log_debug(message)
|
|
449
|
-
Legion::Logging.debug(message) if defined?(Legion::Logging)
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
def log_warn(message)
|
|
453
|
-
defined?(Legion::Logging) ? Legion::Logging.warn(message) : warn(message)
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def log_error(message)
|
|
457
|
-
defined?(Legion::Logging) ? Legion::Logging.error(message) : warn(message)
|
|
458
|
-
end
|
|
459
|
-
|
|
460
447
|
def warning(message, data = {})
|
|
461
448
|
@warnings << {
|
|
462
449
|
message: message
|
|
463
450
|
}.merge(data)
|
|
464
|
-
|
|
451
|
+
log.warn(message)
|
|
465
452
|
end
|
|
466
453
|
|
|
467
454
|
def load_error(message, data = {})
|
|
468
455
|
@errors << {
|
|
469
456
|
message: message
|
|
470
457
|
}.merge(data)
|
|
471
|
-
|
|
458
|
+
log.error(message)
|
|
472
459
|
raise(Error, message)
|
|
473
460
|
end
|
|
474
461
|
|
|
@@ -479,7 +466,7 @@ module Legion
|
|
|
479
466
|
nameservers: config[:nameserver]&.map(&:to_s)&.uniq
|
|
480
467
|
}
|
|
481
468
|
rescue StandardError => e
|
|
482
|
-
|
|
469
|
+
log.warn("Failed to read resolv config: #{e.message}")
|
|
483
470
|
{ search_domains: [], nameservers: [] }
|
|
484
471
|
end
|
|
485
472
|
|
|
@@ -490,10 +477,10 @@ module Legion
|
|
|
490
477
|
|
|
491
478
|
fqdn.include?('.') ? fqdn : nil
|
|
492
479
|
rescue Timeout::Error
|
|
493
|
-
|
|
480
|
+
log.debug('FQDN detection skipped (DNS timeout)')
|
|
494
481
|
nil
|
|
495
482
|
rescue StandardError => e
|
|
496
|
-
|
|
483
|
+
log.debug("FQDN detection skipped (#{e.message.split(':').first})")
|
|
497
484
|
nil
|
|
498
485
|
end
|
|
499
486
|
end
|