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 { unlock := l.lockPass(true) defer unlock() l.sendUnsentLocked(ctx) // R-868 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}}) }