910c0fcd76
build / build (push) Failing after 16s
- libraryagent:0700 目录内 0600 unix socket + SO_PEERCRED 同 uid, 固定 binary/归档/日志/项目/写入者声明,请求只能选范围与作业号 - API:POST/GET /api/v1/library/captures(入队 library.capture + 读队列状态) - worker:library.capture handler 走 socket;启动先 recover;不挂 Docker socket、 不持有来源凭据 - web:整库版本页显示真实作业状态并可发起封存(未接线/无权限给出原因) - scripts/library-agent-install.sh:构建、安装 systemd user 服务并读回权限
93 lines
2.9 KiB
Go
93 lines
2.9 KiB
Go
package libraryagent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
// ClientConfig is what a governance process needs: one socket path. It never
|
|
// carries source credentials or Docker access — those stay with the agent.
|
|
type ClientConfig struct {
|
|
Socket string
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// Result is the agent's answer to one capture/status/recover request.
|
|
type Result struct {
|
|
OK bool
|
|
Scope string
|
|
Job string
|
|
SnapshotID string
|
|
Replay bool
|
|
Error string
|
|
Payload json.RawMessage
|
|
}
|
|
|
|
// Capture asks the agent to seal one library scope. The job id makes the call
|
|
// idempotent: repeating it returns the already sealed candidate.
|
|
func Capture(ctx context.Context, config ClientConfig, scope, job string) (Result, error) {
|
|
return call(ctx, config, map[string]string{"action": "capture", "scope": scope, "job": job})
|
|
}
|
|
|
|
// Status probes the agent (source wiring, writer group, archive usage).
|
|
func Status(ctx context.Context, config ClientConfig) (Result, error) {
|
|
return call(ctx, config, map[string]string{"action": "status"})
|
|
}
|
|
|
|
// Recover restores writers from an interrupted freeze journal.
|
|
func Recover(ctx context.Context, config ClientConfig) (Result, error) {
|
|
return call(ctx, config, map[string]string{"action": "recover"})
|
|
}
|
|
|
|
func call(ctx context.Context, config ClientConfig, request map[string]string) (Result, error) {
|
|
if config.Socket == "" {
|
|
return Result{}, errors.New("library agent socket is not configured")
|
|
}
|
|
timeout := config.Timeout
|
|
if timeout <= 0 {
|
|
timeout = 2 * time.Minute
|
|
}
|
|
bounded, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
var dialer net.Dialer
|
|
conn, err := dialer.DialContext(bounded, "unix", config.Socket)
|
|
if err != nil {
|
|
return Result{}, errors.New("library agent is unreachable")
|
|
}
|
|
defer conn.Close()
|
|
if deadline, ok := bounded.Deadline(); ok {
|
|
_ = conn.SetDeadline(deadline)
|
|
}
|
|
if err = json.NewEncoder(conn).Encode(request); err != nil {
|
|
return Result{}, errors.New("library agent request failed")
|
|
}
|
|
var response struct {
|
|
OK bool `json:"ok"`
|
|
Scope string `json:"scope"`
|
|
Job string `json:"job"`
|
|
Error string `json:"error"`
|
|
Payload json.RawMessage `json:"payload"`
|
|
}
|
|
if err = json.NewDecoder(conn).Decode(&response); err != nil {
|
|
return Result{}, errors.New("library agent returned an invalid response")
|
|
}
|
|
result := Result{OK: response.OK, Scope: response.Scope, Job: response.Job, Error: response.Error, Payload: response.Payload}
|
|
var payload struct {
|
|
SnapshotID string `json:"snapshot_id"`
|
|
Replay bool `json:"replay"`
|
|
}
|
|
if len(response.Payload) > 0 && json.Unmarshal(response.Payload, &payload) == nil {
|
|
result.SnapshotID, result.Replay = payload.SnapshotID, payload.Replay
|
|
}
|
|
if !result.OK {
|
|
if result.Error == "" {
|
|
result.Error = "library agent refused the request"
|
|
}
|
|
return result, errors.New(result.Error)
|
|
}
|
|
return result, nil
|
|
}
|