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
- Variables
- func AdministrationDirectory(root string) string
- func ClearMaintenance(path string) (bool, error)
- func RecordPath(root string, role demo.SchemaRole, name string) string
- func StateDirectory(root string, role demo.SchemaRole, name string) string
- func Unit(p Profile) (string, error)
- func UnitDirectory(root string) string
- func UnitName(role demo.SchemaRole, name string) string
- func WriteMaintenance(path string, reason string, at time.Time) error
- type Account
- type DrainOptions
- type DrainReport
- type Installer
- func (i *Installer) Drain(ctx context.Context, role demo.SchemaRole, name string, options DrainOptions) (DrainReport, error)
- func (i *Installer) Install(ctx context.Context, request Request, assets demo.Assets, start, enable bool) (Report, error)
- func (i *Installer) Resume(ctx context.Context, role demo.SchemaRole, name string) (DrainReport, error)
- func (i *Installer) Start(ctx context.Context, role demo.SchemaRole, name string) (Report, error)
- func (i *Installer) Status(ctx context.Context, role demo.SchemaRole, name string) (Report, error)
- func (i *Installer) Stop(ctx context.Context, role demo.SchemaRole, name string) (Report, error)
- func (i *Installer) Uninstall(ctx context.Context, role demo.SchemaRole, name string, purge bool) (Report, error)
- type Maintenance
- type Manager
- type Options
- type Profile
- type Report
- type Request
- type StagingManager
- func (StagingManager) Disable(context.Context, string) error
- func (StagingManager) Enable(context.Context, string) error
- func (StagingManager) Reload(context.Context) error
- func (StagingManager) Start(context.Context, string) error
- func (StagingManager) State(context.Context, string) (UnitState, error)
- func (StagingManager) Stop(context.Context, string) error
- type Systemctl
- func (s *Systemctl) Disable(ctx context.Context, unit string) error
- func (s *Systemctl) Enable(ctx context.Context, unit string) error
- func (s *Systemctl) Reload(ctx context.Context) error
- func (s *Systemctl) Start(ctx context.Context, unit string) error
- func (s *Systemctl) State(ctx context.Context, unit string) (UnitState, error)
- func (s *Systemctl) Stop(ctx context.Context, unit string) error
- type UnitState
Constants ¶
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 )
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.
const DefaultAccount = "debuglet"
DefaultAccount is the unprivileged service account the generated units use. The Ansible deployment provisions the same name.
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 ¶
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.
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 ¶
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 ¶
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 demo.SchemaRole, name string) string
RecordPath is the installed record of one instance.
func StateDirectory ¶
func StateDirectory(root string, role demo.SchemaRole, 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 ¶
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 capabilities and no new privileges, and the only writable location is the role's own persistent state directory plus its runtime directory.
- 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 ¶
UnitDirectory is where generated units are written below a root.
func UnitName ¶
func UnitName(role demo.SchemaRole, 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 ¶
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 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 (*Installer) Drain ¶
func (i *Installer) Drain(ctx context.Context, role demo.SchemaRole, 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) Resume ¶
func (i *Installer) Resume(ctx context.Context, role demo.SchemaRole, 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) Status ¶
Status reports the installed contract and what the manager and the readiness record currently say about it, without changing anything.
func (*Installer) Stop ¶
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 demo.SchemaRole, 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 demo.SchemaRole `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"`
// 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 ¶
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 ¶
ReadRecord reads the installed record of one instance.
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"`
// 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 demo.SchemaRole
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
}
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) Reload ¶
func (StagingManager) Reload(context.Context) error
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.
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 ¶
NewSystemctl resolves the host's service-manager client.
type UnitState ¶
type UnitState struct {
// Loaded reports whether the manager knows a unit of this name at all.
Loaded bool
// 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 ¶
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.