Give the runtime real readiness, graceful shutdown and stated limits
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 41s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 41s
Closes gap G08. /healthz returned a constant without probing anything, the server called ListenAndServe with no signal handling, and the operational limits of in-memory state, startup-loaded keys and local-only logout lived in code comments rather than anywhere an operator would look. /readyz probes LLDAP, Authelia and privacyIDEA; /healthz stays liveness and probes nothing. Keeping them distinct matters: wiring liveness to dependency health means an orchestrator restarts KeyCape when a dependency blinks, and a restart also discards every in-flight login, so the reaction is worse than the condition it reacts to. LLDAP is probed with a bind rather than a dial, since a rotated or revoked service password leaves the port open and every lookup failing -- exactly what readiness should catch and exactly what a dial would miss. The response names the failing check but never the reason: the endpoint is unauthenticated and upstream error text carries hostnames and sometimes credentials-in-URLs. Results are cached for 2s so an unauthenticated endpoint cannot be used to drive unbounded upstream traffic, and probes run concurrently under a 3s bound so a hung dependency makes the endpoint answer rather than hang with it. SIGTERM and SIGINT now drain in-flight requests for 15s, under the 30s read/write timeouts so a stuck request cannot outlive the window before SIGKILL. docs/operations.md states the single-replica topology and why, and three limits easy to get wrong: the constant key-1 key ID makes same-kid rotation a trap for consumers caching JWKS, removing a client does not revoke its issued tokens, and /logout is local only. No throughput figures are given, since nothing here benchmarks KeyCape. Shared storage and refresh tokens stay excluded, as G08 allows. Verified in the running executable: 503 naming all three checks failed while /healthz returned 200, the LLDAP check flipping to ok once started, and 40/40 requests succeeding across a SIGTERM. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P Assistant: claude-code Assistant-Model: opus Assistant-Process: 713576@bnt-lap001 Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
This commit is contained in:
parent
a9296fdf84
commit
d568b79223
8 changed files with 622 additions and 4 deletions
|
|
@ -435,3 +435,39 @@ var (
|
|||
_ domain.UserRepository = (*LDAPAdapter)(nil)
|
||||
_ domain.GroupLister = (*LDAPAdapter)(nil)
|
||||
)
|
||||
|
||||
// Ping reports whether the directory is reachable and the configured service
|
||||
// credentials still bind. It is the readiness probe for LLDAP (KEY-WP-0025).
|
||||
//
|
||||
// A bind rather than a bare dial: a rotated or revoked service password leaves
|
||||
// the port open and every lookup failing, which is exactly the state readiness
|
||||
// exists to catch. dial already binds, so this opens and closes one connection.
|
||||
func (a *LDAPAdapter) Ping(ctx context.Context) error {
|
||||
type dialResult struct {
|
||||
conn LDAPConn
|
||||
err error
|
||||
}
|
||||
// dial is synchronous and has no context, so race it against the caller's
|
||||
// deadline rather than letting a hung directory outlive the probe budget.
|
||||
results := make(chan dialResult, 1)
|
||||
go func() {
|
||||
conn, err := a.dial()
|
||||
results <- dialResult{conn, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
go func() {
|
||||
if result := <-results; result.err == nil {
|
||||
result.conn.Close()
|
||||
}
|
||||
}()
|
||||
return ctx.Err()
|
||||
case result := <-results:
|
||||
if result.err != nil {
|
||||
return result.err
|
||||
}
|
||||
result.conn.Close()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue