hostmaintenance

package
v0.46.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 25, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

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

View Source
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.

View Source
const (
	OperationUpdatesPlan  = "updates.plan"
	OperationUpdatesApply = "updates.apply"
	OperationReboot       = "reboot"
)

Operations reported in Result.Operation.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
const SchemaVersion = "stackkit.host-maintenance/v1"

SchemaVersion identifies the result document of every host maintenance operation.

Variables

View Source
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.

View Source
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

func IsHeld(name string) bool

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

func PlanDigest(packages []Package) string

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 Command

type Command struct {
	Name string
	Args []string
}

Command is one closed argv. No shell interprets it.

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

func (e Engine) Apply(ctx context.Context, planDigest string) (Result, error)

Apply installs exactly the planned package set when the plan digest still matches a fresh simulation.

func (Engine) Plan

func (e Engine) Plan(ctx context.Context) (Result, error)

Plan refreshes the package index and reports what apply would install.

func (Engine) Reboot

func (e Engine) Reboot(ctx context.Context, delay time.Duration) (Result, error)

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

func (e Engine) RebootGuard(ctx context.Context, maxWait time.Duration) error

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 LocalHost

type LocalHost struct{}

LocalHost is the real node.

func (LocalHost) Executable

func (LocalHost) Executable() (string, error)

func (LocalHost) Exists

func (LocalHost) Exists(path string) bool

func (LocalHost) LockHolder

func (LocalHost) LockHolder(path string) (bool, int, error)

func (LocalHost) LookPath

func (LocalHost) LookPath(name string) (string, error)

func (LocalHost) Now

func (LocalHost) Now() time.Time

func (LocalHost) Processes

func (LocalHost) Processes() ([]Process, error)

func (LocalHost) ReadFile

func (LocalHost) ReadFile(path string) ([]byte, error)

func (LocalHost) Run

func (LocalHost) Run(ctx context.Context, command Command) (Output, error)

func (LocalHost) Sleep

func (LocalHost) Sleep(ctx context.Context, d time.Duration) error

func (LocalHost) TryLock

func (LocalHost) TryLock(path string, createMode os.FileMode) (func(), bool, error)

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 Output

type Output struct {
	Stdout   string
	Stderr   string
	ExitCode int
}

Output is what a command printed and how it exited.

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

type Process struct {
	PID  int
	Exe  string
	Args []string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL