e4de1ab4d8
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
254 lines
8.1 KiB
Go
254 lines
8.1 KiB
Go
package lkgateway
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"net"
|
||
"net/http"
|
||
neturl "net/url"
|
||
"strings"
|
||
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
"os"
|
||
"time"
|
||
)
|
||
|
||
// Status — состояние одной проверяемой подсистемы.
|
||
type Status struct {
|
||
Name string `json:"name"`
|
||
OK bool `json:"ok"`
|
||
Message string `json:"message,omitempty"`
|
||
Detail string `json:"detail,omitempty"`
|
||
// Optional — компонент не обязателен для работы с НРД. Его «не-OK»
|
||
// не делает систему «не готовой» (напр. callback в ЛК).
|
||
Optional bool `json:"optional,omitempty"`
|
||
}
|
||
|
||
// SystemStatus — все проверки.
|
||
type SystemStatus struct {
|
||
Profile string `json:"profile"`
|
||
Provider string `json:"crypto_provider"`
|
||
Checks []Status `json:"checks"`
|
||
CheckedAt time.Time `json:"checked_at"`
|
||
}
|
||
|
||
// CheckOptions — что и как проверять.
|
||
type CheckOptions struct {
|
||
PostgresDSN string // если пусто — режим in-memory, проверки нет
|
||
CryptoSocket string // путь до UDS crypto-service
|
||
NSDAdapterURL string // например http://127.0.0.1:8082
|
||
LKCallbackURL string // куда шлём callback (lk-emulator)
|
||
Profile string // имя профиля nsdadapter (guest-gost...)
|
||
CryptoProvider string // BJ_CRYPTO_PROVIDER (stub|cryptopro|...)
|
||
Timeout time.Duration // таймаут на одну проверку
|
||
}
|
||
|
||
// CheckAll выполняет все доступные проверки и возвращает SystemStatus.
|
||
func CheckAll(ctx context.Context, o CheckOptions) SystemStatus {
|
||
if o.Timeout == 0 {
|
||
o.Timeout = 2 * time.Second
|
||
}
|
||
out := SystemStatus{
|
||
Profile: o.Profile,
|
||
Provider: o.CryptoProvider,
|
||
CheckedAt: time.Now().UTC(),
|
||
}
|
||
|
||
out.Checks = append(out.Checks, checkPostgres(ctx, o))
|
||
out.Checks = append(out.Checks, checkCryptoSocket(o))
|
||
out.Checks = append(out.Checks, checkNSDAdapter(ctx, o))
|
||
out.Checks = append(out.Checks, ishVersionStatus(ctx))
|
||
out.Checks = append(out.Checks, checkLKCallback(ctx, o))
|
||
|
||
return out
|
||
}
|
||
|
||
func checkPostgres(ctx context.Context, o CheckOptions) Status {
|
||
s := Status{Name: "База данных PostgreSQL"}
|
||
if o.PostgresDSN == "" {
|
||
s.OK = true
|
||
s.Optional = true
|
||
s.Message = "in-memory — данные не сохраняются между перезапусками"
|
||
return s
|
||
}
|
||
pctx, cancel := context.WithTimeout(ctx, o.Timeout)
|
||
defer cancel()
|
||
pool, err := pgxpool.New(pctx, o.PostgresDSN)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "ошибка подключения: " + err.Error()
|
||
return s
|
||
}
|
||
defer pool.Close()
|
||
if err := pool.Ping(pctx); err != nil {
|
||
s.OK = false
|
||
s.Message = "не отвечает: " + err.Error()
|
||
return s
|
||
}
|
||
s.OK = true
|
||
s.Message = "подключена, репозиторий m2m_core.deals"
|
||
return s
|
||
}
|
||
|
||
func checkCryptoSocket(o CheckOptions) Status {
|
||
s := Status{Name: "crypto-service (UDS)"}
|
||
if o.CryptoSocket == "" {
|
||
s.OK = false
|
||
s.Message = "BJ_CRYPTO_SOCKET не задан"
|
||
return s
|
||
}
|
||
info, err := os.Stat(o.CryptoSocket)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "сокет недоступен"
|
||
s.Detail = err.Error()
|
||
return s
|
||
}
|
||
if info.Mode()&os.ModeSocket == 0 {
|
||
s.OK = false
|
||
s.Message = "путь существует, но это не сокет"
|
||
s.Detail = o.CryptoSocket
|
||
return s
|
||
}
|
||
// Пробуем подключиться.
|
||
d := net.Dialer{Timeout: o.Timeout}
|
||
conn, err := d.Dial("unix", o.CryptoSocket)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "сокет существует, но не отвечает"
|
||
s.Detail = err.Error()
|
||
return s
|
||
}
|
||
_ = conn.Close()
|
||
s.OK = true
|
||
s.Message = "сокет открыт"
|
||
s.Detail = o.CryptoSocket
|
||
if o.CryptoProvider == "stub" || o.CryptoProvider == "" {
|
||
if devMode(globalRC) {
|
||
s.Message += ", провайдер stub (реальная криптография не подключена)"
|
||
} else {
|
||
s.Message += ", средство подписи не настроено"
|
||
}
|
||
} else {
|
||
s.Message += ", провайдер " + o.CryptoProvider
|
||
}
|
||
return s
|
||
}
|
||
|
||
func checkNSDAdapter(ctx context.Context, o CheckOptions) Status {
|
||
s := Status{Name: "Интеграционный шлюз НРД"}
|
||
if o.NSDAdapterURL == "" {
|
||
s.OK = true
|
||
s.Optional = true
|
||
s.Message = "не настроен — обмен с НРД не выполняется"
|
||
if devMode(globalRC) {
|
||
s.Message = "не подключён — режим эмуляции (mock)"
|
||
}
|
||
return s
|
||
}
|
||
// У ИШ нет /healthz — проверяем рабочий эндпоинт Web API (engine/state
|
||
// отвечает 200 «Running», когда движок поднят).
|
||
st := httpHealth(ctx, o.NSDAdapterURL+"/api/admin/engine/state", o.Timeout, s)
|
||
if st.OK {
|
||
st.Message = "подключён, движок работает"
|
||
}
|
||
return st
|
||
}
|
||
|
||
func checkLKCallback(ctx context.Context, o CheckOptions) Status {
|
||
s := Status{Name: "Callback в личный кабинет", Optional: true}
|
||
if o.LKCallbackURL == "" {
|
||
s.OK = false
|
||
s.Message = "не настроен — уведомления в ЛК отключены (необязательно для работы с НРД)"
|
||
return s
|
||
}
|
||
return httpHealth(ctx, o.LKCallbackURL+"/healthz", o.Timeout, s)
|
||
}
|
||
|
||
func httpHealth(ctx context.Context, url string, timeout time.Duration, s Status) Status {
|
||
// Подключаемся по реальному url (для локальных сервисов — loopback), но в
|
||
// Detail показываем адрес/имя сервера, а не «localhost» — так оператору
|
||
// понятно, на каком узле работает компонент. Для loopback добавляем пометку,
|
||
// что порт не открыт наружу (это подпись, а не кликабельная ссылка).
|
||
disp := displayURL(url)
|
||
if isLoopbackURL(url) {
|
||
disp += " (только локально — доступ с сервера или через SSH-туннель, порт не открыт наружу)"
|
||
}
|
||
c := &http.Client{Timeout: timeout}
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "не получилось собрать запрос"
|
||
s.Detail = err.Error()
|
||
return s
|
||
}
|
||
resp, err := c.Do(req)
|
||
if err != nil {
|
||
s.OK = false
|
||
s.Message = "недоступен"
|
||
s.Detail = err.Error()
|
||
return s
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
s.OK = false
|
||
s.Message = "HTTP " + http.StatusText(resp.StatusCode)
|
||
s.Detail = disp
|
||
return s
|
||
}
|
||
s.OK = true
|
||
s.Message = "OK"
|
||
s.Detail = disp
|
||
return s
|
||
}
|
||
|
||
// displayHost возвращает отображаемое имя/адрес этого сервера: значение
|
||
// BJ_PUBLIC_HOST (если задано, напр. DNS-имя) либо системный hostname.
|
||
func displayHost() string {
|
||
if h := strings.TrimSpace(os.Getenv("BJ_PUBLIC_HOST")); h != "" {
|
||
return h
|
||
}
|
||
if h, err := os.Hostname(); err == nil && h != "" {
|
||
return h
|
||
}
|
||
return "server"
|
||
}
|
||
|
||
// isLoopbackURL сообщает, указывает ли url на loopback-хост (сервис слушает
|
||
// только на этом сервере и недоступен извне).
|
||
func isLoopbackURL(raw string) bool {
|
||
u, err := neturl.Parse(raw)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
switch u.Hostname() {
|
||
case "localhost", "127.0.0.1", "0.0.0.0", "::1":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// displayURL заменяет loopback-хост (localhost/127.0.0.1/0.0.0.0/[::1]) в url на
|
||
// displayHost(). Внешние адреса не трогает. Порт и путь сохраняются.
|
||
func displayURL(raw string) string {
|
||
u, err := neturl.Parse(raw)
|
||
if err != nil || u.Host == "" {
|
||
return raw
|
||
}
|
||
host := u.Hostname()
|
||
switch host {
|
||
case "localhost", "127.0.0.1", "0.0.0.0", "::1":
|
||
newHost := displayHost()
|
||
if p := u.Port(); p != "" {
|
||
u.Host = net.JoinHostPort(newHost, p)
|
||
} else {
|
||
u.Host = newHost
|
||
}
|
||
return u.String()
|
||
}
|
||
return raw
|
||
}
|
||
|
||
// ErrUnknown — общий placeholder.
|
||
var ErrUnknown = errors.New("lkgateway: unknown error")
|