v0.85.0: self-update reworked — in-guest pull + agent swap (Phase 1)
Replaces the dead in-container docker-compose self-update (composePath doesn't exist in the LXC guest). The controller now docker-logins+pulls the target image in-guest (shared socket, its registry token), then delegates the container swap to the host agent (agentapi.SwapController -> POST /controller/swap), which owns the restart + health-verify + rollback. Removed performUpdate compose flow / updateComposeFile / composePath. NewUpdater takes an AgentSwapper. DryRun reports agent_reachable + pull_capable. UI button + poll unchanged; latest-only. Tests: up-to-date no-op / pull-fail agent-not-called / happy pull-then-swap / no-agent unavailable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
This commit is contained in:
@@ -372,6 +372,53 @@ func (c *Client) GuestReboot(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SwapResult mirrors the agent's 202 from POST /controller/swap (agentic controller update, Phase 1).
|
||||
type SwapResult struct {
|
||||
Status string `json:"status"` // "swapping"
|
||||
PreviousImage string `json:"previous_image"` // image before the swap (for the UI/log)
|
||||
TargetImage string `json:"target_image"`
|
||||
}
|
||||
|
||||
// SwapController asks the agent to swap the in-guest controller to `image` (which the controller has
|
||||
// already pulled into the guest's docker storage). The agent responds 202 and performs the swap+verify
|
||||
// +rollback asynchronously, EXTERNALLY to this controller container (so it survives this process being
|
||||
// killed by the swap). Latest-only is enforced by the caller (queryRegistry); the agent re-validates
|
||||
// the ref shape.
|
||||
func (c *Client) SwapController(ctx context.Context, image string) (SwapResult, error) {
|
||||
var out SwapResult
|
||||
body, err := c.post(ctx, "/controller/swap", map[string]string{"image": image})
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return out, fmt.Errorf("agentapi: decode /controller/swap: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SwapStatus mirrors the agent's GET /controller/swap/status (observability for the post-restart UI).
|
||||
type SwapStatus struct {
|
||||
State string `json:"state"` // none | swapping | done | failed
|
||||
InFlight bool `json:"in_flight"`
|
||||
Current string `json:"current"`
|
||||
Previous string `json:"previous"`
|
||||
Target string `json:"target"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// SwapStatus reads the last/in-flight swap outcome for this guest.
|
||||
func (c *Client) SwapStatus(ctx context.Context) (SwapStatus, error) {
|
||||
var out SwapStatus
|
||||
body, err := c.get(ctx, "/controller/swap/status")
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return out, fmt.Errorf("agentapi: decode /controller/swap/status: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// EjectResult mirrors POST /disks/eject (the dependent-guest warning).
|
||||
type EjectResult struct {
|
||||
VMID int `json:"vmid"`
|
||||
|
||||
Reference in New Issue
Block a user