mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
3c778e2b26
v6.1.0-rc.1 retired the legacy update endpoints before a replacement existed, so the UI's Update button failed with an internal-jargon 410 (issue #1564). This lands the replacement end to end: update_container is a typed agentexec operation with its own strict codec, durable receipts, and a request digest bound to the image digest the plan observed; the unified agent bridges execution to the Docker module's existing pull/backup/recreate/verify/rollback implementation (which now reports rollback attempt and outcome); and the container action executor plans, dispatches, and reconciles the operation with declared backup/rollback compensation truth. Containers advertise an admin-approval update capability while an image update with a stated current digest is detected. The legacy endpoints stay retired but return actionable copy. Proven live against a Colima daemon: single-container update, the issue-1564 shared-network-namespace update, and the full UI journey (Update button, governed review, approve, run) all completed with the namespace preserved and the backup retained.
530 lines
22 KiB
Go
530 lines
22 KiB
Go
package agentexec
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// MessageType identifies the type of WebSocket message
|
|
type MessageType string
|
|
|
|
const (
|
|
// Agent -> Server messages
|
|
MsgTypeAgentRegister MessageType = "agent_register"
|
|
MsgTypeAgentPing MessageType = "agent_ping"
|
|
MsgTypeCommandResult MessageType = "command_result"
|
|
MsgTypeHostStorageCleanupResult MessageType = "host_storage_cleanup_result"
|
|
MsgTypeHostUpdateResult MessageType = "host_update_result"
|
|
MsgTypeDockerContainerLifecycleResult MessageType = "docker_container_lifecycle_result"
|
|
MsgTypeDockerContainerUpdateResult MessageType = "docker_container_update_result"
|
|
MsgTypeOperationQueryResult MessageType = "agent_operation_query_result"
|
|
|
|
// Server -> Agent messages
|
|
MsgTypeRegistered MessageType = "registered"
|
|
MsgTypePong MessageType = "pong"
|
|
MsgTypeExecuteCmd MessageType = "execute_command"
|
|
MsgTypeHostStorageCleanup MessageType = "host_storage_cleanup"
|
|
MsgTypeReadFile MessageType = "read_file"
|
|
MsgTypeHostUpdate MessageType = "host_update"
|
|
MsgTypeDockerContainerLifecycle MessageType = "docker_container_lifecycle"
|
|
MsgTypeDockerContainerUpdate MessageType = "docker_container_update"
|
|
MsgTypeOperationQuery MessageType = "agent_operation_query"
|
|
MsgTypeDeployPreflight MessageType = "deploy_preflight"
|
|
MsgTypeDeployInstall MessageType = "deploy_install"
|
|
MsgTypeDeployCancelJob MessageType = "deploy_cancel"
|
|
|
|
// Agent -> Server messages (deploy)
|
|
MsgTypeDeployProgress MessageType = "deploy_progress"
|
|
)
|
|
|
|
// Message is the envelope for all WebSocket messages
|
|
type Message struct {
|
|
Type MessageType `json:"type"`
|
|
ID string `json:"id,omitempty"` // Unique message ID for request/response correlation
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Payload json.RawMessage `json:"payload,omitempty"`
|
|
}
|
|
|
|
// NewMessage creates a message envelope with a safely marshaled payload.
|
|
func NewMessage(messageType MessageType, id string, payload any) (Message, error) {
|
|
msg := Message{
|
|
Type: messageType,
|
|
ID: id,
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := msg.SetPayload(payload); err != nil {
|
|
return Message{}, err
|
|
}
|
|
return msg, nil
|
|
}
|
|
|
|
// SetPayload marshals and sets the payload for this message.
|
|
func (m *Message) SetPayload(payload any) error {
|
|
if payload == nil {
|
|
m.Payload = nil
|
|
return nil
|
|
}
|
|
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.Payload = encoded
|
|
return nil
|
|
}
|
|
|
|
// DecodePayload unmarshals the message payload into target.
|
|
func (m Message) DecodePayload(target any) error {
|
|
if len(m.Payload) == 0 {
|
|
return errors.New("message payload is empty")
|
|
}
|
|
return json.Unmarshal(m.Payload, target)
|
|
}
|
|
|
|
// AgentRegisterPayload is sent by agent on connection
|
|
type AgentRegisterPayload struct {
|
|
AgentID string `json:"agent_id"`
|
|
Hostname string `json:"hostname"`
|
|
Version string `json:"version"`
|
|
Platform string `json:"platform"` // "linux", "windows", "darwin"
|
|
Tags []string `json:"tags,omitempty"`
|
|
Token string `json:"token"` // API token for authentication
|
|
OperationReceiptVersion int `json:"operation_receipt_version,omitempty"`
|
|
}
|
|
|
|
// RegisteredPayload is sent by server after successful registration
|
|
type RegisteredPayload struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// ExecuteCommandPayload is sent by server to request command execution
|
|
type ExecuteCommandPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
Command string `json:"command"`
|
|
ApprovalID string `json:"approval_id,omitempty"`
|
|
ApprovalGrant *CommandApprovalGrant `json:"approval_grant,omitempty"`
|
|
TargetType string `json:"target_type"` // "agent", "container", "vm"
|
|
TargetID string `json:"target_id,omitempty"` // VMID for container/VM
|
|
Timeout int `json:"timeout,omitempty"` // seconds, 0 = default
|
|
// Trusted marks a payload as originating from a Pulse-internal subsystem
|
|
// whose command catalog is hardcoded and vetted (e.g. servicediscovery
|
|
// deep scans wrap read-only inspections in `docker exec`). When set, the
|
|
// server's command policy approval gate is bypassed for this command.
|
|
// This field must only be set by trusted internal call sites — never
|
|
// from a deserialised HTTP body or any user-driven path.
|
|
Trusted bool `json:"trusted,omitempty"`
|
|
|
|
authorization *commandAuthorizationContext
|
|
}
|
|
|
|
type commandAuthorizationContext struct {
|
|
OrgID string
|
|
ActionID string
|
|
}
|
|
|
|
// BindCommandAuthorization attaches server-owned approval scope to a command.
|
|
// The scope is deliberately not serializable, so provider arguments, HTTP
|
|
// payloads, relay callers, and agent wire messages cannot manufacture it.
|
|
func (p *ExecuteCommandPayload) BindCommandAuthorization(orgID, actionID string) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
p.authorization = &commandAuthorizationContext{
|
|
OrgID: strings.TrimSpace(orgID),
|
|
ActionID: strings.TrimSpace(actionID),
|
|
}
|
|
}
|
|
|
|
// ReadFilePayload is sent by server to request file content
|
|
type ReadFilePayload struct {
|
|
RequestID string `json:"request_id"`
|
|
Path string `json:"path"`
|
|
TargetType string `json:"target_type"`
|
|
TargetID string `json:"target_id,omitempty"`
|
|
MaxBytes int64 `json:"max_bytes,omitempty"` // 0 = default (1MB)
|
|
}
|
|
|
|
// CommandResultPayload is sent by agent with command execution result
|
|
type CommandResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
Success bool `json:"success"`
|
|
Stdout string `json:"stdout,omitempty"`
|
|
Stderr string `json:"stderr,omitempty"`
|
|
ExitCode int `json:"exit_code"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
const HostUpdateOperationInstall = "install_os_updates"
|
|
|
|
const (
|
|
DockerContainerOperationStart = "start_container"
|
|
DockerContainerOperationStop = "stop_container"
|
|
DockerContainerOperationRestart = "restart_container"
|
|
DockerContainerOperationUpdate = "update_container"
|
|
)
|
|
|
|
// DockerContainerLifecyclePayload is a closed container lifecycle operation.
|
|
// It carries an immutable container ID and request-bound before state, never
|
|
// command text, flags, a shell fragment, or a user-controlled container name.
|
|
type DockerContainerLifecyclePayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
Runtime string `json:"runtime"`
|
|
ContainerID string `json:"container_id"`
|
|
ExpectedState string `json:"expected_state"`
|
|
ExpectedStartedAt time.Time `json:"expected_started_at,omitempty"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
type DockerContainerLifecycleSnapshot struct {
|
|
ContainerID string `json:"container_id"`
|
|
State string `json:"state"`
|
|
Running bool `json:"running"`
|
|
StartedAt time.Time `json:"started_at,omitempty"`
|
|
RestartCount int `json:"restart_count"`
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
}
|
|
|
|
type DockerContainerLifecycleResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
ContainerID string `json:"container_id"`
|
|
ExecutionPhase string `json:"execution_phase"`
|
|
MutationStarted bool `json:"mutation_started"`
|
|
MutationCompleted bool `json:"mutation_completed"`
|
|
ReadbackRan bool `json:"readback_ran"`
|
|
Before DockerContainerLifecycleSnapshot `json:"before"`
|
|
After DockerContainerLifecycleSnapshot `json:"after"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
const (
|
|
DockerContainerPhasePreflight = "preflight"
|
|
DockerContainerPhaseMutate = "mutate"
|
|
DockerContainerPhaseVerify = "verify"
|
|
DockerContainerPhaseComplete = "complete"
|
|
)
|
|
|
|
// DockerContainerUpdatePayload is the closed, typed container image update
|
|
// operation. It carries an immutable container ID and the exact image digest
|
|
// the plan observed; the agent refuses when the container no longer runs that
|
|
// digest, then pulls the container's own configured image reference and
|
|
// recreates it from its inspected configuration. It never carries command
|
|
// text, flags, a pull reference, or a user-controlled container name.
|
|
type DockerContainerUpdatePayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
Runtime string `json:"runtime"`
|
|
ContainerID string `json:"container_id"`
|
|
ExpectedImageDigest string `json:"expected_image_digest"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
// DockerContainerUpdateResultPayload reports an update outcome. ContainerID
|
|
// is the container the request targeted; a successful update recreates it, so
|
|
// NewContainerID carries the replacement identity and the compensation fields
|
|
// say whether a backup existed and whether rollback was applied on failure.
|
|
type DockerContainerUpdateResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
ContainerID string `json:"container_id"`
|
|
ExecutionPhase string `json:"execution_phase"`
|
|
MutationStarted bool `json:"mutation_started"`
|
|
MutationCompleted bool `json:"mutation_completed"`
|
|
ReadbackRan bool `json:"readback_ran"`
|
|
NewContainerID string `json:"new_container_id,omitempty"`
|
|
ContainerName string `json:"container_name,omitempty"`
|
|
OldImageDigest string `json:"old_image_digest,omitempty"`
|
|
NewImageDigest string `json:"new_image_digest,omitempty"`
|
|
BackupCreated bool `json:"backup_created"`
|
|
BackupContainer string `json:"backup_container,omitempty"`
|
|
RollbackAttempted bool `json:"rollback_attempted"`
|
|
RolledBack bool `json:"rolled_back"`
|
|
After DockerContainerLifecycleSnapshot `json:"after"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
// DockerContainerUpdateOutcome is the neutral bridge between the Docker
|
|
// module's update implementation and the typed command channel, so the host
|
|
// command client does not import the Docker module directly.
|
|
type DockerContainerUpdateOutcome struct {
|
|
Success bool
|
|
ContainerName string
|
|
OldContainerID string
|
|
NewContainerID string
|
|
OldImageDigest string
|
|
NewImageDigest string
|
|
BackupCreated bool
|
|
BackupContainer string
|
|
RollbackAttempted bool
|
|
RolledBack bool
|
|
Error string
|
|
}
|
|
|
|
// HostUpdatePayload is the closed, typed host-package operation sent to a
|
|
// Unified Agent. It intentionally has no command or package-name fields: the
|
|
// agent owns the package-manager command catalog and always updates the whole
|
|
// bounded set returned by its own preflight simulation.
|
|
type HostUpdatePayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
ExpectedInventoryHash string `json:"expected_inventory_hash"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
type HostPackageUpdate struct {
|
|
Name string `json:"name"`
|
|
InstalledVersion string `json:"installed_version,omitempty"`
|
|
AvailableVersion string `json:"available_version,omitempty"`
|
|
}
|
|
|
|
type HostPackageUpdateSnapshot struct {
|
|
Supported bool `json:"supported"`
|
|
Manager string `json:"manager,omitempty"`
|
|
InventoryHash string `json:"inventory_hash,omitempty"`
|
|
PendingCount int `json:"pending_count"`
|
|
Packages []HostPackageUpdate `json:"packages,omitempty"`
|
|
CheckedAt time.Time `json:"checked_at,omitempty"`
|
|
RebootRequired bool `json:"reboot_required,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
const (
|
|
HostUpdateVerificationVerified = "verified"
|
|
HostUpdateVerificationFailed = "failed"
|
|
HostUpdateVerificationInconclusive = "inconclusive"
|
|
)
|
|
|
|
// HostUpdateResultPayload carries execution and read-after-write evidence
|
|
// independently. Success means the package-manager mutation completed;
|
|
// Verification states whether the postcondition was actually observed.
|
|
type HostUpdateResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Success bool `json:"success"`
|
|
ExecutionPhase string `json:"execution_phase"`
|
|
MutationStarted bool `json:"mutation_started"`
|
|
Before HostPackageUpdateSnapshot `json:"before"`
|
|
After HostPackageUpdateSnapshot `json:"after"`
|
|
HealthChecked bool `json:"health_checked"`
|
|
PackageManagerHealthy bool `json:"package_manager_healthy"`
|
|
RecoveryRequired bool `json:"recovery_required"`
|
|
Verification string `json:"verification"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
const (
|
|
HostUpdatePhasePreflight = "preflight"
|
|
HostUpdatePhaseRefresh = "refresh"
|
|
HostUpdatePhaseInstall = "install"
|
|
HostUpdatePhaseVerify = "verify"
|
|
HostUpdatePhaseComplete = "complete"
|
|
)
|
|
|
|
const HostStorageCleanupOperationPackageCache = "clean_package_cache"
|
|
|
|
const HostStorageCleanupMaxReportedBytes int64 = 1 << 60
|
|
|
|
// HostStorageCleanupPayload is the closed storage-pressure operation sent to
|
|
// a Unified Agent. The model cannot select paths or commands: the agent owns
|
|
// the single APT package-cache target and command catalog.
|
|
type HostStorageCleanupPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
RequestDigest string `json:"request_digest"`
|
|
ExpectedFingerprint string `json:"expected_fingerprint"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
type HostStorageCleanupSnapshot struct {
|
|
Supported bool `json:"supported"`
|
|
Provider string `json:"provider,omitempty"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
ReclaimableBytes int64 `json:"reclaimable_bytes"`
|
|
CheckedAt time.Time `json:"checked_at,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
const (
|
|
HostStorageCleanupVerificationVerified = "verified"
|
|
HostStorageCleanupVerificationFailed = "failed"
|
|
HostStorageCleanupVerificationInconclusive = "inconclusive"
|
|
)
|
|
|
|
// HostStorageCleanupResultPayload separates mutation success from the
|
|
// read-after-write observation of reclaimed cache bytes.
|
|
type HostStorageCleanupResultPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
ActionID string `json:"action_id"`
|
|
Success bool `json:"success"`
|
|
ExecutionPhase string `json:"execution_phase"`
|
|
MutationStarted bool `json:"mutation_started"`
|
|
Before HostStorageCleanupSnapshot `json:"before"`
|
|
After HostStorageCleanupSnapshot `json:"after"`
|
|
ReclaimedBytes int64 `json:"reclaimed_bytes"`
|
|
Verification string `json:"verification"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
}
|
|
|
|
const (
|
|
HostStorageCleanupPhasePreflight = "preflight"
|
|
HostStorageCleanupPhaseClean = "clean"
|
|
HostStorageCleanupPhaseVerify = "verify"
|
|
HostStorageCleanupPhaseComplete = "complete"
|
|
)
|
|
|
|
// ConnectedAgent represents an agent connected via WebSocket
|
|
type ConnectedAgent struct {
|
|
AgentID string
|
|
Hostname string
|
|
Version string
|
|
Platform string
|
|
Tags []string
|
|
ConnectedAt time.Time
|
|
OperationReceiptVersion int
|
|
}
|
|
|
|
// --- Deploy protocol payloads ---
|
|
|
|
const MaxDeployParallel = 10
|
|
|
|
// NormalizeDeployMaxParallel applies the shared server/agent concurrency
|
|
// contract before the value reaches worker or channel allocation boundaries.
|
|
func NormalizeDeployMaxParallel(value, defaultValue int) int {
|
|
if value <= 0 {
|
|
value = defaultValue
|
|
}
|
|
if value > MaxDeployParallel {
|
|
return MaxDeployParallel
|
|
}
|
|
return value
|
|
}
|
|
|
|
// DeployPreflightPayload is sent by the server to request SSH preflight checks
|
|
// against cluster peer nodes.
|
|
type DeployPreflightPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
JobID string `json:"job_id"`
|
|
Targets []DeployPreflightTarget `json:"targets"`
|
|
PulseURL string `json:"pulse_url"` // URL peers must reach
|
|
MaxParallel int `json:"max_parallel,omitempty"` // 0 = sequential
|
|
Timeout int `json:"timeout,omitempty"` // per-target seconds, 0 = 120
|
|
}
|
|
|
|
// DeployPreflightTarget identifies a single node to preflight-check via SSH.
|
|
type DeployPreflightTarget struct {
|
|
TargetID string `json:"target_id"` // deploy.Target.ID
|
|
NodeName string `json:"node_name"` // Proxmox node name
|
|
NodeIP string `json:"node_ip"` // IP to SSH into
|
|
}
|
|
|
|
// DeployInstallPayload is sent by the server to request agent installation
|
|
// on cluster peer nodes via SSH.
|
|
type DeployInstallPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
JobID string `json:"job_id"`
|
|
Targets []DeployInstallTarget `json:"targets"`
|
|
PulseURL string `json:"pulse_url"`
|
|
MaxParallel int `json:"max_parallel,omitempty"`
|
|
Timeout int `json:"timeout,omitempty"` // per-target seconds, 0 = 300
|
|
}
|
|
|
|
// DeployInstallTarget identifies a single node for agent installation via SSH.
|
|
type DeployInstallTarget struct {
|
|
TargetID string `json:"target_id"` // deploy.Target.ID
|
|
NodeName string `json:"node_name"`
|
|
NodeIP string `json:"node_ip"`
|
|
Arch string `json:"arch"` // from preflight: amd64, arm64
|
|
BootstrapToken string `json:"bootstrap_token"` // per-target enrollment token
|
|
}
|
|
|
|
// DeployCancelPayload is sent by the server to cancel an in-flight deploy job.
|
|
type DeployCancelPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
JobID string `json:"job_id"`
|
|
}
|
|
|
|
// DeployProgressPayload is sent by the agent to stream progress events for
|
|
// preflight checks and install operations. Multiple progress messages are
|
|
// sent per request, with the final message having Final=true.
|
|
type DeployProgressPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
JobID string `json:"job_id"`
|
|
TargetID string `json:"target_id,omitempty"` // empty for job-level events
|
|
Phase DeployProgressPhase `json:"phase"`
|
|
Status DeployStepStatus `json:"status"`
|
|
Message string `json:"message"`
|
|
Data string `json:"data,omitempty"` // JSON blob for structured results
|
|
Final bool `json:"final"` // true = last message for this request
|
|
}
|
|
|
|
// DeployProgressPhase identifies which stage of the deploy lifecycle this
|
|
// progress event belongs to.
|
|
type DeployProgressPhase string
|
|
|
|
const (
|
|
DeployPhasePreflightSSH DeployProgressPhase = "preflight_ssh"
|
|
DeployPhasePreflightArch DeployProgressPhase = "preflight_arch"
|
|
DeployPhasePreflightReach DeployProgressPhase = "preflight_reachability"
|
|
DeployPhasePreflightAgent DeployProgressPhase = "preflight_existing_agent"
|
|
DeployPhasePreflightComplete DeployProgressPhase = "preflight_complete"
|
|
DeployPhaseInstallTransfer DeployProgressPhase = "install_transfer"
|
|
DeployPhaseInstallExecute DeployProgressPhase = "install_execute"
|
|
DeployPhaseInstallEnrollWait DeployProgressPhase = "install_enroll_wait"
|
|
DeployPhaseInstallComplete DeployProgressPhase = "install_complete"
|
|
DeployPhaseCanceled DeployProgressPhase = "canceled"
|
|
DeployPhaseJobComplete DeployProgressPhase = "job_complete"
|
|
)
|
|
|
|
// DeployStepStatus is the outcome of a single progress step.
|
|
type DeployStepStatus string
|
|
|
|
const (
|
|
DeployStepStarted DeployStepStatus = "started"
|
|
DeployStepOK DeployStepStatus = "ok"
|
|
DeployStepFailed DeployStepStatus = "failed"
|
|
DeployStepSkipped DeployStepStatus = "skipped"
|
|
)
|
|
|
|
// PreflightResultData is the structured data for a completed preflight check.
|
|
// Serialized as JSON in DeployProgressPayload.Data.
|
|
type PreflightResultData struct {
|
|
Arch string `json:"arch,omitempty"`
|
|
HasAgent bool `json:"has_agent"`
|
|
PulseReachable bool `json:"pulse_reachable"`
|
|
SSHReachable bool `json:"ssh_reachable"`
|
|
ErrorDetail string `json:"error_detail,omitempty"`
|
|
}
|
|
|
|
// InstallResultData is the structured data for a completed install step.
|
|
// Serialized as JSON in DeployProgressPayload.Data.
|
|
type InstallResultData struct {
|
|
ExitCode int `json:"exit_code"`
|
|
Output string `json:"output,omitempty"`
|
|
}
|