Files
omnichannel-configserver-mcp/internal/configserver/api_operate.go
T
2026-10-07 20:13:23 +07:00

150 lines
4.7 KiB
Go

package configserver
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
)
// api_operate.go — жизненный цикл сервиса и загрузка фронта.
// Deploy поднимает сервис (docker-compose up -d).
func (a *API) Deploy(ctx context.Context, appID int) (*TaskAck, error) {
return a.lifecycle(ctx, "deploy", appID)
}
// Restart перезапускает сервис (down + up -d).
func (a *API) Restart(ctx context.Context, appID int) (*TaskAck, error) {
return a.lifecycle(ctx, "restart", appID)
}
// Down останавливает сервис (docker-compose down).
func (a *API) Down(ctx context.Context, appID int) (*TaskAck, error) {
return a.lifecycle(ctx, "down", appID)
}
// Migrate запускает миграцию (docker-compose run migration).
func (a *API) Migrate(ctx context.Context, appID int) (*TaskAck, error) {
return a.lifecycle(ctx, "migrate", appID)
}
// lifecycle — общий вызов POST /api/<action>/<app_id>, возвращающий task_id.
func (a *API) lifecycle(ctx context.Context, action string, appID int) (*TaskAck, error) {
ctx, cancel := a.withTimeout(ctx)
defer cancel()
out := &TaskAck{}
path := "/api/" + action + "/" + strconv.Itoa(appID)
if err := a.Sess.postJSON(ctx, path, nil, out); err != nil {
return nil, err
}
return out, nil
}
// UpdateFront загружает локальную сборку фронта и создаёт задачу её доставки
// агенту. Каталог обязан лежать внутри front_roots (jail, fail-closed).
func (a *API) UpdateFront(ctx context.Context, appID int, buildDir string) (*FrontAck, error) {
files, err := a.collectFrontFiles(buildDir)
if err != nil {
return nil, err
}
ctx, cancel := a.withUploadTimeout(ctx)
defer cancel()
raw, err := a.Sess.postMultipart(ctx, "/api/update_front/"+strconv.Itoa(appID), files)
if err != nil {
return nil, err
}
out := &FrontAck{}
if err := json.Unmarshal(raw, out); err != nil {
return nil, fmt.Errorf("разбор ответа update_front: %w", err)
}
return out, nil
}
// collectFrontFiles собирает файлы сборки, проверяя containment и лимиты.
func (a *API) collectFrontFiles(buildDir string) ([]UploadFile, error) {
if len(a.Srv.FrontRoots) == 0 {
return nil, ErrNoFrontRoots
}
root, err := resolveInsideRoots(buildDir, a.Srv.FrontRoots)
if err != nil {
return nil, err
}
var (
files []UploadFile
total int64
)
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
// Симлинки и спецфайлы пропускаем: загружаем только обычные файлы.
if !d.Type().IsRegular() {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
total += info.Size()
if total > a.Srv.MaxUploadBytes {
return validationErrorf("размер сборки превышает лимит %d байт", a.Srv.MaxUploadBytes)
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
files = append(files, UploadFile{Name: filepath.ToSlash(rel), Data: data})
return nil
})
if err != nil {
return nil, fmt.Errorf("чтение сборки %s: %w", buildDir, err)
}
if len(files) == 0 {
return nil, validationErrorf("в каталоге %s нет файлов", buildDir)
}
return files, nil
}
// resolveInsideRoots приводит путь к абсолютному и проверяет, что после
// разрешения симлинков он лежит внутри одного из разрешённых корней.
func resolveInsideRoots(target string, roots []string) (string, error) {
abs, err := filepath.Abs(target)
if err != nil {
return "", fmt.Errorf("некорректный путь %q: %w", target, err)
}
resolved, err := filepath.EvalSymlinks(abs)
if err != nil {
return "", validationErrorf("путь %q недоступен: %v", target, err)
}
resolved = filepath.Clean(resolved)
for _, r := range roots {
rootAbs, err := filepath.Abs(r)
if err != nil {
continue
}
// Симлинки в самом корне не должны «съесть» проверку — разрешаем, если можем.
if rootResolved, err := filepath.EvalSymlinks(rootAbs); err == nil {
rootAbs = rootResolved
}
rootAbs = filepath.Clean(rootAbs)
if resolved == rootAbs || strings.HasPrefix(resolved, rootAbs+string(os.PathSeparator)) {
return resolved, nil
}
}
return "", validationErrorf("путь %q вне разрешённых front_roots", target)
}