Files
2026-10-07 20:13:23 +07:00

290 lines
9.1 KiB
Go

package configserver
import (
"context"
"net/url"
"strconv"
"strings"
"time"
)
// api_observe.go — read-only операции (наблюдение и диагностика).
// WhoAmI — ответ GET /api/whoami.
type WhoAmI struct {
Authenticated bool `json:"authenticated"`
Username string `json:"username"`
}
// Capabilities — результат capability-probe: версия API и доступные группы
// JSON-эндпоинтов v1.1.0. Нужен, чтобы инструменты не «тихо» ломались на
// старых стендах, а возвращали внятное сообщение.
type Capabilities struct {
Version string `json:"version"`
Features map[string]bool `json:"features"`
}
// Health проверяет живость сервера (GET /health, без авторизации).
func (a *API) Health(ctx context.Context) (int, string, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
resp, err := a.Sess.getPublic(ctx, "/health")
if err != nil {
return 0, "", err
}
return resp.status, strings.TrimSpace(string(resp.body)), nil
}
// WhoAmI проверяет текущую авторизацию.
func (a *API) WhoAmI(ctx context.Context) (*WhoAmI, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &WhoAmI{}
if err := a.Sess.getJSON(ctx, "/api/whoami", nil, out); err != nil {
return nil, err
}
return out, nil
}
// Probe определяет версию API. Ключевой признак v1.1.0 — публичный JSON
// `/api/whoami` (в <=1.0.20 его нет, middleware отвечает 401).
func (a *API) Probe(ctx context.Context) (*Capabilities, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
resp, err := a.Sess.getPublic(ctx, "/api/whoami")
if err != nil {
return nil, err
}
caps := &Capabilities{Version: "legacy (<1.1.0)", Features: map[string]bool{}}
if resp.status == 200 && strings.Contains(resp.ctype, "json") {
caps.Version = "1.1.0"
for _, f := range []string{
"api_login", "api_whoami", "application_detail",
"set_compose", "set_env", "tasks_filter", "task_get", "update_front",
} {
caps.Features[f] = true
}
}
return caps, nil
}
// ListApplications возвращает список сервисов.
func (a *API) ListApplications(ctx context.Context, includePresence bool) ([]Application, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
q := url.Values{}
if includePresence {
q.Set("include_presence", "true")
}
var out []Application
if err := a.Sess.getJSON(ctx, "/api/applications", q, &out); err != nil {
return nil, err
}
return out, nil
}
// GetApplication возвращает детализацию сервиса.
func (a *API) GetApplication(ctx context.Context, id int) (*ApplicationDetail, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &ApplicationDetail{}
if err := a.Sess.getJSON(ctx, "/api/application/"+strconv.Itoa(id), nil, out); err != nil {
return nil, err
}
return out, nil
}
// GetConfig возвращает текущие compose/env сервиса (эндпоинт агента, публичный).
func (a *API) GetConfig(ctx context.Context, id int) (*RawConfig, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &RawConfig{}
if err := a.Sess.getJSON(ctx, "/api/get_config/"+strconv.Itoa(id), nil, out); err != nil {
return nil, err
}
return out, nil
}
// EnvFiles возвращает имена текущих env-файлов сервиса.
func (a *API) EnvFiles(ctx context.Context, id int) ([]string, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
var out []string
if err := a.Sess.getJSON(ctx, "/api/env_files/"+strconv.Itoa(id), nil, &out); err != nil {
return nil, err
}
return out, nil
}
// ComposeVersion возвращает конкретную версию compose.
func (a *API) ComposeVersion(ctx context.Context, versionID int) (*ComposeVersion, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &ComposeVersion{}
if err := a.Sess.getJSON(ctx, "/api/compose_version/"+strconv.Itoa(versionID), nil, out); err != nil {
return nil, err
}
return out, nil
}
// EnvVersion возвращает конкретную версию env-файла.
func (a *API) EnvVersion(ctx context.Context, versionID int) (*EnvVersion, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &EnvVersion{}
if err := a.Sess.getJSON(ctx, "/api/env_version/"+strconv.Itoa(versionID), nil, out); err != nil {
return nil, err
}
return out, nil
}
// EnvVersions возвращает все версии конкретного env-файла.
func (a *API) EnvVersions(ctx context.Context, appID int, filename string) ([]EnvVersion, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
path := "/api/env_versions/" + strconv.Itoa(appID) + "/" + url.PathEscape(filename)
var out []EnvVersion
if err := a.Sess.getJSON(ctx, path, nil, &out); err != nil {
return nil, err
}
return out, nil
}
// TaskFilter — фильтры списка задач.
type TaskFilter struct {
HostIP string
Status string
TaskType string
Limit int
}
// ListTasks возвращает задачи с фильтрами (GET /api/tasks).
func (a *API) ListTasks(ctx context.Context, f TaskFilter) ([]Task, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
q := url.Values{}
if f.HostIP != "" {
q.Set("host_ip", f.HostIP)
}
if f.Status != "" {
q.Set("status", f.Status)
}
if f.TaskType != "" {
q.Set("task_type", f.TaskType)
}
if f.Limit > 0 {
q.Set("limit", strconv.Itoa(f.Limit))
}
var out []Task
if err := a.Sess.getJSON(ctx, "/api/tasks", q, &out); err != nil {
return nil, err
}
return out, nil
}
// GetTask возвращает задачу по task_id.
func (a *API) GetTask(ctx context.Context, taskID string) (*Task, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &Task{}
if err := a.Sess.getJSON(ctx, "/api/task/"+url.PathEscape(taskID), nil, out); err != nil {
return nil, err
}
return out, nil
}
// WaitTask опрашивает задачу до завершения (completed/failed) или до истечения
// task_poll_max_sec. Возвращает последнее известное состояние задачи; признак
// незавершённости — Status == "pending"/"in_progress". Loop агента
// последовательный, поэтому ожидание строго ограничено конфигом.
func (a *API) WaitTask(ctx context.Context, taskID string) (*Task, error) {
deadline := time.Now().Add(time.Duration(a.Srv.TaskPollMaxSec) * time.Second)
interval := time.Duration(a.Srv.TaskPollIntervalSec) * time.Second
if interval <= 0 {
interval = time.Second
}
for {
task, err := a.GetTask(ctx, taskID)
if err != nil {
return nil, err
}
if task.Status == "completed" || task.Status == "failed" {
return task, nil
}
if !time.Now().Before(deadline) {
return task, nil
}
select {
case <-ctx.Done():
return task, nil
case <-time.After(interval):
}
}
}
// DeploymentFiles возвращает schema/manifest/prebuild_vars/ip_overrides.
func (a *API) DeploymentFiles(ctx context.Context) (*DeploymentFiles, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &DeploymentFiles{}
if err := a.Sess.getJSON(ctx, "/api/deployment/files", nil, out); err != nil {
return nil, err
}
return out, nil
}
// IPMatch возвращает диагностику сопоставления IP схемы и агентов.
func (a *API) IPMatch(ctx context.Context) (*IPMatch, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &IPMatch{}
if err := a.Sess.getJSON(ctx, "/api/deployment/ip_match", nil, out); err != nil {
return nil, err
}
return out, nil
}
// ReleaseJob возвращает статус фоновой загрузки релиза.
func (a *API) ReleaseJob(ctx context.Context) (*ReleaseJob, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &ReleaseJob{}
if err := a.Sess.getJSON(ctx, "/api/deployment/release_job", nil, &out); err != nil {
return nil, err
}
return out, nil
}
// DeploymentTasks возвращает последние release-задачи (sync/start).
func (a *API) DeploymentTasks(ctx context.Context) ([]Task, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
var out []Task
if err := a.Sess.getJSON(ctx, "/api/deployment/tasks", nil, &out); err != nil {
return nil, err
}
return out, nil
}
// Stats возвращает счётчики версий.
func (a *API) Stats(ctx context.Context) (*Stats, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &Stats{}
if err := a.Sess.getJSON(ctx, "/api/stats", nil, out); err != nil {
return nil, err
}
return out, nil
}
// ServiceMap возвращает карту серверов и сервисов.
func (a *API) ServiceMap(ctx context.Context) (*ServiceMap, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &ServiceMap{}
if err := a.Sess.getJSON(ctx, "/api/service_map", nil, out); err != nil {
return nil, err
}
return out, nil
}