legion-crypt 1.5.12 → 1.5.37
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/.rubocop.yml +3 -0
- data/CHANGELOG.md +146 -1
- data/README.md +68 -14
- data/legion-crypt.gemspec +2 -1
- data/lib/legion/crypt/cert_rotation.rb +83 -65
- data/lib/legion/crypt/cipher.rb +5 -15
- data/lib/legion/crypt/cluster_secret.rb +71 -19
- data/lib/legion/crypt/erasure.rb +3 -10
- data/lib/legion/crypt/jwks_client.rb +43 -31
- data/lib/legion/crypt/jwt.rb +8 -7
- data/lib/legion/crypt/kerberos_auth.rb +18 -8
- data/lib/legion/crypt/ldap_auth.rb +14 -13
- data/lib/legion/crypt/lease_manager.rb +485 -335
- data/lib/legion/crypt/mtls.rb +3 -6
- data/lib/legion/crypt/partition_keys.rb +2 -0
- data/lib/legion/crypt/settings.rb +2 -1
- data/lib/legion/crypt/spiffe/workload_api_client.rb +83 -71
- data/lib/legion/crypt/token_renewer.rb +212 -82
- data/lib/legion/crypt/vault.rb +113 -83
- data/lib/legion/crypt/vault_client.rb +355 -0
- data/lib/legion/crypt/vault_cluster.rb +161 -39
- data/lib/legion/crypt/vault_entity.rb +21 -34
- data/lib/legion/crypt/vault_jwt_auth.rb +9 -20
- data/lib/legion/crypt/vault_kerberos_auth.rb +11 -14
- data/lib/legion/crypt/vault_operation.rb +27 -0
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +116 -63
- metadata +23 -9
- data/lib/legion/logging/helper.rb +0 -101
- data/lib/legion/logging.rb +0 -58
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
4
|
require 'singleton'
|
|
5
5
|
require 'timeout'
|
|
6
|
+
require 'legion/crypt/vault_operation'
|
|
6
7
|
|
|
7
8
|
module Legion
|
|
8
9
|
module Crypt
|
|
@@ -11,188 +12,160 @@ module Legion
|
|
|
11
12
|
include Legion::Logging::Helper
|
|
12
13
|
|
|
13
14
|
RENEWAL_CHECK_INTERVAL = 5
|
|
15
|
+
MINIMUM_SLEEP = 0.01
|
|
16
|
+
MAX_RETRY_DELAY = 60
|
|
14
17
|
|
|
15
18
|
def initialize
|
|
16
19
|
@lease_cache = {}
|
|
17
20
|
@active_leases = {}
|
|
21
|
+
@owned_leases = {}
|
|
22
|
+
@pending = {}
|
|
18
23
|
@refs = {}
|
|
24
|
+
@lease_locks = {}
|
|
25
|
+
@state_mutex = Mutex.new
|
|
26
|
+
@wake = ConditionVariable.new
|
|
19
27
|
@running = false
|
|
20
28
|
@renewal_thread = nil
|
|
21
|
-
@
|
|
29
|
+
@generation = 0
|
|
30
|
+
@stopped = false
|
|
31
|
+
@renewal_generation = nil
|
|
22
32
|
end
|
|
23
33
|
|
|
24
34
|
def start(definitions, vault_client: nil)
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
client = vault_client || Legion::Crypt.vault_client
|
|
36
|
+
generation = @state_mutex.synchronize do
|
|
37
|
+
return if Legion::Crypt.respond_to?(:stopping) && Legion::Crypt.stopping
|
|
27
38
|
|
|
39
|
+
@stopped = false
|
|
40
|
+
@vault_client = client
|
|
41
|
+
@generation
|
|
42
|
+
end
|
|
28
43
|
register_at_exit_hook
|
|
44
|
+
(definitions || {}).each do |name, opts|
|
|
45
|
+
break unless lifecycle_current?(generation: generation)
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
definitions.each do |name, opts|
|
|
47
|
+
name = name.to_sym
|
|
32
48
|
path = opts['path'] || opts[:path]
|
|
33
49
|
next unless path
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
revoke_expired_lease(name)
|
|
51
|
+
pending = @state_mutex.synchronize { @pending[name] }
|
|
52
|
+
next if lease_valid?(name, path: path, client: client)
|
|
53
|
+
next if pending && pending[:path] == path && pending[:client].equal?(client) && pending[:next_attempt_at] > monotonic_now
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
log.info("LeaseManager: fetching lease '#{name}' from #{path}")
|
|
44
|
-
response = logical.read(path)
|
|
45
|
-
unless response
|
|
46
|
-
log.warn("LeaseManager: no data at '#{name}' (#{path}) — path may not exist or role not configured")
|
|
47
|
-
next
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
log_lease_response(name, response)
|
|
51
|
-
cache_lease(name, response, path: path)
|
|
52
|
-
log.info("LeaseManager: fetched lease '#{name}' from #{path} " \
|
|
53
|
-
"(lease_id=#{response.lease_id.to_s[0..11]}... ttl=#{response.lease_duration}s renewable=#{response.renewable?})")
|
|
54
|
-
rescue StandardError => e
|
|
55
|
-
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.start', lease_name: name, path: path)
|
|
56
|
-
log.warn("LeaseManager: failed to fetch lease '#{name}' from #{path}: #{e.message}")
|
|
57
|
-
end
|
|
55
|
+
acquire_lease(name: name, path: path, client: client, generation: generation)
|
|
58
56
|
end
|
|
59
57
|
end
|
|
60
58
|
|
|
61
59
|
def fetched_count
|
|
62
|
-
@state_mutex.synchronize { @active_leases.
|
|
60
|
+
@state_mutex.synchronize { @active_leases.count { |name, _lease| cached_lease_data(name: name) } }
|
|
63
61
|
end
|
|
64
62
|
|
|
65
63
|
def fetch(name, key)
|
|
66
|
-
data =
|
|
67
|
-
|
|
68
|
-
end
|
|
69
|
-
return nil unless data
|
|
64
|
+
data = lease_data(name)
|
|
65
|
+
return unless data
|
|
70
66
|
|
|
71
|
-
data[key.to_sym]
|
|
67
|
+
data.key?(key.to_sym) ? data[key.to_sym] : data[key.to_s]
|
|
72
68
|
end
|
|
73
69
|
|
|
74
70
|
def lease_data(name)
|
|
75
|
-
@state_mutex.synchronize {
|
|
71
|
+
@state_mutex.synchronize { cached_lease_data(name: name.to_sym) }
|
|
76
72
|
end
|
|
77
73
|
|
|
78
74
|
attr_reader :active_leases
|
|
79
75
|
|
|
80
|
-
def register_ref(name, key, path)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
def register_ref(name, key, path, generation: @generation)
|
|
77
|
+
return false unless path.is_a?(Array) && !path.empty?
|
|
78
|
+
|
|
79
|
+
with_current_state(generation: generation) do
|
|
80
|
+
@refs[name.to_sym] ||= {}
|
|
81
|
+
destinations = (@refs[name.to_sym][key.to_sym] ||= [])
|
|
82
|
+
destinations << path.dup.freeze unless destinations.include?(path)
|
|
84
83
|
end
|
|
85
84
|
end
|
|
86
85
|
|
|
87
|
-
def push_to_settings(name)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
def push_to_settings(name, generation: @generation, active: nil)
|
|
87
|
+
count = with_current_state(generation: generation, active: active) do
|
|
88
|
+
refs = @refs[name.to_sym]
|
|
89
|
+
data = cached_lease_data(name: name.to_sym)
|
|
90
|
+
next 0 if refs.nil? || refs.empty? || data.nil?
|
|
91
|
+
|
|
92
|
+
refs.sum do |key, paths|
|
|
93
|
+
next 0 unless data.key?(key) || data.key?(key.to_s)
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
value = data.key?(key) ? data[key] : data[key.to_s]
|
|
96
|
+
paths.count { |path| write_setting(path, value) }
|
|
97
|
+
end
|
|
99
98
|
end
|
|
99
|
+
return unless count
|
|
100
100
|
|
|
101
|
-
log.info("Lease '#{name}' rotated — updated #{
|
|
101
|
+
log.info("Lease '#{name}' rotated — updated #{count} settings reference(s)")
|
|
102
|
+
count
|
|
102
103
|
end
|
|
103
104
|
|
|
104
|
-
# Public Vault client accessors used by Crypt for bootstrap/swap operations.
|
|
105
|
-
# Delegates to the configured vault_client or falls back to ::Vault.
|
|
106
105
|
def vault_logical
|
|
107
|
-
|
|
106
|
+
client = @state_mutex.synchronize { @vault_client } || Legion::Crypt.vault_client
|
|
107
|
+
client.logical
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def vault_sys
|
|
111
|
-
|
|
111
|
+
client = @state_mutex.synchronize { @vault_client } || Legion::Crypt.vault_client
|
|
112
|
+
client.sys
|
|
112
113
|
end
|
|
113
114
|
|
|
114
|
-
def reissue_all
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
lease_names.each do |name|
|
|
119
|
-
lease = @state_mutex.synchronize { @active_leases[name]&.dup }
|
|
120
|
-
next unless lease && lease[:path]
|
|
115
|
+
def reissue_all(active: nil)
|
|
116
|
+
names, generation = @state_mutex.synchronize { [@active_leases.keys, @generation] }
|
|
117
|
+
names.each do |name|
|
|
118
|
+
break unless lifecycle_current?(generation: generation, active: active)
|
|
121
119
|
|
|
122
|
-
reissue_lease(name)
|
|
120
|
+
reissue_lease(name, generation: generation, active: active)
|
|
123
121
|
end
|
|
124
|
-
log.info('LeaseManager: reissue_all complete')
|
|
125
122
|
end
|
|
126
123
|
|
|
127
124
|
def register_dynamic_lease(name:, path:, response:, settings_refs:)
|
|
125
|
+
name = name.to_sym
|
|
126
|
+
generation, client = @state_mutex.synchronize { [@generation, @vault_client] }
|
|
127
|
+
client ||= Legion::Crypt.vault_client
|
|
128
128
|
register_at_exit_hook
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
fetched_at: Time.now,
|
|
137
|
-
renewable: response.renewable?,
|
|
138
|
-
path: path
|
|
139
|
-
}
|
|
129
|
+
ownership = track_ownership(name: name, client: client, lease_id: response.lease_id)
|
|
130
|
+
install_started = false
|
|
131
|
+
with_lease_operation(name: name) do |deadline|
|
|
132
|
+
settings_refs.each { |ref| register_ref(name, ref[:key], ref[:path], generation: generation) }
|
|
133
|
+
install_started = true
|
|
134
|
+
install_lease(name: name, path: path, response: response, client: client, generation: generation,
|
|
135
|
+
deadline: deadline, reconnect: true, ownership: ownership)
|
|
140
136
|
end
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
rescue StandardError => e
|
|
138
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.register_dynamic_lease', lease_name: name)
|
|
139
|
+
unless install_started
|
|
140
|
+
@state_mutex.synchronize do
|
|
141
|
+
entry = @owned_leases[ownership]
|
|
142
|
+
entry[:retired] = true if entry && @active_leases[name]&.dig(:ownership) != ownership
|
|
143
|
+
end
|
|
143
144
|
end
|
|
144
|
-
|
|
145
|
+
schedule_retry(name: name, path: path, client: client, generation: generation)
|
|
146
|
+
false
|
|
145
147
|
end
|
|
146
148
|
|
|
147
|
-
def reissue_lease(name)
|
|
149
|
+
def reissue_lease(name, generation: @generation, active: nil, deadline: nil)
|
|
150
|
+
name = name.to_sym
|
|
148
151
|
lease = @state_mutex.synchronize { @active_leases[name]&.dup }
|
|
149
|
-
unless lease && lease[:path]
|
|
150
|
-
log.warn("LeaseManager: cannot reissue lease '#{name}' — no path stored for re-read")
|
|
151
|
-
return
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
log.info("LeaseManager: reissuing lease '#{name}' from #{lease[:path]}")
|
|
155
|
-
response = logical.read(lease[:path])
|
|
156
|
-
unless response&.data
|
|
157
|
-
log.warn("LeaseManager: reissue for '#{name}' returned no data from #{lease[:path]}")
|
|
158
|
-
return
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
updated = @state_mutex.synchronize do
|
|
162
|
-
active_lease = @active_leases[name]
|
|
163
|
-
next false unless active_lease
|
|
164
|
-
|
|
165
|
-
@lease_cache[name] = response.data
|
|
166
|
-
active_lease.merge!(
|
|
167
|
-
lease_id: response.lease_id,
|
|
168
|
-
lease_duration: response.lease_duration,
|
|
169
|
-
expires_at: Time.now + (response.lease_duration || 0),
|
|
170
|
-
fetched_at: Time.now,
|
|
171
|
-
renewable: response.renewable?
|
|
172
|
-
)
|
|
173
|
-
true
|
|
174
|
-
end
|
|
175
|
-
unless updated
|
|
176
|
-
log.warn("LeaseManager: reissue for '#{name}' skipped — lease was removed during reissue (likely shutdown)")
|
|
177
|
-
return
|
|
178
|
-
end
|
|
152
|
+
return false unless lease && lease[:path] && lifecycle_current?(generation: generation, active: active)
|
|
179
153
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"(new_lease_id=#{lease_id_preview}... ttl=#{response.lease_duration}s)")
|
|
183
|
-
push_to_settings(name)
|
|
184
|
-
trigger_reconnect(name)
|
|
185
|
-
rescue StandardError => e
|
|
186
|
-
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.reissue_lease', lease_name: name)
|
|
187
|
-
log.warn("LeaseManager: failed to reissue lease '#{name}': #{e.message}")
|
|
154
|
+
acquire_lease(name: name, path: lease[:path], client: lease[:client], generation: generation,
|
|
155
|
+
active: active, deadline: deadline, lease: lease)
|
|
188
156
|
end
|
|
189
157
|
|
|
190
158
|
def start_renewal_thread
|
|
159
|
+
previous = @state_mutex.synchronize { @renewal_thread if @renewal_generation != @generation }
|
|
160
|
+
Legion::Crypt::VaultOperation.with_timeout { previous.join } if previous&.alive?
|
|
191
161
|
@state_mutex.synchronize do
|
|
192
|
-
return if @renewal_thread&.alive?
|
|
162
|
+
return if @renewal_thread&.alive? || @stopped
|
|
163
|
+
return if Legion::Crypt.respond_to?(:stopping) && Legion::Crypt.stopping
|
|
193
164
|
|
|
194
165
|
@running = true
|
|
195
|
-
|
|
166
|
+
generation = @generation
|
|
167
|
+
@renewal_generation = generation
|
|
168
|
+
@renewal_thread = Thread.new { renewal_loop(generation: generation) }
|
|
196
169
|
end
|
|
197
170
|
log.info 'LeaseManager renewal thread started'
|
|
198
171
|
end
|
|
@@ -201,41 +174,43 @@ module Legion
|
|
|
201
174
|
@state_mutex.synchronize { @renewal_thread&.alive? || false }
|
|
202
175
|
end
|
|
203
176
|
|
|
204
|
-
def
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
begin
|
|
214
|
-
sys.revoke(lease_id)
|
|
215
|
-
log.debug("LeaseManager: revoked lease '#{name}' (#{lease_id})")
|
|
216
|
-
rescue StandardError => e
|
|
217
|
-
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.shutdown', lease_name: name)
|
|
218
|
-
log.warn("LeaseManager: failed to revoke lease '#{name}' (#{lease_id}): #{e.message}")
|
|
219
|
-
end
|
|
177
|
+
def request_stop
|
|
178
|
+
@state_mutex.synchronize do
|
|
179
|
+
@stopped = true
|
|
180
|
+
@running = false
|
|
181
|
+
@generation += 1
|
|
182
|
+
@lease_locks.clear
|
|
183
|
+
@wake.broadcast
|
|
184
|
+
@renewal_thread
|
|
220
185
|
end
|
|
186
|
+
end
|
|
221
187
|
|
|
188
|
+
def shutdown(deadline: nil)
|
|
189
|
+
deadline ||= monotonic_now + Legion::Crypt::VaultOperation::SHUTDOWN_TIMEOUT
|
|
190
|
+
request_stop
|
|
222
191
|
@state_mutex.synchronize do
|
|
223
|
-
@lease_cache.clear
|
|
224
192
|
@active_leases.clear
|
|
193
|
+
@lease_cache.clear
|
|
225
194
|
@refs.clear
|
|
195
|
+
@pending.clear
|
|
226
196
|
@vault_client = nil
|
|
197
|
+
@owned_leases.each_value { |entry| entry[:retired] = true }
|
|
227
198
|
end
|
|
199
|
+
stop_renewal_thread(deadline: deadline)
|
|
200
|
+
retire_unused_leases(deadline: deadline, force: true)
|
|
228
201
|
log.info 'LeaseManager shutdown complete'
|
|
229
202
|
end
|
|
230
203
|
|
|
231
204
|
def reset!
|
|
205
|
+
stop_renewal_thread
|
|
232
206
|
@state_mutex.synchronize do
|
|
233
|
-
@running = false
|
|
234
207
|
@lease_cache.clear
|
|
235
208
|
@active_leases.clear
|
|
209
|
+
@owned_leases.clear
|
|
210
|
+
@pending.clear
|
|
236
211
|
@refs.clear
|
|
237
212
|
@vault_client = nil
|
|
238
|
-
@
|
|
213
|
+
@stopped = false
|
|
239
214
|
end
|
|
240
215
|
end
|
|
241
216
|
|
|
@@ -245,280 +220,455 @@ module Legion
|
|
|
245
220
|
return if @at_exit_registered
|
|
246
221
|
|
|
247
222
|
at_exit do
|
|
248
|
-
next if @state_mutex.synchronize { @
|
|
223
|
+
next if @state_mutex.synchronize { @owned_leases.empty? }
|
|
249
224
|
|
|
250
|
-
|
|
251
|
-
rescue
|
|
252
|
-
|
|
253
|
-
rescue StandardError => e # best effort on crash
|
|
254
|
-
log.warn("[LeaseManager] at_exit shutdown failed: #{e.class}: #{e.message}")
|
|
225
|
+
shutdown
|
|
226
|
+
rescue StandardError => e
|
|
227
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.at_exit')
|
|
255
228
|
end
|
|
256
229
|
@at_exit_registered = true
|
|
257
230
|
end
|
|
258
231
|
|
|
259
|
-
def
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
fetched_at: Time.now,
|
|
268
|
-
path: path
|
|
269
|
-
}
|
|
232
|
+
def monotonic_now
|
|
233
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def with_lease_operation(name:, deadline: nil, **)
|
|
237
|
+
lock = @state_mutex.synchronize { @lease_locks[name] ||= Mutex.new }
|
|
238
|
+
Legion::Crypt::VaultOperation.with_timeout(deadline: deadline) do |operation_deadline|
|
|
239
|
+
lock.synchronize { yield operation_deadline }
|
|
270
240
|
end
|
|
271
241
|
end
|
|
272
242
|
|
|
273
|
-
def
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"lease_duration=#{response.lease_duration}s, " \
|
|
277
|
-
"renewable=#{response.renewable?}, " \
|
|
278
|
-
"data_keys=#{data_keys.inspect}")
|
|
279
|
-
return unless response.data&.key?(:username)
|
|
243
|
+
def lease_operation_current?(name:, pending: nil, lease: nil, **)
|
|
244
|
+
@state_mutex.synchronize do
|
|
245
|
+
next false if pending && !@pending[name].equal?(pending)
|
|
280
246
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"vhost=#{response.data[:vhost] || 'N/A'}, " \
|
|
284
|
-
"tags=#{response.data[:tags] || 'N/A'}")
|
|
247
|
+
lease.nil? || @active_leases.dig(name, :ownership) == lease[:ownership]
|
|
248
|
+
end
|
|
285
249
|
end
|
|
286
250
|
|
|
287
|
-
def
|
|
288
|
-
|
|
289
|
-
|
|
251
|
+
def acquire_lease(name:, path:, client:, generation:, **options)
|
|
252
|
+
active = options[:active]
|
|
253
|
+
deadline = options[:deadline]
|
|
254
|
+
with_lease_operation(name: name, deadline: deadline) do |operation_deadline|
|
|
255
|
+
return false unless lifecycle_current?(generation: generation, active: active)
|
|
256
|
+
return false unless lease_operation_current?(name: name, pending: options[:pending], lease: options[:lease])
|
|
257
|
+
raise ArgumentError, 'Lease acquisition requires a path' unless path
|
|
258
|
+
|
|
259
|
+
response = client.logical.read(path: path)
|
|
260
|
+
raise "Vault returned no lease for #{name}" unless response
|
|
261
|
+
|
|
262
|
+
install_lease(name: name, path: path, response: response, client: client, generation: generation,
|
|
263
|
+
active: active, deadline: operation_deadline)
|
|
264
|
+
end
|
|
265
|
+
rescue StandardError => e
|
|
266
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.acquire_lease', lease_name: name, path: path)
|
|
267
|
+
schedule_retry(name: name, path: path, client: client, generation: generation, active: active,
|
|
268
|
+
pending: options[:pending], lease: options[:lease])
|
|
269
|
+
false
|
|
290
270
|
end
|
|
291
271
|
|
|
292
|
-
def
|
|
293
|
-
|
|
294
|
-
|
|
272
|
+
def track_ownership(name:, client:, lease_id:, **)
|
|
273
|
+
return if lease_id.nil? || lease_id.empty?
|
|
274
|
+
|
|
275
|
+
key = [client, lease_id]
|
|
276
|
+
@state_mutex.synchronize do
|
|
277
|
+
@owned_leases[key] ||= { name: name, client: client, lease_id: lease_id, retired: false,
|
|
278
|
+
revoking: false, attempts: 0, next_attempt_at: 0 }
|
|
279
|
+
end
|
|
280
|
+
key
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def install_lease(name:, path:, response:, client:, **context)
|
|
284
|
+
lease_id = context.fetch(:lease_id) { response.lease_id }
|
|
285
|
+
ownership = context.fetch(:ownership) { track_ownership(name: name, client: client, lease_id: lease_id) }
|
|
286
|
+
handoff = { name: name, ownership: ownership, context: context }
|
|
287
|
+
now = Time.now
|
|
288
|
+
ttl = response.lease_duration || 0
|
|
289
|
+
candidate = { lease_id: lease_id, lease_duration: ttl, renewable: response.renewable?,
|
|
290
|
+
expires_at: ttl.positive? ? now + ttl : nil, fetched_at: now, path: path,
|
|
291
|
+
client: client, ownership: ownership }
|
|
292
|
+
handoff[:candidate] = candidate
|
|
293
|
+
handoff[:data] = context.fetch(:data) { response.data }
|
|
294
|
+
return false unless stage_install(handoff: handoff)
|
|
295
|
+
|
|
296
|
+
generation = context.fetch(:generation)
|
|
297
|
+
active = context[:active]
|
|
298
|
+
handoff[:attempted_reconnect] = handoff[:reconnect]
|
|
299
|
+
raise "Lease consumer reconnect failed for #{name}" if handoff[:reconnect] && !trigger_reconnect(name, generation: generation, active: active)
|
|
300
|
+
|
|
301
|
+
committed = with_current_state(generation: generation, active: active) do
|
|
302
|
+
next false unless @active_leases[name].equal?(candidate)
|
|
303
|
+
|
|
304
|
+
@pending.delete(name)
|
|
305
|
+
@owned_leases.each do |key, entry|
|
|
306
|
+
entry[:retired] = true if entry[:name] == name && key != ownership
|
|
307
|
+
end
|
|
308
|
+
@wake.broadcast
|
|
309
|
+
true
|
|
310
|
+
end
|
|
311
|
+
committed
|
|
312
|
+
ensure
|
|
313
|
+
abort_install(handoff: handoff) if handoff && !committed
|
|
314
|
+
retire_unused_leases(name: name, deadline: context[:deadline])
|
|
295
315
|
end
|
|
296
316
|
|
|
297
|
-
def
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
317
|
+
def stage_install(handoff:, **)
|
|
318
|
+
name = handoff[:name]
|
|
319
|
+
data = handoff[:data]
|
|
320
|
+
context = handoff[:context]
|
|
321
|
+
|
|
322
|
+
with_current_state(generation: context.fetch(:generation), active: context[:active]) do
|
|
323
|
+
validate_lease_data!(name: name, data: data, require_credentials: context[:reconnect] || @refs.dig(name, :password))
|
|
324
|
+
owner = @owned_leases[handoff[:ownership]]
|
|
325
|
+
raise 'Lease candidate is being revoked or has already been retired' if handoff[:ownership] && (!owner || owner[:revoking])
|
|
326
|
+
|
|
327
|
+
owner[:retired] = false if owner
|
|
328
|
+
|
|
329
|
+
previous = @active_leases[name]
|
|
330
|
+
snapshots = setting_snapshots(name: name, data: data)
|
|
331
|
+
transaction = { previous: previous, data: @lease_cache[name], settings: snapshots, candidate: handoff[:candidate] }
|
|
332
|
+
handoff[:transaction] = transaction
|
|
333
|
+
snapshots.each { |target, key, _old, value| target[key] = value }
|
|
334
|
+
@lease_cache[name] = data
|
|
335
|
+
@active_leases[name] = handoff[:candidate]
|
|
336
|
+
handoff[:reconnect] = context[:reconnect] || (previous && transaction[:data] != data) || !snapshots.empty?
|
|
337
|
+
true
|
|
301
338
|
end
|
|
302
|
-
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def validate_lease_data!(name:, data:, require_credentials:, **)
|
|
342
|
+
raise "Vault returned invalid lease data for #{name}" unless data.is_a?(Hash)
|
|
343
|
+
return unless name == :rabbitmq && require_credentials
|
|
344
|
+
|
|
345
|
+
username = data[:username] || data['username']
|
|
346
|
+
password = data[:password] || data['password']
|
|
347
|
+
raise 'RabbitMQ lease requires username and password' unless username && password
|
|
348
|
+
end
|
|
303
349
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
350
|
+
def abort_install(handoff:, **)
|
|
351
|
+
name = handoff[:name]
|
|
352
|
+
context = handoff[:context]
|
|
353
|
+
generation = context.fetch(:generation)
|
|
354
|
+
active = context[:active]
|
|
355
|
+
ownership = handoff[:ownership]
|
|
356
|
+
restored = rollback_install(name: name, transaction: handoff[:transaction], generation: generation, active: active)
|
|
357
|
+
safe_to_retire = !handoff[:attempted_reconnect] || !lifecycle_current?(generation: generation, active: active)
|
|
358
|
+
if handoff[:attempted_reconnect] && restored
|
|
359
|
+
begin
|
|
360
|
+
safe_to_retire = Legion::Crypt::VaultOperation.with_timeout(deadline: context[:deadline]) do
|
|
361
|
+
trigger_reconnect(name, generation: generation, active: active)
|
|
362
|
+
end
|
|
363
|
+
rescue StandardError => e
|
|
364
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.rollback_reconnect', lease_name: name)
|
|
365
|
+
end
|
|
308
366
|
end
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
else
|
|
313
|
-
@state_mutex.synchronize { @renewal_thread = nil }
|
|
314
|
-
log.debug 'LeaseManager renewal thread stopped'
|
|
367
|
+
@state_mutex.synchronize do
|
|
368
|
+
entry = @owned_leases[ownership]
|
|
369
|
+
entry[:retired] = true if entry && safe_to_retire && @active_leases[name]&.dig(:ownership) != ownership
|
|
315
370
|
end
|
|
316
371
|
end
|
|
317
372
|
|
|
318
|
-
def
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
373
|
+
def setting_snapshots(name:, data:, **)
|
|
374
|
+
(@refs[name] || {}).flat_map do |key, paths|
|
|
375
|
+
raise "Lease #{name} omitted referenced key #{key}" unless data.key?(key) || data.key?(key.to_s)
|
|
376
|
+
|
|
377
|
+
value = data.key?(key) ? data[key] : data[key.to_s]
|
|
378
|
+
paths.filter_map do |path|
|
|
379
|
+
target, segment = setting_target(path: path)
|
|
380
|
+
raise "Lease #{name} has an unwritable settings destination #{path.inspect}" unless target
|
|
381
|
+
next if target[segment] == value
|
|
382
|
+
|
|
383
|
+
[target, segment, target[segment], value]
|
|
384
|
+
end
|
|
323
385
|
end
|
|
324
|
-
log.info 'LeaseManager: renewal loop exiting'
|
|
325
|
-
rescue StandardError => e
|
|
326
|
-
handle_exception(e, level: :error, operation: 'crypt.lease_manager.renewal_loop')
|
|
327
|
-
log.error("LeaseManager: renewal loop error: #{e.message} — restarting")
|
|
328
|
-
retry if running?
|
|
329
386
|
end
|
|
330
387
|
|
|
331
|
-
def
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
next unless
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
elsif lease[:path]
|
|
345
|
-
log.info("LeaseManager: lease '#{name}' is non-renewable — re-issuing from #{lease[:path]}")
|
|
346
|
-
reissue_lease(name)
|
|
388
|
+
def rollback_install(name:, transaction:, generation:, active:, **)
|
|
389
|
+
return false unless transaction
|
|
390
|
+
|
|
391
|
+
with_current_state(generation: generation, active: active) do
|
|
392
|
+
current = @active_leases[name]
|
|
393
|
+
next false unless current.equal?(transaction[:candidate]) || current.equal?(transaction[:previous])
|
|
394
|
+
|
|
395
|
+
transaction[:settings].each do |target, key, previous, candidate|
|
|
396
|
+
target[key] = previous if target[key] == candidate
|
|
397
|
+
end
|
|
398
|
+
if transaction[:previous]
|
|
399
|
+
@active_leases[name] = transaction[:previous]
|
|
400
|
+
@lease_cache[name] = transaction[:data]
|
|
347
401
|
else
|
|
348
|
-
|
|
349
|
-
|
|
402
|
+
@active_leases.delete(name)
|
|
403
|
+
@lease_cache.delete(name)
|
|
350
404
|
end
|
|
405
|
+
true
|
|
351
406
|
end
|
|
407
|
+
rescue StandardError => e
|
|
408
|
+
handle_exception(e, level: :error, operation: 'crypt.lease_manager.rollback_install', lease_name: name)
|
|
409
|
+
false
|
|
352
410
|
end
|
|
353
411
|
|
|
354
|
-
def
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if lease[:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
412
|
+
def schedule_retry(name:, path:, client:, generation:, **options)
|
|
413
|
+
with_current_state(generation: generation, active: options[:active]) do
|
|
414
|
+
previous = @pending[name]
|
|
415
|
+
next false if options[:pending] && !previous.equal?(options[:pending])
|
|
416
|
+
next false if options[:lease] && @active_leases.dig(name, :ownership) != options[:lease][:ownership]
|
|
417
|
+
|
|
418
|
+
attempts = previous && previous[:path] == path && previous[:client].equal?(client) ? previous[:attempts] : 0
|
|
419
|
+
attempts = [attempts + 1, 7].min
|
|
420
|
+
@pending[name] = { path: path, client: client, operation: options.fetch(:operation, :acquire), attempts: attempts,
|
|
421
|
+
next_attempt_at: monotonic_now + [2**(attempts - 1), MAX_RETRY_DELAY].min }
|
|
422
|
+
@wake.broadcast
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def retire_unused_leases(name: nil, deadline: nil, force: false, **)
|
|
427
|
+
keys = @state_mutex.synchronize do
|
|
428
|
+
@owned_leases.filter_map do |key, entry|
|
|
429
|
+
key if entry[:retired] && (!name || entry[:name] == name) && (force || entry[:next_attempt_at] <= monotonic_now)
|
|
364
430
|
end
|
|
365
|
-
return
|
|
366
431
|
end
|
|
432
|
+
keys.each { |key| revoke_owned_lease(key: key, deadline: deadline) }
|
|
433
|
+
end
|
|
367
434
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
next unless current_lease
|
|
435
|
+
def revoke_owned_lease(key:, deadline:, **)
|
|
436
|
+
entry = @state_mutex.synchronize do
|
|
437
|
+
item = @owned_leases[key]
|
|
438
|
+
next unless item && item[:retired] && !item[:revoking]
|
|
439
|
+
next if @active_leases.any? { |_name, lease| lease[:ownership] == key }
|
|
374
440
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
current_lease[:expires_at] = Time.now + new_ttl
|
|
378
|
-
end
|
|
379
|
-
current_lease[:renewable] = response.renewable? if response.respond_to?(:renewable?)
|
|
441
|
+
item[:revoking] = true
|
|
442
|
+
item
|
|
380
443
|
end
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
444
|
+
return unless entry
|
|
445
|
+
|
|
446
|
+
Legion::Crypt::VaultOperation.with_timeout(deadline: deadline) { entry[:client].sys.revoke(lease_id: entry[:lease_id]) }
|
|
447
|
+
@state_mutex.synchronize { @owned_leases.delete(key) if @owned_leases[key].equal?(entry) }
|
|
448
|
+
rescue StandardError => e
|
|
449
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.revoke_owned_lease', lease_name: entry&.dig(:name))
|
|
450
|
+
@state_mutex.synchronize do
|
|
451
|
+
if entry && @owned_leases[key].equal?(entry)
|
|
452
|
+
entry[:attempts] = [entry[:attempts] + 1, 7].min
|
|
453
|
+
entry[:next_attempt_at] = monotonic_now + [2**(entry[:attempts] - 1), MAX_RETRY_DELAY].min
|
|
454
|
+
end
|
|
385
455
|
end
|
|
456
|
+
ensure
|
|
457
|
+
@state_mutex.synchronize { entry[:revoking] = false if entry }
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def stop_renewal_thread(deadline: nil)
|
|
461
|
+
thread = request_stop
|
|
462
|
+
return unless thread
|
|
386
463
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
464
|
+
Legion::Crypt::VaultOperation.with_timeout(deadline: deadline) { thread.join }
|
|
465
|
+
@state_mutex.synchronize { @renewal_thread = nil if @renewal_thread.equal?(thread) }
|
|
466
|
+
rescue StandardError => e
|
|
467
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.stop_renewal_thread')
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def renewal_loop(generation:)
|
|
471
|
+
while running?(generation: generation)
|
|
472
|
+
renew_approaching_leases(generation: generation)
|
|
473
|
+
interruptible_sleep(generation: generation)
|
|
392
474
|
end
|
|
393
475
|
rescue StandardError => e
|
|
394
|
-
handle_exception(e, level: :
|
|
395
|
-
|
|
396
|
-
if
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
476
|
+
handle_exception(e, level: :error, operation: 'crypt.lease_manager.renewal_loop')
|
|
477
|
+
interruptible_sleep(RENEWAL_CHECK_INTERVAL, generation: generation)
|
|
478
|
+
retry if running?(generation: generation)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Called while holding @state_mutex, immediately before the condition wait.
|
|
482
|
+
def next_maintenance_delay
|
|
483
|
+
delays = @pending.values.map { |entry| entry[:next_attempt_at] - monotonic_now }
|
|
484
|
+
@active_leases.each do |name, lease|
|
|
485
|
+
delays << renewal_delay(lease: lease) unless @pending.key?(name)
|
|
486
|
+
end
|
|
487
|
+
@owned_leases.each do |key, entry|
|
|
488
|
+
next unless entry[:retired] && !entry[:revoking]
|
|
489
|
+
next if @active_leases.each_value.any? { |lease| lease[:ownership] == key }
|
|
490
|
+
|
|
491
|
+
delays << (entry[:next_attempt_at] - monotonic_now)
|
|
402
492
|
end
|
|
493
|
+
(delays.min || RENEWAL_CHECK_INTERVAL).clamp(MINIMUM_SLEEP, RENEWAL_CHECK_INTERVAL)
|
|
403
494
|
end
|
|
404
495
|
|
|
405
|
-
def
|
|
406
|
-
|
|
407
|
-
|
|
496
|
+
def renew_approaching_leases(generation: @generation)
|
|
497
|
+
leases = @state_mutex.synchronize { @active_leases.to_a.sort_by { |_name, lease| renewal_delay(lease: lease) } }
|
|
498
|
+
attempted = []
|
|
499
|
+
leases.each do |name, lease|
|
|
500
|
+
break unless lifecycle_current?(generation: generation)
|
|
501
|
+
next if @state_mutex.synchronize { @pending.key?(name) }
|
|
502
|
+
next if renewal_delay(lease: lease).positive?
|
|
408
503
|
|
|
409
|
-
|
|
410
|
-
|
|
504
|
+
attempted << name
|
|
505
|
+
if lease_renewable?(lease: lease)
|
|
506
|
+
renew_lease(name, lease.dup, generation: generation)
|
|
507
|
+
else
|
|
508
|
+
acquire_lease(name: name, path: lease[:path], client: lease[:client], generation: generation, lease: lease)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
pending = @state_mutex.synchronize { @pending.to_a }
|
|
512
|
+
pending.each do |name, entry|
|
|
513
|
+
break unless lifecycle_current?(generation: generation)
|
|
514
|
+
next if attempted.include?(name) || entry[:next_attempt_at] > monotonic_now
|
|
411
515
|
|
|
412
|
-
|
|
516
|
+
lease = @state_mutex.synchronize { @active_leases[name]&.dup }
|
|
517
|
+
if entry[:operation] == :renew && lease_renewable?(lease: lease)
|
|
518
|
+
renew_lease(name, lease, generation: generation, pending: entry)
|
|
519
|
+
else
|
|
520
|
+
acquire_lease(name: name, path: entry[:path], client: entry[:client], generation: generation, pending: entry, lease: lease)
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
retire_unused_leases if lifecycle_current?(generation: generation)
|
|
413
524
|
end
|
|
414
525
|
|
|
415
|
-
def
|
|
416
|
-
|
|
417
|
-
|
|
526
|
+
def renew_lease(name, lease, generation: @generation, pending: nil)
|
|
527
|
+
name = name.to_sym
|
|
528
|
+
operation_deadline = nil
|
|
529
|
+
with_lease_operation(name: name) do |deadline|
|
|
530
|
+
operation_deadline = deadline
|
|
531
|
+
return false unless lifecycle_current?(generation: generation)
|
|
532
|
+
return false unless lease_operation_current?(name: name, pending: pending, lease: lease)
|
|
418
533
|
|
|
419
|
-
|
|
420
|
-
return if lease_id.nil? || lease_id.empty?
|
|
534
|
+
raise 'Lease cannot be renewed without an ID' if lease[:lease_id].to_s.empty?
|
|
421
535
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
536
|
+
response = lease[:client].sys.renew(lease_id: lease[:lease_id])
|
|
537
|
+
raise 'Lease renewal did not return a positive TTL' unless response&.lease_duration&.positive?
|
|
538
|
+
|
|
539
|
+
data = response.data
|
|
540
|
+
data = @state_mutex.synchronize { @lease_cache[name] } if data.nil? || data.empty?
|
|
541
|
+
lease_id = response.lease_id.to_s.empty? ? lease[:lease_id] : response.lease_id
|
|
542
|
+
ownership = if lease_id == lease[:lease_id]
|
|
543
|
+
lease[:ownership]
|
|
544
|
+
else
|
|
545
|
+
track_ownership(name: name, client: lease[:client], lease_id: lease_id)
|
|
546
|
+
end
|
|
547
|
+
install_lease(name: name, path: lease[:path], response: response, client: lease[:client], generation: generation,
|
|
548
|
+
deadline: deadline, data: data, lease_id: lease_id, ownership: ownership)
|
|
433
549
|
end
|
|
550
|
+
rescue StandardError => e
|
|
551
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.renew_lease', lease_name: name)
|
|
552
|
+
return reissue_lease(name, generation: generation, deadline: operation_deadline) if operation_deadline && !e.is_a?(Timeout::Error) && lease[:path]
|
|
553
|
+
|
|
554
|
+
schedule_retry(name: name, path: lease[:path], client: lease[:client], generation: generation, operation: :renew, pending: pending)
|
|
555
|
+
false
|
|
434
556
|
end
|
|
435
557
|
|
|
436
|
-
def
|
|
437
|
-
|
|
438
|
-
|
|
558
|
+
def lease_valid?(name, path:, client:)
|
|
559
|
+
lease = @state_mutex.synchronize { @active_leases[name.to_sym] }
|
|
560
|
+
lease && lease[:path] == path && lease[:client].equal?(client) && lease_unexpired?(lease: lease)
|
|
561
|
+
end
|
|
439
562
|
|
|
440
|
-
|
|
563
|
+
# Called while holding @state_mutex; expired credentials are never exposed to readers.
|
|
564
|
+
def cached_lease_data(name:, **)
|
|
565
|
+
lease = @active_leases[name]
|
|
566
|
+
return @lease_cache[name] if lease && lease_unexpired?(lease: lease)
|
|
441
567
|
|
|
442
|
-
|
|
443
|
-
|
|
568
|
+
@lease_cache.delete(name)
|
|
569
|
+
nil
|
|
444
570
|
end
|
|
445
571
|
|
|
446
|
-
def
|
|
447
|
-
|
|
572
|
+
def lease_unexpired?(lease:, **)
|
|
573
|
+
lease[:expires_at].nil? || lease[:expires_at] > Time.now
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def lease_renewable?(lease:, **)
|
|
577
|
+
lease && lease[:renewable] && lease_unexpired?(lease: lease)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def renewal_delay(lease:, **)
|
|
581
|
+
ttl = lease[:lease_duration] || 0
|
|
582
|
+
return lease[:fetched_at] + RENEWAL_CHECK_INTERVAL - Time.now unless ttl.positive?
|
|
583
|
+
return 0 unless lease[:expires_at]
|
|
584
|
+
|
|
585
|
+
lease[:expires_at] - Time.now - (ttl * 0.5)
|
|
586
|
+
end
|
|
448
587
|
|
|
449
|
-
|
|
450
|
-
|
|
588
|
+
def setting_target(path:, **)
|
|
589
|
+
return unless path.is_a?(Array) && !path.empty?
|
|
451
590
|
|
|
452
|
-
|
|
591
|
+
target = Legion::Settings.get.settings
|
|
592
|
+
path.each_with_index do |segment, index|
|
|
593
|
+
valid =
|
|
594
|
+
case target
|
|
595
|
+
when Hash then segment.is_a?(Symbol) && target.key?(segment)
|
|
596
|
+
when Array then segment.is_a?(Integer) && segment >= 0 && segment < target.length
|
|
597
|
+
end
|
|
598
|
+
return nil unless valid
|
|
599
|
+
return target.frozen? ? nil : [target, segment] if index == path.length - 1
|
|
600
|
+
|
|
601
|
+
target = target[segment]
|
|
453
602
|
end
|
|
454
|
-
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def write_setting(path, value)
|
|
606
|
+
target, key = setting_target(path: path)
|
|
607
|
+
return false unless target
|
|
608
|
+
|
|
609
|
+
target[key] = value
|
|
610
|
+
true
|
|
455
611
|
rescue StandardError => e
|
|
456
|
-
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.write_setting', path: path.
|
|
457
|
-
|
|
612
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.write_setting', path: path.inspect)
|
|
613
|
+
false
|
|
458
614
|
end
|
|
459
615
|
|
|
460
|
-
def trigger_reconnect(name)
|
|
461
|
-
|
|
462
|
-
|
|
616
|
+
def trigger_reconnect(name, generation: @generation, active: nil)
|
|
617
|
+
return false unless lifecycle_current?(generation: generation, active: active)
|
|
618
|
+
|
|
619
|
+
case name.to_sym
|
|
463
620
|
when :rabbitmq
|
|
464
|
-
return unless defined?(Legion::Transport::Connection)
|
|
621
|
+
return true unless defined?(Legion::Transport::Connection)
|
|
465
622
|
|
|
466
|
-
Legion::Transport::Connection
|
|
467
|
-
|
|
623
|
+
connection = Legion::Transport::Connection
|
|
624
|
+
connection.force_reconnect == true && connection.session_open?
|
|
468
625
|
when :postgresql
|
|
469
|
-
|
|
470
|
-
when :redis
|
|
471
|
-
return unless defined?(Legion::Cache)
|
|
626
|
+
return true unless defined?(Legion::Data::Connection)
|
|
472
627
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
628
|
+
connection = Legion::Data::Connection
|
|
629
|
+
connection.respond_to?(:reconnect_with_fresh_creds) && connection.reconnect_with_fresh_creds == true
|
|
630
|
+
when :redis
|
|
631
|
+
return true unless defined?(Legion::Cache)
|
|
632
|
+
|
|
633
|
+
cache = Legion::Cache
|
|
634
|
+
result = if cache.respond_to?(:restart)
|
|
635
|
+
cache.restart
|
|
636
|
+
elsif cache.respond_to?(:reconnect)
|
|
637
|
+
cache.reconnect
|
|
638
|
+
end
|
|
639
|
+
result == true && (!cache.respond_to?(:connected?) || cache.connected?)
|
|
640
|
+
else
|
|
641
|
+
true
|
|
479
642
|
end
|
|
480
643
|
rescue StandardError => e
|
|
481
644
|
handle_exception(e, level: :error, operation: 'crypt.lease_manager.trigger_reconnect', lease_name: name)
|
|
482
|
-
|
|
483
|
-
'services may be unavailable until the next lease rotation')
|
|
645
|
+
false
|
|
484
646
|
end
|
|
485
647
|
|
|
486
|
-
def
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
return
|
|
490
|
-
end
|
|
648
|
+
def with_current_state(generation:, active: nil, **)
|
|
649
|
+
@state_mutex.synchronize do
|
|
650
|
+
next false unless lifecycle_current?(generation: generation)
|
|
491
651
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
log.error("LeaseManager: FAILED to reconnect data layer after '#{name}' reissue — " \
|
|
498
|
-
'Apollo and other DB-backed services may be unavailable')
|
|
499
|
-
end
|
|
500
|
-
elsif Legion::Data::Connection.respond_to?(:sequel) && Legion::Data::Connection.sequel
|
|
501
|
-
log.warn('LeaseManager: legion-data does not support reconnect_with_fresh_creds — ' \
|
|
502
|
-
'falling back to pool disconnect (may use stale credentials)')
|
|
503
|
-
Legion::Data::Connection.sequel.disconnect
|
|
504
|
-
Legion::Data::Connection.sequel.test_connection
|
|
505
|
-
log.info("LeaseManager: triggered data pool reconnect after '#{name}' reissue (legacy path)")
|
|
506
|
-
else
|
|
507
|
-
log.warn("LeaseManager: no active data connection to reconnect after '#{name}' reissue")
|
|
652
|
+
next yield unless active
|
|
653
|
+
|
|
654
|
+
result = false
|
|
655
|
+
active.call { result = yield }
|
|
656
|
+
result
|
|
508
657
|
end
|
|
509
658
|
end
|
|
510
659
|
|
|
511
|
-
def
|
|
512
|
-
|
|
660
|
+
def lifecycle_current?(generation:, active: nil, **)
|
|
661
|
+
!@stopped && generation == @generation &&
|
|
662
|
+
!(Legion::Crypt.respond_to?(:stopping) && Legion::Crypt.stopping) && (!active || active.call)
|
|
513
663
|
end
|
|
514
664
|
|
|
515
|
-
def
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
519
|
-
break if remaining <= 0 || !running?
|
|
665
|
+
def running?(generation: @generation)
|
|
666
|
+
@state_mutex.synchronize { @running && lifecycle_current?(generation: generation) }
|
|
667
|
+
end
|
|
520
668
|
|
|
521
|
-
|
|
669
|
+
def interruptible_sleep(seconds = nil, generation: @generation)
|
|
670
|
+
@state_mutex.synchronize do
|
|
671
|
+
@wake.wait(@state_mutex, seconds || next_maintenance_delay) if @running && lifecycle_current?(generation: generation)
|
|
522
672
|
end
|
|
523
673
|
end
|
|
524
674
|
end
|