// 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 }