legion-settings 1.3.4 → 1.3.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f66aca7decaa9e60f84abf3d4b2538f42a90978289916f535395991161bb8e24
4
- data.tar.gz: 41c5f1bac7ec1e12d0034ad872816405588d880542efbe7c420ec958697f1991
3
+ metadata.gz: 220738db670fe9bbda99acc5d64d903de88268afdc83bb0ef6c482f550b7b769
4
+ data.tar.gz: b46f3ff6ed58a9a7a0269230814ea2a9204953970c93877747c5d28c23410c8b
5
5
  SHA512:
6
- metadata.gz: 967b78c6ca4e9e2bee7a2ac1e53c6198a0b0606985e16fca8734c061be6b5b5b4f30e7ed882c387af00554f3323e9008269e036eaec09e9b7ee12d1e1c8ad349
7
- data.tar.gz: 2451834575d97a57f1f11c41556f48cf581358624c793d8ac011158b174df2072988d1135af0b9fc3656d1fd608734ab155a2f5b7419d71f77a613e5d8677994
6
+ metadata.gz: 2e8a71bf3705c3f2a809b454c1bc676f7ab1f7c2e3c2dd1b3d6b317f5126ba84bde201b75a1f65e5111d31f1b67408e173eef8da3683d4d19ee48af83c69bdc0
7
+ data.tar.gz: a537874627f4d55ee13bb3ec4cbce0e869337a15c0f6c1bfd012a16f081c79f8bb7a56f39d7431c7eb6c1e6e525cda8a98155f534bfac32177a135b1aa96e7cd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Legion::Settings Changelog
2
2
 
3
+ ## [1.3.5] - 2026-03-19
4
+
5
+ ### Added
6
+ - DNS-based bootstrap discovery: auto-detect corporate config from `legion-bootstrap.<search-domain>`
7
+ - `Settings[:dns]` populated with FQDN, default domain, search domains, and nameservers
8
+ - `DnsBootstrap` class with DNS resolution, HTTPS fetch, local caching, and background refresh
9
+ - First boot blocks on fetch; subsequent boots use cache with async refresh
10
+ - Opt-out via `LEGION_DNS_BOOTSTRAP=false` environment variable
11
+
3
12
  ## [1.3.4] - 2026-03-18
4
13
 
5
14
  ### Fixed
data/CLAUDE.md CHANGED
@@ -8,6 +8,7 @@
8
8
  Hash-like configuration store for the LegionIO framework. Loads settings from JSON files, directories, and environment variables. Provides a unified `Legion::Settings[:key]` accessor used by all other Legion gems. Includes schema-based validation with type inference, enum constraints, and cross-module checks.
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/legion-settings
11
+ **Version**: 1.3.4
11
12
  **License**: Apache-2.0
12
13
 
13
14
  ## Architecture
data/README.md CHANGED
@@ -2,6 +2,8 @@
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.4
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'resolv'
7
+ require 'uri'
8
+
9
+ module Legion
10
+ module Settings
11
+ class DnsBootstrap
12
+ CACHE_FILENAME = '_dns_bootstrap.json'
13
+ HOSTNAME_PREFIX = 'legion-bootstrap'
14
+ URL_PATH = '/legion/bootstrap.json'
15
+ HTTP_TIMEOUT = 10
16
+
17
+ attr_reader :default_domain, :hostname, :url, :cache_path
18
+
19
+ def initialize(default_domain:, cache_dir: nil)
20
+ @default_domain = default_domain
21
+ @hostname = "#{HOSTNAME_PREFIX}.#{default_domain}"
22
+ @url = "https://#{@hostname}#{URL_PATH}"
23
+ dir = cache_dir || File.expand_path('~/.legionio/settings')
24
+ @cache_path = File.join(dir, CACHE_FILENAME)
25
+ end
26
+
27
+ def resolve?
28
+ Resolv.getaddress(@hostname)
29
+ true
30
+ rescue Resolv::ResolvError, Resolv::ResolvTimeout
31
+ false
32
+ end
33
+
34
+ def fetch
35
+ return nil unless resolve?
36
+
37
+ uri = URI.parse(@url)
38
+ http = Net::HTTP.new(uri.host, uri.port)
39
+ http.use_ssl = true
40
+ http.open_timeout = HTTP_TIMEOUT
41
+ http.read_timeout = HTTP_TIMEOUT
42
+ response = http.request(Net::HTTP::Get.new(uri))
43
+ return nil unless response.is_a?(Net::HTTPSuccess)
44
+
45
+ ::JSON.parse(response.body, symbolize_names: true)
46
+ rescue StandardError
47
+ nil
48
+ end
49
+
50
+ def write_cache(config)
51
+ FileUtils.mkdir_p(File.dirname(@cache_path))
52
+ payload = config.merge(
53
+ _dns_bootstrap_meta: {
54
+ fetched_at: Time.now.utc.iso8601,
55
+ hostname: @hostname,
56
+ url: @url
57
+ }
58
+ )
59
+ tmp = "#{@cache_path}.tmp"
60
+ File.write(tmp, ::JSON.pretty_generate(payload))
61
+ File.rename(tmp, @cache_path)
62
+ end
63
+
64
+ def read_cache
65
+ return nil unless File.exist?(@cache_path)
66
+
67
+ raw = ::JSON.parse(File.read(@cache_path), symbolize_names: true)
68
+ raw.delete(:_dns_bootstrap_meta)
69
+ raw
70
+ rescue ::JSON::ParserError
71
+ FileUtils.rm_f(@cache_path)
72
+ nil
73
+ end
74
+
75
+ def cache_exists?
76
+ File.exist?(@cache_path)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'resolv'
3
4
  require 'socket'
4
5
  require 'legion/settings/os'
6
+ require_relative 'dns_bootstrap'
5
7
 
6
8
  module Legion
7
9
  module Settings
@@ -19,6 +21,17 @@ module Legion
19
21
  @loaded_files = []
20
22
  end
21
23
 
24
+ def dns_defaults
25
+ resolv_config = read_resolv_config
26
+ {
27
+ fqdn: detect_fqdn,
28
+ default_domain: resolv_config[:search_domains]&.first,
29
+ search_domains: resolv_config[:search_domains] || [],
30
+ nameservers: resolv_config[:nameservers] || [],
31
+ bootstrap: { enabled: true }
32
+ }
33
+ end
34
+
22
35
  def client_defaults
23
36
  {
24
37
  hostname: system_hostname,
@@ -71,7 +84,8 @@ module Legion
71
84
  },
72
85
  transport: { connected: false },
73
86
  data: { connected: false },
74
- role: { profile: nil, extensions: [] }
87
+ role: { profile: nil, extensions: [] },
88
+ dns: dns_defaults
75
89
  }
76
90
  end
77
91
 
@@ -116,6 +130,27 @@ module Legion
116
130
  load_api_env
117
131
  end
118
132
 
133
+ def load_dns_bootstrap(cache_dir: nil)
134
+ return if ENV['LEGION_DNS_BOOTSTRAP'] == 'false'
135
+
136
+ domain = @settings.dig(:dns, :default_domain)
137
+ return unless domain
138
+ return unless @settings.dig(:dns, :bootstrap, :enabled)
139
+
140
+ dir = cache_dir || File.expand_path('~/.legionio/settings')
141
+ bootstrap = DnsBootstrap.new(default_domain: domain, cache_dir: dir)
142
+
143
+ config = if bootstrap.cache_exists?
144
+ load_dns_from_cache(bootstrap)
145
+ else
146
+ load_dns_first_boot(bootstrap)
147
+ end
148
+
149
+ return unless config
150
+
151
+ merge_dns_config(config, bootstrap)
152
+ end
153
+
119
154
  def load_module_settings(config)
120
155
  @settings = deep_merge(config, @settings)
121
156
  end
@@ -184,6 +219,39 @@ module Legion
184
219
 
185
220
  private
186
221
 
222
+ def load_dns_from_cache(bootstrap)
223
+ config = bootstrap.read_cache
224
+ start_dns_background_refresh(bootstrap) if config
225
+ config
226
+ end
227
+
228
+ def load_dns_first_boot(bootstrap)
229
+ Legion::Logging.debug("DNS bootstrap: first boot, fetching from #{bootstrap.url}")
230
+ config = bootstrap.fetch
231
+ bootstrap.write_cache(config) if config
232
+ config
233
+ end
234
+
235
+ def merge_dns_config(config, bootstrap)
236
+ @settings = deep_merge(config, @settings)
237
+ @settings[:dns] ||= {}
238
+ @settings[:dns][:corp_bootstrap] = {
239
+ discovered: true,
240
+ hostname: bootstrap.hostname,
241
+ url: bootstrap.url
242
+ }
243
+ @indifferent_access = false
244
+ end
245
+
246
+ def start_dns_background_refresh(bootstrap)
247
+ Thread.new do
248
+ fresh = bootstrap.fetch
249
+ bootstrap.write_cache(fresh) if fresh
250
+ rescue StandardError
251
+ # Background refresh is best-effort
252
+ end
253
+ end
254
+
187
255
  def setting_category(category)
188
256
  @settings[category].map do |name, details|
189
257
  details.merge(name: name.to_s)
@@ -301,6 +369,25 @@ module Legion
301
369
  Legion::Logging.error(message)
302
370
  raise(Error, message)
303
371
  end
372
+
373
+ def read_resolv_config
374
+ config = Resolv::DNS::Config.default_config_hash
375
+ {
376
+ search_domains: config[:search]&.map(&:to_s)&.uniq,
377
+ nameservers: config[:nameserver]&.map(&:to_s)&.uniq
378
+ }
379
+ rescue StandardError
380
+ { search_domains: [], nameservers: [] }
381
+ end
382
+
383
+ def detect_fqdn
384
+ fqdn = Addrinfo.getaddrinfo(Socket.gethostname, nil).first&.canonname
385
+ return nil if fqdn.nil?
386
+
387
+ fqdn.include?('.') ? fqdn : nil
388
+ rescue StandardError
389
+ nil
390
+ end
304
391
  end
305
392
  end
306
393
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Settings
5
- VERSION = '1.3.4'
5
+ VERSION = '1.3.5'
6
6
  end
7
7
  end
@@ -17,6 +17,7 @@ module Legion
17
17
  def load(options = {})
18
18
  @loader = Legion::Settings::Loader.new
19
19
  @loader.load_env
20
+ @loader.load_dns_bootstrap
20
21
  @loader.load_file(options[:config_file]) if options[:config_file]
21
22
  @loader.load_directory(options[:config_dir]) if options[:config_dir]
22
23
  options[:config_dirs]&.each do |directory|
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
4
+ version: 1.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -46,6 +46,7 @@ files:
46
46
  - docs/plans/2026-03-17-config-error-filename-implementation.md
47
47
  - legion-settings.gemspec
48
48
  - lib/legion/settings.rb
49
+ - lib/legion/settings/dns_bootstrap.rb
49
50
  - lib/legion/settings/loader.rb
50
51
  - lib/legion/settings/os.rb
51
52
  - lib/legion/settings/resolver.rb