diff --git a/images/go_chains_hc/README.md b/images/go_chains_hc/README.md index 2af9475..da58cc8 100644 --- a/images/go_chains_hc/README.md +++ b/images/go_chains_hc/README.md @@ -1,6 +1,6 @@ ## Connected Chains Healthcheck -A minimal Go binary/container that exposes a `/readyz` HTTP endpoint for blockchain node readiness probes. Designed to be used alongside any compatible node (Bitcoin, Dogecoin, Litecoin, etc.) in a Kubernetes pod. +A minimal Go binary/container that exposes a `/readyz` HTTP endpoint for blockchain node readiness probes. Designed to be used alongside any compatible node in a Kubernetes pod or as standalone Docker service. ### How it works @@ -8,12 +8,22 @@ On each `/readyz` request, the sidecar runs a configurable set of checks against ### Checks +#### Bitcoin / Dogecoin + | Check | RPC Method | Description | |---|---|---| -| `blockdownload` | `getblockchaininfo` | Passes when `initialblockdownload` is `false`. Default for all chains. | +| `blockdownload` | `getblockchaininfo` | Passes when `initialblockdownload` is `false` | | `txindex` | `getindexinfo` | Passes when `txindex.synced` is `true`. | | `connectioncount` | `getconnectioncount` | Passes when the node has at least `MIN_CONNECTIONS` peers. | +#### XRPL + +| Check | RPC Method | Description | +|---|---|---| +| `serverstatus` | `ping` | Passes when `status` is `success` | +| `nodesynced` | `server_info` | Passes when `server_state` is `full`\|`proposing`\|`validating`. | +| `peercount` | `server_info` | Passes when the node has at least `MIN_CONNECTIONS` peers. | + ### Environment Variables | Variable | Required | Default | Description | @@ -21,7 +31,7 @@ On each `/readyz` request, the sidecar runs a configurable set of checks against | `NODE_URL` | yes | — | RPC endpoint, e.g. `http://localhost:8332` | | `NODE_USER` | no | — | RPC auth username | | `NODE_PASS` | no | — | RPC auth password | -| `CHECKS` | no | `blockdownload` | Comma-separated list of checks to run (`blockdownload, txindex, connectioncount`) | +| `CHECKS` | yes | - | Comma-separated list of checks to run (Check above section) | | `MIN_CONNECTIONS` | no | `8` | Minimum peer connections required for the `connectioncount` check | | `DEBUG` | no | `false` | Enable debug logs | diff --git a/images/go_chains_hc/checks.go b/images/go_chains_hc/checks.go index 8b7828f..35b6872 100644 --- a/images/go_chains_hc/checks.go +++ b/images/go_chains_hc/checks.go @@ -15,9 +15,15 @@ type Check func(ctx context.Context, client *http.Client, cfg Config) error const jsonRPCVersion = "2.0" var registry = map[string]Check{ + // BTC/Dogecoin "blockdownload": checkBlockDownload, "txindex": checkTxIndex, "connectioncount": checkConnectionCount, + + // Ripple + "nodesynced": checkNodeServerState, + "peercount": checkPeerCount, + "serverstatus": checkServerStatus, } type rpcRequest struct { @@ -139,3 +145,74 @@ func checkConnectionCount(ctx context.Context, client *http.Client, cfg Config) return nil } + +func checkServerStatus(ctx context.Context, client *http.Client, cfg Config) error { + result, err := doRPC(ctx, client, cfg, "ping") + if err != nil { + return err + } + + var info struct { + Status string `json:"status"` + } + if err := json.Unmarshal(result, &info); err != nil { + return fmt.Errorf("parse ping response: %w", err) + } + if info.Status != "success" { + return fmt.Errorf("unexpected status: %q", info.Status) + } + + return nil +} + +func checkNodeServerState(ctx context.Context, client *http.Client, cfg Config) error { + result, err := doRPC(ctx, client, cfg, "server_info") + if err != nil { + return err + } + + var info struct { + State struct { + ServerState string `json:"server_state"` + } `json:"info"` + } + + if err := json.Unmarshal(result, &info); err != nil { + return fmt.Errorf("parse server_state response: %w", err) + } + + validServerStates := map[string]bool{ + "full": true, + "validating": true, + "proposing": true, + } + + if !validServerStates[info.State.ServerState] { + return fmt.Errorf("unexpected server_state: %q", info.State.ServerState) + } + + return nil +} + +func checkPeerCount(ctx context.Context, client *http.Client, cfg Config) error { + result, err := doRPC(ctx, client, cfg, "server_info") + if err != nil { + return err + } + + var info struct { + State struct { + Peers int `json:"peers"` + } `json:"info"` + } + + if err := json.Unmarshal(result, &info); err != nil { + return fmt.Errorf("parse server_state response: %w", err) + } + + if info.State.Peers < cfg.MinConnections { + return fmt.Errorf("not enough peers: %d < %d", info.State.Peers, cfg.MinConnections) + } + + return nil +} diff --git a/images/go_chains_hc/main.go b/images/go_chains_hc/main.go index 72a4290..256834a 100644 --- a/images/go_chains_hc/main.go +++ b/images/go_chains_hc/main.go @@ -29,9 +29,9 @@ func configFromEnv() (Config, error) { return Config{}, fmt.Errorf("NODE_URL is required") } - checks := []string{"blockdownload"} - if raw := os.Getenv("CHECKS"); raw != "" { - checks = strings.Split(raw, ",") + checks := strings.Split(os.Getenv("CHECKS"), ",") + if len(checks) == 0 || checks[0] == "" { + return Config{}, fmt.Errorf("CHECKS is required") } for _, name := range checks {