ci / api / api (push) Successful in 50s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m0s
ci / db / db (push) Successful in 58s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m27s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m39s
ci / ts / ts (packages + web) (push) Successful in 2m40s
154 lines
5.3 KiB
Go
154 lines
5.3 KiB
Go
// Package management authorizes server administration and bridges fixed operations
|
|
// to the host worker. The API never receives a Docker socket or executes a shell.
|
|
package management
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"pipu/api/internal/api/gen"
|
|
"pipu/api/internal/auth"
|
|
"pipu/api/internal/buildinfo"
|
|
"pipu/api/internal/platform/apperr"
|
|
)
|
|
|
|
type Bridge interface {
|
|
Call(context.Context, string, string, any, any) error
|
|
}
|
|
|
|
type Mailbox struct{ Dir string }
|
|
|
|
func (m Mailbox) Call(ctx context.Context, actor, operation string, body, out any) error {
|
|
heartbeat, err := os.ReadFile(filepath.Join(m.Dir, "heartbeat"))
|
|
if err != nil {
|
|
return apperr.New(503, "management.unavailable", "host agent is not installed or running")
|
|
}
|
|
at, err := strconv.ParseFloat(strings.TrimSpace(string(heartbeat)), 64)
|
|
if err != nil || time.Since(time.Unix(int64(at), 0)) > 60*time.Second {
|
|
return apperr.New(503, "management.unavailable", "host agent heartbeat expired")
|
|
}
|
|
id := uuid.NewString()
|
|
request := filepath.Join(m.Dir, id+".request.json")
|
|
response := filepath.Join(m.Dir, id+".response.json")
|
|
raw, err := json.Marshal(map[string]any{"operation": operation, "actor": actor, "body": body, "created_at": time.Now().Unix()})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp(m.Dir, ".request-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
if _, err = tmp.Write(raw); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err = tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err = os.Rename(tmp.Name(), request); err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(response)
|
|
defer os.Remove(request)
|
|
ctx, cancel := context.WithTimeout(ctx, 40*time.Second)
|
|
defer cancel()
|
|
ticker := time.NewTicker(100 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return apperr.New(504, "management.timeout", "host agent did not respond; check status before retrying")
|
|
case <-ticker.C:
|
|
raw, err := os.ReadFile(response)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var result struct {
|
|
Data json.RawMessage `json:"data"`
|
|
Code string `json:"code"`
|
|
Status int `json:"status"`
|
|
}
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
return err
|
|
}
|
|
if result.Code != "" {
|
|
status := result.Status
|
|
if status < 400 || status > 599 {
|
|
status = 502
|
|
}
|
|
return apperr.New(status, result.Code, "host management operation failed")
|
|
}
|
|
return json.Unmarshal(result.Data, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
type Handler struct{ bridge Bridge }
|
|
|
|
func NewHandler(bridge Bridge) *Handler { return &Handler{bridge: bridge} }
|
|
|
|
func (h *Handler) call(ctx context.Context, admin bool, operation string, body, out any) error {
|
|
p, ok := auth.PrincipalFrom(ctx)
|
|
if !ok {
|
|
return apperr.Unauthorized(auth.CodeUnauthorized, "authentication required")
|
|
}
|
|
if (admin && p.Role != auth.RoleAdmin) || (!admin && !p.Allowed(auth.PermManageUpdates)) {
|
|
return apperr.Forbidden(auth.CodeForbidden, "server management permission required")
|
|
}
|
|
return h.bridge.Call(ctx, p.UserID.String(), operation, body, out)
|
|
}
|
|
|
|
func (h *Handler) GetServerUpdates(ctx context.Context, req gen.GetServerUpdatesRequestObject) (gen.GetServerUpdatesResponseObject, error) {
|
|
var out gen.ServerUpdates
|
|
err := h.call(ctx, false, "updates.get", map[string]any{"source_id": req.Params.SourceId,
|
|
"channel": req.Params.Channel, "version": req.Params.Version, "fresh": req.Params.Fresh,
|
|
"_running_version": buildinfo.Version, "_running_commit": buildinfo.Revision}, &out)
|
|
out.CurrentVersion = buildinfo.Version
|
|
return gen.GetServerUpdates200JSONResponse(out), err
|
|
}
|
|
|
|
func (h *Handler) SaveServerUpdateSources(ctx context.Context, req gen.SaveServerUpdateSourcesRequestObject) (gen.SaveServerUpdateSourcesResponseObject, error) {
|
|
var out gen.ServerSources
|
|
err := h.call(ctx, false, "sources.save", req.Body, &out)
|
|
return gen.SaveServerUpdateSources200JSONResponse(out), err
|
|
}
|
|
|
|
func (h *Handler) StartServerUpdate(ctx context.Context, req gen.StartServerUpdateRequestObject) (gen.StartServerUpdateResponseObject, error) {
|
|
var out gen.ServerJob
|
|
err := h.call(ctx, false, "updates.start", map[string]any{
|
|
"source_id": req.Body.SourceId, "channel": req.Body.Channel, "version": req.Body.Version,
|
|
"expected_commit": req.Body.ExpectedCommit, "_running_version": buildinfo.Version, "_running_commit": buildinfo.Revision,
|
|
}, &out)
|
|
return gen.StartServerUpdate202JSONResponse(out), err
|
|
}
|
|
|
|
func (h *Handler) GetServerConfig(ctx context.Context, _ gen.GetServerConfigRequestObject) (gen.GetServerConfigResponseObject, error) {
|
|
var out gen.ServerConfig
|
|
err := h.call(ctx, true, "config.get", nil, &out)
|
|
return gen.GetServerConfig200JSONResponse(out), err
|
|
}
|
|
|
|
func (h *Handler) SaveServerConfig(ctx context.Context, req gen.SaveServerConfigRequestObject) (gen.SaveServerConfigResponseObject, error) {
|
|
var out gen.ServerConfig
|
|
err := h.call(ctx, true, "config.save", req.Body, &out)
|
|
return gen.SaveServerConfig200JSONResponse(out), err
|
|
}
|
|
|
|
func (h *Handler) ApplyServerConfig(ctx context.Context, _ gen.ApplyServerConfigRequestObject) (gen.ApplyServerConfigResponseObject, error) {
|
|
var out gen.ServerJob
|
|
err := h.call(ctx, true, "config.apply", nil, &out)
|
|
return gen.ApplyServerConfig202JSONResponse(out), err
|
|
}
|