Files
Bridge-and-Join-s/internal/lkgateway/checks.go
T

254 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")