b1746c25af
gates / gates (push) Successful in 19s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
89 lines
3.4 KiB
Go
89 lines
3.4 KiB
Go
package osupdate
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/signedjobs"
|
|
)
|
|
|
|
// OpDockerStep is the signed op class of a Docker engine step (`11` §5.8): a ring-1 box takes an approved engine set,
|
|
// and every box takes an UNDO, only through it. CC may sign it until the first paying customer (R-530 ruling).
|
|
const OpDockerStep = "os_docker_step"
|
|
|
|
// DockerStepParams are the signed params. The wrapper compares Packages and Undo with the plan byte-for-byte.
|
|
type DockerStepParams struct {
|
|
ReleaseID string `json:"release_id"`
|
|
Packages []Package `json:"packages"`
|
|
Undo bool `json:"undo"`
|
|
VMID int `json:"vmid,omitempty"`
|
|
}
|
|
|
|
// DockerStepExecutor runs a verified os_docker_step (signedjobs.Executor). Guest finds the box's customer guest when
|
|
// the params name none; Gate (optional) takes the host-wide heavy-op gate so a step never runs beside a backup.
|
|
type DockerStepExecutor struct {
|
|
Leg *Leg
|
|
Guest func(ctx context.Context) (int, error)
|
|
Gate func(ctx context.Context) (release func(), err error)
|
|
}
|
|
|
|
// Execute implements signedjobs.Executor.
|
|
func (e DockerStepExecutor) Execute(ctx context.Context, op string, params json.RawMessage) error {
|
|
if op != OpDockerStep {
|
|
return signedjobs.ErrNoExecutor
|
|
}
|
|
so, ok := signedjobs.SignedOpFrom(ctx)
|
|
if !ok {
|
|
return fmt.Errorf("os_docker_step: no signed envelope in the context — the wrapper could not verify it")
|
|
}
|
|
var p DockerStepParams
|
|
if err := json.Unmarshal(params, &p); err != nil || len(p.Packages) == 0 {
|
|
return fmt.Errorf("os_docker_step: params must name the engine set: %v", err)
|
|
}
|
|
vmid := p.VMID
|
|
if vmid == 0 {
|
|
if e.Guest == nil {
|
|
return fmt.Errorf("os_docker_step: no vmid and no guest finder")
|
|
}
|
|
v, err := e.Guest(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("os_docker_step: find the customer guest: %w", err)
|
|
}
|
|
vmid = v
|
|
}
|
|
if e.Gate != nil {
|
|
release, err := e.Gate(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("os_docker_step: heavy-op gate busy (a backup or restore-test runs): %w", err)
|
|
}
|
|
defer release()
|
|
}
|
|
rep := e.Leg.RunDockerSigned(ctx, vmid, p, so.Blob, string(so.Sig))
|
|
switch rep.Outcome {
|
|
case "applied", "nothing":
|
|
if rep.Healthy {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("os_docker_step: %s (%s) %s", rep.Outcome, rep.HealthReason, string(rep.Refused))
|
|
}
|
|
|
|
// RunDockerSigned is one signed Docker step (ring 1 or an undo): live-restore first (decision 87, a no-op when on),
|
|
// then the docker layer with the signed envelope, which the wrapper verifies itself.
|
|
func (l *Leg) RunDockerSigned(ctx context.Context, vmid int, p DockerStepParams, blob []byte, sig string) Report {
|
|
runID := l.now().UTC().Format("20060102T150405Z")
|
|
lg := l.log().With("run", runID, "vmid", vmid, "trigger", "signed", "release", p.ReleaseID, "undo", p.Undo)
|
|
if err := l.EnsureLiveRestore(ctx, runID, vmid); err != nil {
|
|
return l.finish(ctx, lg, Report{RunID: runID, Layer: LayerDocker, Trigger: "signed", Ring: l.Block().Ring, VMID: vmid,
|
|
Mode: "apply", ReleaseID: p.ReleaseID, Outcome: "failed", HealthReason: "live-restore could not be turned on: " + err.Error()})
|
|
}
|
|
rid := p.ReleaseID
|
|
if rid == "" {
|
|
rid = "signed-" + runID
|
|
}
|
|
return l.runLayer(ctx, runID, LayerDocker, vmid, "signed", l.Block(), dockerOpts{releaseID: rid, packages: p.Packages,
|
|
undo: p.Undo, signed: map[string]string{"blob_b64": base64.StdEncoding.EncodeToString(blob), "sig": sig}})
|
|
}
|