service

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

Documentation

Overview

Package service installs a verified Debuglet role payload as a service the host's service manager supervises. It owns one coherent contract: exactly one unit, one persistent state directory, one service account and one readiness record per installed role instance. It starts, stops and inspects only the units it wrote, and it never migrates a database, replays interrupted work or touches a unit it does not own.

Index

Constants

View Source
const (
	// ReadyTimeout bounds the observation of a published readiness record
	// after the service manager reports the unit started. It matches the
	// startup bound of the foreground role commands.
	ReadyTimeout = 30 * time.Second
	// StopBudget is the unit's TimeoutStopSec: the time the service manager
	// gives the daemon to revoke admission, join its owned local work and
	// exit before it is killed. It has to exceed the daemon's own bounded
	// local join (scheduler.CleanupTimeout per session) with room for the
	// operating system to reap the process group.
	StopBudget = 45 * time.Second
	// StopObservation is how long a stop is waited for by default. It
	// exceeds StopBudget, because a daemon that uses its whole budget has
	// not failed: reporting it incomplete would be this command's
	// impatience rather than the daemon's.
	StopObservation = StopBudget + 30*time.Second
	// DrainBudget is the default whole-operation bound for a drain. It
	// exceeds the stop observation so a drain reports what the service
	// manager actually did rather than its own impatience.
	DrainBudget = StopObservation + 15*time.Second
)
View Source
const (
	DefaultHTTPPort = 9000
	DefaultGRPCPort = 9001
)

Default dispatcher ports. They match the foreground dispatcher role, so an executor pointed at a managed dispatcher needs no extra configuration.

View Source
const DefaultAccount = "debuglet"

DefaultAccount is the unprivileged service account the generated units use. The Ansible deployment provisions the same name.

View Source
const MaintenanceFileEnv = "DEBUGLET_MAINTENANCE_FILE"

MaintenanceFileEnv is the environment variable a generated dispatcher unit sets to point the daemon at its admission switch. The dispatcher defines the same name and reads the same record; internal/dispatcher's maintenance test checks the two against each other, so the writer here and the reader there cannot drift apart unnoticed.

Variables

View Source
var ErrDrainIncomplete = errors.New("drain incomplete: local ownership has not joined")

ErrDrainIncomplete reports a drain whose local ownership did not join inside its budget. The node is neither serving nor known to be finished.

View Source
var ErrStagingRoot = errors.New("an alternate root stages files only; it cannot drive the host service manager")

ErrStagingRoot reports an operation that would have to act on the host's service manager while the caller asked for an alternate filesystem root.

Functions

func AdministrationDirectory

func AdministrationDirectory(root string) string

AdministrationDirectory holds what only an administrator may write: the installed record of each instance and each dispatcher's maintenance switch. They are deliberately outside the state directory, because that directory is handed to the service account, and an account that can write its own directory can replace anything in it. A daemon must not be able to tell a later privileged command where to look, what to remove or what to chown.

func ClearMaintenance

func ClearMaintenance(path string) (bool, error)

ClearMaintenance resumes submission admission by removing the switch. A dispatcher reads the switch for every submission, so admission resumes without a restart.

func RecordPath

func RecordPath(root string, role storagecheck.Role, name string) string

RecordPath is the installed record of one instance.

func StateDirectory

func StateDirectory(root string, role storagecheck.Role, name string) string

StateDirectory is where a role instance keeps its persistent state. It holds only what the daemon itself serves: its database, its role identity and its generated configuration.

func Unit

func Unit(p Profile) (string, error)

Unit renders the service-manager unit of one profile. The text is a pure function of the profile, so an unchanged installation rewrites byte-identical content and a changed payload version is visible in the unit itself.

The unit states four things an operator has to be able to rely on:

  • It starts one verified payload by absolute path. No search path, no working-directory lookup and no shell are involved.
  • It runs as an existing unprivileged account with no new privileges. Only enrolled automatic-counter executors receive the four packet capabilities. Writes stay within persistent/runtime state and, for shared executors, the delegated cgroup subtree.
  • Stopping it sends SIGTERM to the daemon alone and gives it the whole stop budget to revoke admission and join its owned local work. Only after that budget does the service manager kill the remaining processes.
  • Started is not ready. The daemon publishes a readiness record in its runtime directory; that record, not process creation, is what an installation or a restart observes.

func UnitDirectory

func UnitDirectory(root string) string

UnitDirectory is where generated units are written below a root.

func UnitName

func UnitName(role storagecheck.Role, name string) string

UnitName returns the unit one role instance owns. A separate unit per instance keeps every operation on exactly one service: an install, a stop or an uninstall of one role never names, reloads or restarts another.

func WriteMaintenance

func WriteMaintenance(path string, reason string, at time.Time) error

WriteMaintenance stops submission admission on a managed dispatcher. The record is published atomically, mode 0644, in the administration directory: the running daemon reads it without any change to its privileges and cannot write, replace or unlink it, so a dispatcher cannot take itself out of maintenance.

Writing it stops one thing: the admission of new submissions. Accepted work keeps its persistence and its schedule, connected executors keep running, results keep being reported and every query keeps answering.

Types

type Account

type Account struct{ UID, GID int }

Account is the resolved service account a unit runs as.

type ArchiveReport added in v0.3.0

type ArchiveReport struct {
	Operation string             `json:"operation"`
	Name      string             `json:"name"`
	Unit      string             `json:"unit"`
	DryRun    bool               `json:"dry_run"`
	Run       sqlite.ArchivedRun `json:"run"`
}

type DrainOptions

type DrainOptions struct {
	// Reason is the operator note the dispatcher switch carries.
	Reason string
	// Timeout bounds the whole drain. Zero uses DrainBudget.
	Timeout time.Duration
	// Disable also stops the unit from starting again on its own, so a
	// reboot during maintenance does not undo the drain. It applies to a
	// role that is drained by being stopped: a dispatcher is not stopped,
	// and its switch is a file an administrator owns that survives a reboot
	// on its own, so this says nothing about one.
	Disable bool
}

Drain removes one managed role from eligible capacity and reports what happened to the work it held. The two roles are drained by different means because they hold different things:

  • An executor is stopped. Stopping is what revokes its control eligibility, signals its active runs and joins its own local work; this package adds no second lifecycle controller beside it. Only executors that were actually stopped are affected: every other executor keeps serving, because nothing in a drain touches another unit.
  • A dispatcher is not stopped. It keeps serving its accepted work, its executors and its queries, and only the admission of new submissions is switched off.

The wait-versus-cancel policy is explicit and is the daemon's own, not a new one: a drain waits for local ownership to join inside the stop budget and cancels nothing beyond what stopping already signals. Queued work stays persisted and is never replayed. Work that was already running is signaled and joined; a run that ends as a result of that is reported by the ordinary terminal path, whose delivery to the dispatcher may still be unknown.

A drain that does not join inside its budget is reported as incomplete. It is then not a drain at all: nothing may be deleted, upgraded or reinitialized on the strength of it, because the process may still own the database.

type DrainReport

type DrainReport struct {
	Operation string `json:"operation"`
	Role      string `json:"role"`
	Name      string `json:"name"`
	Unit      string `json:"unit"`
	// Outcome is drained, paused, resumed, started or incomplete.
	Outcome string `json:"outcome"`
	// Joined reports proven local completion: the service manager reports
	// the unit inactive with a successful result and a zero exit status,
	// which is the daemon saying it released what it owned. It is the only
	// field that may be used to authorize deletion, an upgrade or closing
	// the database.
	Joined  bool   `json:"joined"`
	Enabled bool   `json:"enabled"`
	Active  string `json:"active,omitempty"`
	Ready   bool   `json:"ready,omitempty"`
	// Disposition is read from the persistent state once, after the join.
	Disposition *scheduler.Disposition `json:"disposition,omitempty"`
	StateDir    string                 `json:"state_dir,omitempty"`
	Changed     []string               `json:"changed,omitempty"`
	Note        string                 `json:"note,omitempty"`
}

DrainReport is what a drain or its reversal observed.

type Installer

type Installer struct {
	// contains filtered or unexported fields
}

Installer performs the managed-service operations of one root.

func New

func New(options Options) (*Installer, error)

New returns an installer for the supplied options.

func (*Installer) ArchiveRun added in v0.3.0

func (i *Installer) ArchiveRun(ctx context.Context, name string, id uuid.UUID, reason string, apply bool) (ArchiveReport, error)

ArchiveRun operates only on a drained managed executor. It neither stops nor restarts a service: an incomplete drain cannot be overridden by this command.

func (*Installer) CheckCertificateRenewal added in v0.3.0

func (i *Installer) CheckCertificateRenewal(ctx context.Context, name string) (Profile, error)

CheckCertificateRenewal requires an enrolled managed executor with a clean, disabled shutdown. An expired old certificate remains usable for recovery; enrollment is authorized by a fresh account-issued token, not that certificate. Administrators must exclude concurrent starts and state changes throughout.

func (*Installer) Drain

func (i *Installer) Drain(ctx context.Context, role storagecheck.Role, name string, options DrainOptions) (DrainReport, error)

Drain performs the drain of one installed role instance.

func (*Installer) Install

func (i *Installer) Install(ctx context.Context, request Request, assets demo.Assets, start, enable bool) (Report, error)

Install writes, or brings up to date, one managed role instance from a verified payload and, when start is true, starts it and observes its readiness record. It is repeatable: running it again with the same payload and request changes nothing and reports nothing changed.

It never restarts a running daemon, never migrates a database, never creates an account and never touches a unit other than this instance's own.

func (*Installer) Prune added in v0.3.0

func (i *Installer) Prune(ctx context.Context, prefix, version, component string, dryRun bool) (Report, error)

Prune removes one verified inactive administrator-owned system package. User-owned installation prefixes are not supported. Retained service records pin their payload even after uninstall, so recovery never loses its binary. The shared installer lock prevents a simultaneous install changing the CLI link. Operators must stop foreground roles before pruning their version.

func (*Installer) RenewCertificate added in v0.3.0

func (i *Installer) RenewCertificate(ctx context.Context, name string, certificate []byte) error

RenewCertificate atomically publishes only a certificate valid for the saved key and retained trust roots. State, key, configuration, unit and record stay intact. It rechecks shutdown after the enrollment request has completed.

func (*Installer) ReplacePackage added in v0.3.0

func (i *Installer) ReplacePackage(ctx context.Context, request Request, target demo.Assets) (Report, error)

ReplacePackage selects an already verified full payload for a drained, disabled enrolled executor. The operator must retain a stopped-state backup and exclude concurrent administration. No state is migrated, replayed or relocated, and no service is started or enabled.

func (*Installer) Resume

func (i *Installer) Resume(ctx context.Context, role storagecheck.Role, name string) (DrainReport, error)

Resume reverses a drain: it clears a dispatcher's admission switch, or it enables and starts a drained executor again and observes its readiness. It replays nothing: work retained from before the drain stays quarantined.

func (*Installer) Start

func (i *Installer) Start(ctx context.Context, role storagecheck.Role, name string) (Report, error)

Start starts an installed instance and observes its readiness record.

func (*Installer) Status

func (i *Installer) Status(ctx context.Context, role storagecheck.Role, name string) (Report, error)

Status reports the installed contract and what the manager and the readiness record currently say about it, without changing anything.

func (*Installer) Stop

func (i *Installer) Stop(ctx context.Context, role storagecheck.Role, name string) (Report, error)

Stop stops an installed instance within the stop budget and verifies that the manager actually reports it down. It leaves the unit enabled: stopping for a moment is not the same decision as removing a node from service, which is what Drain is for.

func (*Installer) Uninstall

func (i *Installer) Uninstall(ctx context.Context, role storagecheck.Role, name string, purge bool) (Report, error)

Uninstall stops the instance, disables it and removes the unit it owns. The persistent state directory is kept unless purge is set, and purge is refused until the manager reports the unit stopped: until local ownership has joined, no database, identity or result may be deleted.

type Maintenance

type Maintenance struct {
	SchemaVersion int    `json:"schema_version"`
	Paused        bool   `json:"paused"`
	Reason        string `json:"reason,omitempty"`
	Since         string `json:"since,omitempty"`
}

Maintenance is the dispatcher admission switch as an operator sees it.

func ReadMaintenance

func ReadMaintenance(path string) (Maintenance, error)

ReadMaintenance reports the switch an operator previously wrote. An absent switch is reported as not paused, with no error.

type Manager

type Manager interface {
	// Reload makes the manager re-read unit files from disk.
	Reload(ctx context.Context) error
	// Enable and Disable change only whether the unit starts at boot.
	Enable(ctx context.Context, unit string) error
	Disable(ctx context.Context, unit string) error
	// Start and Stop are synchronous: Stop returns when the manager has
	// finished stopping the unit or its own stop timeout expired.
	Start(ctx context.Context, unit string) error
	Stop(ctx context.Context, unit string) error
	// State reports what the manager believes about one unit.
	State(ctx context.Context, unit string) (UnitState, error)
}

Manager is the service manager an installation drives. It is an interface for one reason: nothing in this package may depend on running as root on a host with a live service manager, so the file operations, the unit contents and the readiness observation are exercised against a recorder instead.

type Options

type Options struct {
	// Root prefixes every managed path; empty means the real root.
	Root string
	// Manager is the service manager to drive. Required.
	Manager Manager
	// LookupAccount resolves the service account. nil uses the host's user
	// database.
	LookupAccount func(user, group string) (Account, error)
	// Chown applies the service account to one already-open managed file.
	// nil uses the operating system's fchown on the descriptor itself.
	Chown func(file *os.File, uid, gid int) error
	// ReadyTimeout bounds the readiness observation. Zero uses ReadyTimeout.
	ReadyTimeout time.Duration
	// StopTimeout bounds a stop and the verification that it finished. Zero
	// uses StopObservation, which allows for a daemon that uses its whole
	// shutdown budget.
	StopTimeout time.Duration
}

Options configure an installer. The three function fields are the only seams: they exist so the file operations, the generated unit and the readiness observation can be exercised without a live service manager, a provisioned account or root privileges.

type Profile

type Profile struct {
	Role storagecheck.Role `json:"role"`
	Name string            `json:"name"`
	// Root is the filesystem prefix every path below is derived from.
	Root string `json:"root"`
	// Unit is the service-manager unit this instance owns.
	Unit string `json:"unit"`
	// User and Group are the service account the daemon runs as. The
	// account has to exist already: installing a service never creates,
	// modifies or removes an account.
	User  string `json:"user"`
	Group string `json:"group"`
	// Version and SourceSHA identify the verified installed payload this
	// unit starts, and PayloadRoot is that payload's directory.
	Version     string `json:"version"`
	SourceSHA   string `json:"source_sha"`
	PayloadRoot string `json:"payload_root"`
	// Executable is the daemon inside PayloadRoot the unit starts.
	Executable string `json:"executable"`
	// StateDir persists the database, the role identity and the generated
	// configuration across restarts; RuntimeDir holds only the readiness
	// record and is recreated for every start.
	StateDir   string `json:"state_dir"`
	RuntimeDir string `json:"runtime_dir"`
	UnitPath   string `json:"unit_path"`
	// ExecutorID is the persistent identity of a managed executor. A
	// restart keeps it; it is empty for a dispatcher.
	ExecutorID string `json:"executor_id,omitempty"`
	// Enrolled preserves the configuration and identity created by executor join.
	Enrolled bool `json:"enrolled,omitempty"`
	// PacketCapabilities records the capability set installed for an enrolled
	// executor. Missing in older records means the original capability-free unit.
	PacketCapabilities bool `json:"packet_capabilities,omitempty"`
	// SharedIsolation retains the managed shared-worker preset across replacements.
	SharedIsolation bool `json:"shared_isolation,omitempty"`
	// HTTPPort and GRPCPort are the dispatcher's loopback listeners.
	HTTPPort int `json:"http_port,omitempty"`
	GRPCPort int `json:"grpc_port,omitempty"`
	// DispatcherGRPC and DispatcherHTTP are the control addresses a managed
	// executor connects to.
	DispatcherGRPC string `json:"dispatcher_grpc,omitempty"`
	DispatcherHTTP string `json:"dispatcher_http,omitempty"`
	// MaintenanceFile is the dispatcher's admission switch. Its presence
	// stops submission admission; it is empty for an executor.
	MaintenanceFile string `json:"maintenance_file,omitempty"`
	// ConfigPath, DatabasePath and ReadyFile are the generated
	// configuration, the served database and the readiness record.
	ConfigPath   string `json:"config_path"`
	DatabasePath string `json:"database_path"`
	ReadyFile    string `json:"ready_file"`
	// StopBudgetSeconds is the unit's stop timeout, recorded so an operator
	// reading the record knows the budget without parsing the unit.
	StopBudgetSeconds int `json:"stop_budget_seconds"`
}

Profile is the complete description of one managed role instance. Every path is absolute inside Root, so a staging root and the real root differ in exactly one value and nothing else about the contract changes.

func DerivePaths

func DerivePaths(root string, role storagecheck.Role, name string) (Profile, error)

DerivePaths computes every path of one role instance from its root, role and name alone. Nothing it returns is read from disk, so a later operation can check an installed record against it: the record says which version and account an instance was installed with, and it is never allowed to say where the instance lives.

func ReadRecord

func ReadRecord(root string, role storagecheck.Role, name string) (Profile, error)

ReadRecord reads the installed record of one instance.

func Resolve

func Resolve(request Request, assets demo.Assets) (Profile, error)

Resolve derives the complete profile of a request against a verified payload. It performs no I/O beyond the assets the caller already verified.

func (Profile) SamePaths

func (p Profile) SamePaths(other Profile) bool

SamePaths reports whether two profiles describe the same instance in the same places. Everything an operation acts on is compared: a record that disagrees about any of them is not this instance's record.

type Report

type Report struct {
	Operation string `json:"operation"`
	Role      string `json:"role"`
	Name      string `json:"name"`
	Unit      string `json:"unit"`
	Version   string `json:"version"`
	// State is the observed outcome: installed, ready, started, stopped,
	// uninstalled, not-installed, or incomplete.
	State string `json:"state"`
	// Ready reports an observed readiness record published by the running
	// process, never the manager's belief that a process exists.
	Ready bool `json:"ready"`
	// Joined reports a daemon that finished its own shutdown. It is the
	// only thing that permits deleting, upgrading or rebuilding state.
	Joined      bool   `json:"joined,omitempty"`
	Enabled     bool   `json:"enabled"`
	Active      string `json:"active,omitempty"`
	MainPID     int    `json:"main_pid,omitempty"`
	ExecutorID  string `json:"executor_id,omitempty"`
	Endpoint    string `json:"endpoint,omitempty"`
	StateDir    string `json:"state_dir,omitempty"`
	UnitPath    string `json:"unit_path,omitempty"`
	PackagePath string `json:"package_path,omitempty"`
	// Changed lists what this operation actually altered on disk or in the
	// service manager. A repeated identical install changes nothing.
	Changed []string `json:"changed,omitempty"`
	// RestartRequired reports a running unit whose unit file or generated
	// configuration changed. Installing never restarts a running daemon.
	RestartRequired bool `json:"restart_required,omitempty"`
	// Note carries the one thing an operator has to know that the fields
	// above do not already say.
	Note string `json:"note,omitempty"`
}

Report is what one managed-service operation observed. Every field is either something this command did or something it read back; nothing is inferred from a manager call having returned successfully.

type Request

type Request struct {
	Role storagecheck.Role
	Name string
	// Root prefixes every managed path. Empty means the real root.
	Root string
	// User and Group default to DefaultAccount.
	User, Group string
	// HTTPPort and GRPCPort are required for a dispatcher; a managed
	// service needs stable ports, so 0 is refused.
	HTTPPort, GRPCPort int
	// DispatcherGRPC and DispatcherHTTP are required for an executor.
	DispatcherGRPC, DispatcherHTTP string
	// EnrolledState adopts an enrolled executor at its canonical managed path.
	EnrolledState string
	// Isolation selects the shared preset on first enrolled service installation.
	Isolation string
}

Request is what an operator asks for. Everything else in a Profile is derived, so two installs of the same request produce the same unit.

type StagingManager

type StagingManager struct{}

StagingManager stands in for the service manager when files are written below an alternate root. Such a tree is not a second installation: the host has exactly one service manager, its units live at their own absolute paths and a unit's runtime directory is always the real one, so acting on a unit named by a staged tree would act on the production instance of that name. Every operation that would do so is refused here rather than performed against the wrong target.

func (StagingManager) Disable

func (StagingManager) Enable

func (StagingManager) Reload

Reload has nothing to re-read: no unit of a staged tree was ever loaded, and the manager's own units were not touched. It is the one operation that is not refused, because it changes nothing either way.

func (StagingManager) Start

func (StagingManager) State

State reports the staged instance as one the manager does not know, which is what it is: no unit of a staged tree was ever loaded.

func (StagingManager) Stop

type Systemctl

type Systemctl struct {
	// Path is the absolute systemctl binary. NewSystemctl resolves it.
	Path string
}

Systemctl drives the host service manager through its command-line client. Every call names exactly one unit, so no operation can reach a unit this package did not write.

func NewSystemctl

func NewSystemctl() (*Systemctl, error)

NewSystemctl resolves the host's service-manager client.

func (*Systemctl) Disable

func (s *Systemctl) Disable(ctx context.Context, unit string) error

func (*Systemctl) Enable

func (s *Systemctl) Enable(ctx context.Context, unit string) error

func (*Systemctl) Reload

func (s *Systemctl) Reload(ctx context.Context) error

func (*Systemctl) Start

func (s *Systemctl) Start(ctx context.Context, unit string) error

func (*Systemctl) State

func (s *Systemctl) State(ctx context.Context, unit string) (UnitState, error)

State asks for the exact properties this package uses. A unit the manager does not know reports LoadState=not-found rather than an error, so an uninstalled service is distinguishable from an unreachable manager.

func (*Systemctl) Stop

func (s *Systemctl) Stop(ctx context.Context, unit string) error

type UnitState

type UnitState struct {
	// Loaded reports whether the manager knows a unit of this name at all.
	Loaded bool
	// FragmentPath and DropInPaths identify the definition the manager loaded.
	FragmentPath string
	DropInPaths  string
	// Active is the manager's active state ("active", "inactive",
	// "failed", "activating", "deactivating").
	Active string
	// Sub is the manager's finer substate ("running", "dead", "exited").
	Sub string
	// Enabled reports whether the unit starts on its own at boot.
	Enabled bool
	// MainPID is the daemon's process ID, or 0 when none is running. A
	// readiness record is only believed when it names this process.
	MainPID int
	// Result is how the manager's last attempt to run the unit ended:
	// "success", or "timeout", "exit-code", "signal", "core-dump",
	// "watchdog" and the like for a failure.
	Result string
	// ExecMainStatus is the daemon's exit status, or the signal number
	// that killed it.
	ExecMainStatus int
}

UnitState is what the service manager reports about one unit. It is deliberately a small, literal view: it says what the manager believes, never what the daemon inside the unit has actually finished.

func (UnitState) Joined

func (s UnitState) Joined() bool

Joined reports a unit whose daemon finished on its own terms, which is the only thing that proves local ownership was released.

It is deliberately not "the unit is down" and not "the readiness record is gone". The readiness record lives in the unit's runtime directory, which the manager removes whenever the unit stops, killed or not, so its absence proves nothing at all. What does prove something is how the daemon left: an inactive unit with a successful result and a zero exit status means the daemon ran its whole shutdown path, joined its own work and closed its database before exiting. A unit killed at its stop timeout, or one that exited nonzero, is failed with a result that says so, and its retained state may still be owned by a process that never finished.

func (UnitState) Running

func (s UnitState) Running() bool

Running reports a unit the manager considers up.

func (UnitState) Stopped

func (s UnitState) Stopped() bool

Stopped reports a unit with no process left: it is down, whether it got there by exiting or by being killed.

Jump to

Keyboard shortcuts

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