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 }