Documentation
¶
Overview ¶
Package hostmaintenance plans and carries out operating-system package updates and reboots on one node.
It is the node-side half of host maintenance: Techstack dispatches the typed operation, the pinned StackKits CLI executes it here. Every host interaction goes through the Host interface so the decisions (what is pending, what is held, whether the node may be touched at all) are testable without root and on any operating system.
Index ¶
- Constants
- Variables
- func IsHeld(name string) bool
- func PlanDigest(packages []Package) string
- type Change
- type Command
- type Engine
- type FailureError
- type Host
- type LocalHost
- func (LocalHost) Executable() (string, error)
- func (LocalHost) Exists(path string) bool
- func (LocalHost) LockHolder(path string) (bool, int, error)
- func (LocalHost) LookPath(name string) (string, error)
- func (LocalHost) Now() time.Time
- func (LocalHost) Processes() ([]Process, error)
- func (LocalHost) ReadFile(path string) ([]byte, error)
- func (LocalHost) Run(ctx context.Context, command Command) (Output, error)
- func (LocalHost) Sleep(ctx context.Context, d time.Duration) error
- func (LocalHost) TryLock(path string, createMode os.FileMode) (func(), bool, error)
- type OSInfo
- type Output
- type Package
- type Problem
- type Process
- type RefusalError
- type Result
Constants ¶
const ( // UpdateUnitPrefix names the transient systemd unit apply runs apt in. UpdateUnitPrefix = "kombify-host-update-" // RebootUnitPrefix names the transient timer and service that reboot. RebootUnitPrefix = "kombify-host-reboot-" // MaintenanceLockPath serializes host maintenance operations on a node: // apply holds it from admission until its update unit ends, reboot while // it schedules, and the reboot guard while it requests the reboot. // It lives in /run, which only root can write: /run/lock is // world-writable, so any local user could pre-create and hold it. MaintenanceLockPath = "/run/kombify-host-maintenance.lock" )
Paths and unit names the node-side contract depends on.
const ( OperationUpdatesPlan = "updates.plan" OperationUpdatesApply = "updates.apply" OperationReboot = "reboot" )
Operations reported in Result.Operation.
const ( CodeUnsupportedPackageManager = "unsupported_package_manager" CodeUnsupportedOS = "unsupported_os" CodePlanStale = "plan_stale" CodePackageManagerBusy = "package_manager_busy" CodeControlPlaneHost = "control_plane_host" CodeUnattendedRebootUnsupported = "unattended_reboot_unsupported" CodeDpkgBroken = "dpkg_broken" )
Refusal codes. A refusal is written before any side effect: nothing on the host changed.
const ( FailureHostProbe = "host_probe_failed" FailureRefresh = "package_index_refresh_failed" FailureSimulation = "simulation_failed" FailureInstallSetRejected = "install_set_rejected" FailureInstall = "install_failed" FailureSchedule = "reboot_schedule_failed" // FailureWaitExpired accompanies OutcomeRunning: the CLI stopped waiting, // the update unit did not stop. FailureWaitExpired = "apply_wait_expired" )
Failure codes. A failure happened while executing; for apply it may have changed some packages, which Result.Changes records.
const ( OutcomeApplied = "applied" OutcomeNoop = "noop" OutcomeFailed = "failed" // OutcomeRunning means the CLI stopped waiting while the update unit keeps // running. dpkg is never interrupted by a CLI or agent timeout. OutcomeRunning = "running" )
Apply outcomes.
const ( PlanTimeout = 3 * time.Minute ApplyWaitTimeout = 20 * time.Minute MinRebootDelay = time.Second MaxRebootDelay = 5 * time.Minute // RebootGuardMaxWait bounds how long the reboot guard waits at fire time // for a package operation to finish. When it expires the reboot is // abandoned, never forced; boot_id stays the same. RebootGuardMaxWait = 15 * time.Minute )
Bounded durations. The plan bound covers the whole command; the apply bound is the CLI's wait for the update unit, not a limit on dpkg.
const ( ControlPlaneUnit = "techstack.service" ControlPlaneExecutable = "techstack" ControlPlaneCommand = "serve" ControlPlaneImage = "ghcr.io/kombifyio/techstack" ControlPlaneService = "techstack" )
The Techstack control plane as it is shipped: the packaged systemd unit (kombify-Techstack packaging/techstack.service, `/usr/bin/techstack serve`), the container image (`/app/techstack serve` in ghcr.io/kombifyio/techstack) and the self-hosted Compose service `techstack`. Every enrolled worker runs the same binary as `techstack agent`, which is not a control plane.
const HoldScope = "" /* 128-byte string literal not displayed */
HoldScope is reported with every plan so no reader mistakes the hold for a system-wide apt hold.
const SchemaVersion = "stackkit.host-maintenance/v1"
SchemaVersion identifies the result document of every host maintenance operation.
Variables ¶
var ErrRebootAbandoned = errors.New("reboot abandoned")
ErrRebootAbandoned is returned by the guard when the node stayed busy past its wait; the reboot did not happen.
var ErrStillRunning = errors.New("host update is still running in its systemd unit")
ErrStillRunning is returned when apply stopped waiting for a unit that is still running.
Functions ¶
func IsHeld ¶
IsHeld reports whether a package is held by default: the container runtime is upgraded deliberately, never as a side effect of OS updates, because upgrading it restarts every workload on the node.
The hold binds `stackkit host updates apply` only. unattended-upgrades is governed by the StackKit base policy, not by this list.
func PlanDigest ¶
PlanDigest is `sha256:` plus the hex SHA-256 of the lexically sorted `name=version` lines of the packages apply would install, each line terminated by a line feed. Held packages are not part of it.
Types ¶
type Change ¶
type Change struct {
Name string `json:"name"`
Before string `json:"before"`
After string `json:"after"`
}
Change is one package's installed version before and after apply. An empty version means the package was not installed.
type Engine ¶
type Engine struct {
Host Host
// Progress receives rollout-style progress events. Nil discards them.
Progress func(phase, status, message string, attrs map[string]string)
}
Engine runs host maintenance operations against a Host.
func (Engine) Apply ¶
Apply installs exactly the planned package set when the plan digest still matches a fresh simulation.
func (Engine) Reboot ¶
Reboot schedules a guarded reboot after delay and returns without waiting.
The timer does not run a bare `systemctl reboot`: it runs this binary's reboot guard, which checks again at fire time, waits for any package operation to finish, and requests the reboot while holding the dpkg locks.
func (Engine) RebootGuard ¶
RebootGuard runs when the reboot timer fires. It re-checks the node, waits up to maxWait for package operations and host updates to finish, then requests the reboot while holding the maintenance and dpkg locks, so no apt or dpkg run can start before the shutdown stops this process. When the node stays busy it abandons the reboot rather than killing dpkg.
The wait is bounded by a poll count, not the wall clock, so a clock step (NTP) can neither end it early nor stretch it.
type FailureError ¶
type FailureError struct{ Problem Problem }
FailureError is returned when an operation failed while executing.
func (*FailureError) Error ¶
func (e *FailureError) Error() string
type Host ¶
type Host interface {
// LookPath resolves an executable like exec.LookPath.
LookPath(name string) (string, error)
// ReadFile reads a file like os.ReadFile.
ReadFile(path string) ([]byte, error)
// Exists reports whether a path exists.
Exists(path string) bool
// LockHolder reports whether a process holds a POSIX write lock on path,
// and its PID when known. A missing file is not locked.
LockHolder(path string) (held bool, pid int, err error)
// TryLock takes a POSIX write lock on path without waiting. A missing
// file is created with createMode, or is an error when createMode is 0.
// acquired is false when another process holds the lock. release drops
// it; the kernel drops it when the process exits.
TryLock(path string, createMode os.FileMode) (release func(), acquired bool, err error)
// Processes lists every running process with its executable and argv.
// A process that cannot be read for lack of permission is an error: an
// incomplete list cannot rule a process out. Processes that exit while
// they are read are skipped.
Processes() ([]Process, error)
// Executable is the absolute path of the running stackkit binary.
Executable() (string, error)
// Run executes a command. A nonzero exit is reported in Output.ExitCode
// with a nil error; the error is for a command that could not run or a
// context that ended.
Run(ctx context.Context, command Command) (Output, error)
// Now is the host clock.
Now() time.Time
// Sleep waits for d or until ctx ends.
Sleep(ctx context.Context, d time.Duration) error
}
Host is every interaction host maintenance has with the node.
type OSInfo ¶
type OSInfo struct {
ID string `json:"id"`
VersionID string `json:"version_id,omitempty"`
PrettyName string `json:"pretty_name,omitempty"`
}
OSInfo is what /etc/os-release reported.
type Package ¶
type Package struct {
Name string `json:"name"`
From string `json:"from"`
To string `json:"to"`
Security bool `json:"security"`
}
Package is one pending upgrade.
type Problem ¶
type Problem struct {
Code string `json:"code"`
Message string `json:"message"`
Guidance []string `json:"guidance,omitempty"`
}
Problem is a refusal or failure with a stable code. Message is diagnostic and must not be used as a policy input.
type Process ¶
Process is one running process: its executable path (as seen from its own mount namespace for a container) and its argv.
type RefusalError ¶
type RefusalError struct{ Problem Problem }
RefusalError is returned when an operation refused before any side effect.
func (*RefusalError) Error ¶
func (e *RefusalError) Error() string
type Result ¶
type Result struct {
SchemaVersion string `json:"schema_version"`
Operation string `json:"operation"`
ObservedAt time.Time `json:"observed_at"`
OS *OSInfo `json:"os,omitempty"`
PackageManager string `json:"package_manager,omitempty"`
// Plan fields; apply repeats them from its re-simulation.
PlanDigest string `json:"plan_digest,omitempty"`
PendingCount *int `json:"pending_count,omitempty"`
SecurityCount *int `json:"security_count,omitempty"`
Packages []Package `json:"packages,omitempty"`
Held []Package `json:"held,omitempty"`
KeptBack []string `json:"kept_back,omitempty"`
HoldScope string `json:"hold_scope,omitempty"`
RebootLikely *bool `json:"reboot_likely,omitempty"`
DpkgProblems []string `json:"dpkg_problems,omitempty"`
// Apply fields.
Outcome string `json:"outcome,omitempty"`
Unit string `json:"unit,omitempty"`
Changes []Change `json:"changes,omitempty"`
RebootRequired *bool `json:"reboot_required,omitempty"`
RebootRequiredPackages []string `json:"reboot_required_packages,omitempty"`
AutoMarksNotRestored []string `json:"auto_marks_not_restored,omitempty"`
StartedAt time.Time `json:"started_at,omitzero"`
FinishedAt time.Time `json:"finished_at,omitzero"`
// Reboot fields.
Scheduled *bool `json:"scheduled,omitempty"`
BootIDBefore string `json:"boot_id_before,omitempty"`
ScheduledAt time.Time `json:"scheduled_at,omitzero"`
DelaySeconds int `json:"delay_seconds,omitempty"`
Refusal *Problem `json:"refusal,omitempty"`
Failure *Problem `json:"failure,omitempty"`
}
Result is the stackkit.host-maintenance/v1 document.