Module: Legion::Settings::Resolver

Extended by:
Logging::Helper
Defined in:
lib/legion/settings/resolver.rb

Constant Summary collapse

VAULT_PATTERN =
%r{\Avault://(.+?)#(.+)\z}
ENV_PATTERN =
%r{\Aenv://(.+)\z}
LEASE_PATTERN =
%r{\Alease://(.+?)#(.+)\z}
URI_PATTERN =
%r{\A(?:vault|env|lease)://}

Class Method Summary collapse

Class Method Details

.count_lease_refs(hash) ⇒ Object



232
233
234
235
236
237
238
239
# File 'lib/legion/settings/resolver.rb', line 232

def count_lease_refs(hash)
  case hash
  when String then hash.match?(LEASE_PATTERN) ? 1 : 0
  when Array  then hash.sum { |value| count_lease_refs(value) }
  when Hash   then hash.sum { |_key, value| count_lease_refs(value) }
  else 0
  end
end

.count_vault_refs(hash) ⇒ Object



87
88
89
90
91
92
93
94
# File 'lib/legion/settings/resolver.rb', line 87

def count_vault_refs(hash)
  case hash
  when String then hash.match?(VAULT_PATTERN) ? 1 : 0
  when Array  then hash.sum { |value| count_vault_refs(value) }
  when Hash   then hash.sum { |_key, value| count_vault_refs(value) }
  else 0
  end
end

.handle_array_value(container, key, value, current_path) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/legion/settings/resolver.rb', line 148

def handle_array_value(container, key, value, current_path, &)
  if resolvable_chain?(value) && value.all? { |entry| !entry.is_a?(Hash) && !entry.is_a?(Array) }
    resolved = resolve_chain(value)
    if resolved.nil?
      log.warn("Settings resolver: fallback chain exhausted for #{current_path}")
      yield(:unresolved) if block_given?
    else
      container[key] = resolved
      register_lease_refs_from_chain(value, current_path)
      yield(:resolved) if block_given?
    end
  else
    walk(value, path: current_path, &)
  end
end

.handle_string_value(container, key, value, current_path) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/legion/settings/resolver.rb', line 134

def handle_string_value(container, key, value, current_path)
  return unless value.match?(URI_PATTERN)

  resolved = resolve_single(value)
  if resolved.nil?
    log.warn("Settings resolver: could not resolve #{current_path} (#{value})")
    yield(:unresolved) if block_given?
  else
    container[key] = resolved
    register_lease_ref(value, current_path) if value.match?(LEASE_PATTERN)
    yield(:resolved) if block_given?
  end
end

.has_vault_refs?(hash) ⇒ Boolean

rubocop:disable Naming/PredicatePrefix

Returns:

  • (Boolean)


83
84
85
# File 'lib/legion/settings/resolver.rb', line 83

def has_vault_refs?(hash) # rubocop:disable Naming/PredicatePrefix
  count_vault_refs(hash).positive?
end

.lease_manager_available?Boolean

Returns:

  • (Boolean)


198
199
200
201
202
203
# File 'lib/legion/settings/resolver.rb', line 198

def lease_manager_available?
  defined?(Legion::Crypt::LeaseManager)
rescue StandardError => e
  log.debug("Legion::Settings::Resolver#lease_manager_available? failed: #{e.message}")
  false
end

.register_lease_ref(value, path_string) ⇒ Object



209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/legion/settings/resolver.rb', line 209

def register_lease_ref(value, path_string)
  return unless lease_manager_available?

  m = value.match(LEASE_PATTERN)
  return unless m

  path_parts = path_string.split('.').map(&:to_sym)
  Legion::Crypt::LeaseManager.instance.register_ref(m[1], m[2], path_parts)
rescue StandardError => e
  log.debug("Legion::Settings::Resolver#register_lease_ref failed for #{path_string}: #{e.message}")
  nil
end

.register_lease_refs_from_chain(arr, path_string) ⇒ Object



222
223
224
225
226
227
228
229
230
# File 'lib/legion/settings/resolver.rb', line 222

def register_lease_refs_from_chain(arr, path_string)
  return unless lease_manager_available?

  arr.each do |entry|
    next unless entry.is_a?(String)

    register_lease_ref(entry, path_string) if entry.match?(LEASE_PATTERN)
  end
end

.resolvable_chain?(arr) ⇒ Boolean

Returns:

  • (Boolean)


205
206
207
# File 'lib/legion/settings/resolver.rb', line 205

def resolvable_chain?(arr)
  arr.any? { |v| v.is_a?(String) && v.match?(URI_PATTERN) }
end

.resolve_chain(arr) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
# File 'lib/legion/settings/resolver.rb', line 71

def resolve_chain(arr)
  arr.each do |entry|
    result = if entry.is_a?(String) && entry.match?(URI_PATTERN)
               resolve_single(entry)
             else
               entry
             end
    return result unless result.nil?
  end
  nil
end

.resolve_lease(name, key) ⇒ Object



189
190
191
192
193
194
195
196
# File 'lib/legion/settings/resolver.rb', line 189

def resolve_lease(name, key)
  return nil unless lease_manager_available?

  Legion::Crypt::LeaseManager.instance.fetch(name, key)
rescue StandardError => e
  log.debug("Settings resolver: lease fetch failed for #{name}##{key}: #{e.message}")
  nil
end

.resolve_logger_settingsObject



241
242
243
244
# File 'lib/legion/settings/resolver.rb', line 241

def resolve_logger_settings
  raw_logging = Legion::Settings.loader&.settings&.dig(:logging) if Legion::Settings.respond_to?(:loader)
  raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
end

.resolve_secrets!(settings_hash) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/legion/settings/resolver.rb', line 17

def resolve_secrets!(settings_hash)
  return settings_hash unless settings_hash.is_a?(Hash)

  @vault_available = vault_connected?
  @vault_cache     = {}

  vault_count = count_vault_refs(settings_hash)
  log.warn("Vault not connected — #{vault_count} vault:// reference(s) will not be resolved") if vault_count.positive? && !@vault_available

  lease_count = count_lease_refs(settings_hash)
  log.warn("LeaseManager not available — #{lease_count} lease:// reference(s) will not be resolved") if lease_count.positive? && !lease_manager_available?

  resolved = 0
  unresolved = 0
  walk(settings_hash, path: '') do |result|
    if result == :resolved
      resolved += 1
    elsif result == :unresolved
      unresolved += 1
    end
  end

  log.info("Settings resolver: #{resolved} resolved, #{unresolved} unresolved") if resolved.positive? || unresolved.positive?

  settings_hash
end

.resolve_single(str) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/legion/settings/resolver.rb', line 59

def resolve_single(str)
  if (m = str.match(VAULT_PATTERN))
    resolve_vault(m[1], m[2])
  elsif (m = str.match(LEASE_PATTERN))
    resolve_lease(m[1], m[2])
  elsif (m = str.match(ENV_PATTERN))
    ENV.fetch(m[1], nil)
  else
    str
  end
end

.resolve_value(value) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/legion/settings/resolver.rb', line 44

def resolve_value(value)
  case value
  when String
    return value unless value.match?(URI_PATTERN)

    resolve_single(value)
  when Array
    return value unless resolvable_chain?(value)

    resolve_chain(value)
  else
    value
  end
end

.resolve_vault(path, key) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/legion/settings/resolver.rb', line 164

def resolve_vault(path, key)
  log.debug("resolve_vault: path=#{path}, key=#{key}, vault_available=#{@vault_available}")
  return nil unless @vault_available

  @vault_cache[path] ||= begin
    log.debug("resolve_vault: calling Legion::Crypt.read(#{path.inspect})")
    result = Legion::Crypt.read(path)
    log.debug("resolve_vault: read returned #{result.nil? ? 'nil' : "keys=#{result.keys.inspect}"}")
    result
  rescue StandardError => e
    log.warn("Settings resolver: vault read failed for #{path}: #{e.class}=#{e.message}")
    nil
  end

  data = @vault_cache[path]
  unless data.is_a?(Hash)
    log.debug("resolve_vault: data at #{path} is #{data.class}, returning nil")
    return nil
  end

  value = data[key.to_sym] || data[key.to_s]
  log.debug("resolve_vault: #{path}##{key} = #{value.nil? ? 'nil' : '<present>'}")
  value
end

.vault_connected?Boolean

Returns:

  • (Boolean)


96
97
98
99
100
101
102
103
104
105
106
# File 'lib/legion/settings/resolver.rb', line 96

def vault_connected?
  return false unless defined?(Legion::Crypt)
  return false unless defined?(Legion::Settings)
  return Legion::Crypt.vault_connected? if Legion::Crypt.respond_to?(:vault_connected?)

  Legion::Settings[:crypt][:vault][:connected] == true ||
    (Legion::Crypt.respond_to?(:connected_clusters) && Legion::Crypt.connected_clusters.any?)
rescue StandardError => e
  log.debug("Legion::Settings::Resolver#vault_connected? failed: #{e.message}")
  false
end

.walk(hash, path:) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/legion/settings/resolver.rb', line 108

def walk(hash, path:, &)
  case hash
  when Hash
    hash.each do |key, value|
      current_path = path.empty? ? key.to_s : "#{path}.#{key}"
      walk_value(hash, key, value, current_path, &)
    end
  when Array
    hash.each_with_index do |value, index|
      current_path = "#{path}[#{index}]"
      walk_value(hash, index, value, current_path, &)
    end
  end
end

.walk_value(container, key, value, current_path) ⇒ Object



123
124
125
126
127
128
129
130
131
132
# File 'lib/legion/settings/resolver.rb', line 123

def walk_value(container, key, value, current_path, &)
  case value
  when Hash
    walk(value, path: current_path, &)
  when String
    handle_string_value(container, key, value, current_path) { |status| yield(status) if block_given? }
  when Array
    handle_array_value(container, key, value, current_path) { |status| yield(status) if block_given? }
  end
end