lifeops

package
v0.10.0 Latest Latest
Warning

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

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

Documentation

Overview

Package lifeops answers what a local billet deployment is actually doing.

IT PRINTS NOTHING. Operator output belongs to cmd/billet, the one package allowed to write to stdout; lifeops returns facts and lets the command render them. That split is what lets every judgment here be asserted by value rather than by scraping text out of a terminal.

The facts it gathers are the ones that differ silently. A host can have the right config, a valid App key and a running service, and still be executing a binary that was replaced hours ago, or a unit an override has quietly rewritten — and nothing on the happy path says so.

Index

Constants

View Source
const DefaultStabilityWait = 6 * time.Second

DefaultStabilityWait is how long a service must keep the process it started before `up` calls it up.

SIX SECONDS, matching the host role's own probe. It is not a health check and does not pretend to be: no finite window proves future life. What it catches is the failure this command would otherwise report as success — a service that reaches READY=1 and then dies, which Restart=on-failure turns into a crash loop that looks "active" at any single instant.

View Source
const DefaultTimeout = 30 * time.Second

DefaultTimeout bounds one systemctl call. Generous: these are local queries, and the cost of it being too small is a diagnostic that fails on a busy host rather than reporting what it found.

Variables

This section is empty.

Functions

func Contained

func Contained(dir, p string) bool

Contained reports whether path is inside dir — the same directory or something under it, by path COMPONENT rather than by string prefix.

"/var/lib/billet-evil" has the prefix "/var/lib/billet" and is a different directory that no unit makes writable.

Types

type ConvergeOption

type ConvergeOption func(*Converger)

ConvergeOption configures a Converger.

func WithStabilityWait

func WithStabilityWait(d time.Duration) ConvergeOption

WithStabilityWait sets how long a started service must hold its process.

type Converger

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

Converger changes a host. It is a separate type from Inspector because the two answer to different rules — an inspection may run anywhere and reports what it finds, while this refuses to act on anything it cannot establish.

func NewConverger

func NewConverger(i *Inspector, opts ...ConvergeOption) *Converger

NewConverger builds one around an Inspector.

func (*Converger) ApplyOwnership

func (c *Converger) ApplyOwnership(changes []OwnershipChange, uid, gid int) error

ApplyOwnership makes the changes a plan named.

THROUGH A DESCRIPTOR, NOT A PATHNAME. This runs as root, and os.Chown follows symlinks: a path that named a regular file when the plan was made and a symlink by the time it is applied would hand an arbitrary file's ownership to the service account. The file is opened without following, checked to be the same file the planner saw, and changed through the descriptor — so whatever happens to the NAME afterwards, the operation lands on the inode that was verified.

THE MODE IS TIGHTENED BEFORE THE OWNER IS TRANSFERRED. Reversed, a chown that succeeds and a chmod that fails leaves a private key owned by the service account with whatever permissions it had — which is how a 0644 App key becomes readable by every process running as that account's group.

func (*Converger) CollateralNote

func (c *Converger) CollateralNote() string

CollateralNote explains how enabling ONE service can commit another, in this manager's own terms.

IT IS THE BACKEND'S SENTENCE because the mechanism is: systemd has `[Install] Also=`, which writes links for a unit nobody named and is invisible in every property of the one that was. launchd has no such thing and would have a different sentence, or none. The shared command detects the collateral change the same way on both — by comparing before and after — and only the explanation differs.

func (*Converger) Disable

func (c *Converger) Disable(ctx context.Context, unit string) error

Disable undoes an Enable this run performed.

ONLY WHAT THIS RUN DID. A failed `up` must not leave a unit committed to boot that nothing established can run — and must equally not disable one an operator had enabled before it arrived.

func (*Converger) DisableCmd

func (c *Converger) DisableCmd(unit string) string

DisableCmd renders how an operator undoes an enablement themselves.

func (*Converger) Enable

func (c *Converger) Enable(ctx context.Context, unit string) error

Enable commits a unit to future boots.

func (*Converger) EnabledNow

func (c *Converger) EnabledNow(ctx context.Context, unit string) (Enablement, error)

EnabledNow reports a unit's enablement state at this moment.

The plan's answer is from before `billet check` talked to GitHub, and another operator may have enabled the unit in between. Enabling is idempotent, so success proves nothing about who did it — and rolling back an enablement this run did not perform would disable a service somebody else just committed.

IT ANSWERS WITH THE STATE ITSELF, not a yes or no. Folding it to a boolean made `masked`, `static`, `linked`, an empty answer and every state systemd adds later mean what `disabled` means — permission to enable. Only the exact string is that, and the caller says so.

func (*Converger) EnablementCmd

func (c *Converger) EnablementCmd(units ...string) string

EnablementCmd renders the command an operator runs to see for themselves what billet just told them about enablement.

A REFUSAL AN OPERATOR CANNOT ACT ON IS A DEAD END, so every one of them carries the command that resolves it — and that command belongs to the service manager rather than to billet, which makes it the backend's to name.

func (*Converger) Identity

func (c *Converger) Identity(req UpRequest) (int, int, error)

Identity resolves the service account for a caller that already planned.

func (*Converger) ManagerName

func (c *Converger) ManagerName() string

ManagerName is what billet calls this service manager in a sentence.

func (*Converger) Plan

func (c *Converger) Plan(ctx context.Context, req UpRequest) (UpPlan, error)

Plan decides what `up` would do, and every reason it would not.

IT COLLECTS REFUSALS RATHER THAN RETURNING THE FIRST. An operator who has to re-run a command to discover the next thing wrong with their host is being made to pay for a diagnostic that already knew.

func (*Converger) ProveStable

func (c *Converger) ProveStable(ctx context.Context, unit string) error

ProveStable establishes that a unit is holding the process it has.

It exists separately from StartAndProve because a service that was ALREADY running when `up` arrived gets enabled too, and the sample the planner took is by then minutes old — `billet check` runs in between and talks to GitHub. Enabling on that sample would commit a crash loop to every future boot on the evidence of one instant that has since passed.

func (*Converger) RepairPaths

func (c *Converger) RepairPaths(
	dir string, targets []RepairTarget, uid, gid int,
) ([]string, error)

RepairPaths gives the service account back a NAMED SET of entries a privileged command created in a state directory.

THE CALLER SUPPLIES THE SET, and for `billet local restore` that set is derived from the plan it just executed rather than from a list kept here. A hand-maintained second list is the failure this whole area keeps producing: preflightState covers what `billet check` creates and covers none of what a restore publishes — no `ca/`, no `authority-created` — so a restore run as root (which is what an operator restoring onto a packaged host IS, because the App key lands in root-owned /etc/billet) left every one of those files owned by root inside a service-owned directory, where systemd's StateDirectory= will never look at them again. Measured in scripts/restore-rehearsal.sh: the service account could not open the deployment root had just restored.

There is no walk, which is what makes it safe: nothing is discovered, so nothing planted can be reached, and every entry named is one this run put there.

func (*Converger) RepairServerState

func (c *Converger) RepairServerState(dir string, uid, gid int) ([]string, error)

RepairServerState gives the server back the state a root-run preflight created underneath it.

MEASURED, NOT ASSUMED, and it is the reason this function exists at all: `billet check` opens the ledger as the invoking process, so running it as root creates billet.db and billet.lock owned by root. systemd's StateDirectory= repairs ownership RECURSIVELY when the top directory's owner is wrong — verified on systemd 255 — but does nothing at all when the directory is already correct, which is every run after the first. The result is a billet-owned directory holding a root-owned ledger, and a server that starts and cannot write.

Only entries owned by root are touched, only regular files, and only those with a single link: a hard link is the same inode under two names, and chowning one here would change the file it aliases wherever else that file lives.

func (*Converger) Revalidate

func (c *Converger) Revalidate(ctx context.Context, req UpRequest, want UnitPlan) error

Revalidate re-establishes that one unit is still what the plan decided about, immediately before acting on it.

THE PLAN IS OLD BY THE TIME ANYTHING HAPPENS. `billet check` talks to GitHub in between, which takes as long as the network takes, and in that window a unit can be edited and daemon-reloaded into something else — a different ExecStart, a different account, a privilege prefix — or a drain can begin, turning the inactive unit the plan saw into one that is deactivating. Acting on the older answer would start a service billet never validated, or start one through a drain that is waiting for the jobs on this host.

It cannot close the last instant; nothing short of holding systemd still can. It closes the seconds that `billet check` spends on the network, which is where the window actually is.

func (*Converger) Running

func (c *Converger) Running(ctx context.Context, req UpRequest) ([]RunningFacts, error)

Running reports what each wanted service is executing right now.

IT BELONGS TO THE BACKEND, not to a shared inspection. `down` refuses to act on an installation running a different build of billet, and the facts that refusal rests on — is it active, is the running process this build — are answered in a completely different way by each service manager. Getting them from a systemd inspector meant the refusal silently reported on systemd units on a host that has none.

func (*Converger) Services

func (c *Converger) Services() (string, string)

Services names this backend's two services, server first.

SHARED CODE MUST NOT KNOW THE UNIT NAMES. Every place the lifecycle commands talk about "the other service" — the bystander snapshot, the enablement comparison, the order `down` stops things in — needs a pair of identifiers, and reaching for deploy's systemd constants there quietly makes those commands systemd-only: a converger for another service manager would be handed `billet-server.service`, inspect a service that does not exist, find nothing, and report that nothing had changed. Asking the backend is what keeps the ORDER shared and the vocabulary local. The server is returned first, matching the order `up` starts them in.

func (*Converger) Snapshot

func (c *Converger) Snapshot(ctx context.Context, unit string) (string, error)

Snapshot is what a unit looks like right now, as one comparable string.

It exists so a caller can prove that starting ONE service did not disturb another. billet refuses a unit that names the other one in its dependencies, but a transaction can reach further than that — a unit billet's own unit pulls in may itself conflict with the other service — and no practical amount of property-reading models what systemd will actually do. Comparing before and after does not need to: whatever the mechanism, a service that changed while billet was starting something else is a service billet disturbed.

func (*Converger) StartAndProve

func (c *Converger) StartAndProve(ctx context.Context, unit string) (string, error)

StartAndProve starts a unit and establishes that it stayed up.

START, THEN PROVE, THEN THE CALLER ENABLES — never `enable --now`. Enabling first commits a unit to every future boot before anything has established it can run at all, and unwinding that is a second mutation on a host that has just failed one.

The packaged units are Type=notify, so `systemctl start` does not return until billet has sent READY=1 or TimeoutStartSec has elapsed: its exit status IS the readiness signal. The window that follows exists for what readiness cannot cover — a service that reaches ready and then dies, which Restart=on-failure turns into a crash loop reading "active" at any instant.

func (*Converger) StartTimer added in v0.6.0

func (c *Converger) StartTimer(ctx context.Context, unit string) error

StartTimer starts a timer unit and proves it is armed.

NOT StartAndProve, which proves a PROCESS held its pid across a settle window: a timer has no process, and `active` is systemd saying the next elapse is scheduled, which is the whole fact a caller needs. Enable and start are two calls because `enable --now` commits a unit to every future boot before anything proved it can run, the rule every other unit here follows.

func (*Converger) StopAndProve

func (c *Converger) StopAndProve(ctx context.Context, unit string) (StopResult, error)

StopAndProve stops a unit and establishes that it is actually gone.

THE EXIT STATUS IS NOT THE ANSWER, which is why this is not a one-line wrapper. `systemctl stop` returns when the job it queued completes, and a unit whose main process ignored SIGTERM is killed at TimeoutStopSec — after which the unit is inactive and `failed`. A caller about to report "this host is down" needs the state the manager holds now, not the return code of a command.

type Enablement

type Enablement struct {
	Enabled Tristate
	// How is the manager's own word, for a diagnostic an operator can act on.
	How string
}

Enablement is whether a service will start itself at the next boot.

A VERDICT AND A WORD, rather than the manager's raw string. systemd answers with one of a dozen words (`enabled`, `disabled`, `static`, `masked`, `linked`, `enabled-runtime`, `indirect`, …); launchd has no such string at all — a label is enabled when its plist is installed and its entry in the durable override database does not say `disabled`. Shared code comparing against the literal "disabled" is therefore code only systemd can satisfy, and a second backend would have to invent systemd's vocabulary to be understood.

Unknown is the common case rather than the exotic one: every systemd state except exactly `enabled` and exactly `disabled` lands there, because those are the only two billet has a rule for. That is deliberate — `up` acts only on a definite No, so anything it cannot classify refuses instead of being guessed at, including a state a future systemd adds.

type FileFacts

type FileFacts struct {
	Path   string
	Exists Tristate
	Mode   fs.FileMode
	Owner  string
	Group  string
	// Info is what the stat returned, kept so a later privileged operation can
	// prove it is acting on the same file rather than on the same NAME.
	Info fs.FileInfo
	// Err records why the facts are incomplete. A stat that failed for a reason
	// other than absence leaves Exists Unknown and this set, so a caller reports
	// the reason instead of inventing an answer.
	Err error
}

FileFacts is what one path on disk is, as far as this process can see.

type Inspector

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

Inspector reads a host. Every external dependency is a seam so the whole thing is testable without root, without systemd and without a real account database — none of which a unit test can supply.

func NewInspector

func NewInspector(opts ...Option) *Inspector

NewInspector builds an Inspector.

func (*Inspector) Inspect

func (i *Inspector) Inspect(ctx context.Context, configPath string, keyPaths []string) (Report, error)

Inspect gathers what this machine is doing with the given config.

It reports rather than refuses: a status command must answer on a host that is half-configured, which is exactly when somebody runs it. Callers that need a decision make it from these facts.

type Option

type Option func(*Inspector)

Option configures an Inspector.

func WithSystemctl

func WithSystemctl(path string) Option

WithSystemctl names the systemctl binary, skipping the PATH lookup.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout bounds each systemctl call.

type OwnershipChange

type OwnershipChange struct {
	Path  string
	Owner string
	Group string
	Mode  fs.FileMode
	// Why names the failure this prevents, for the operator reading --dry-run.
	Why string
	// contains filtered or unexported fields
}

OwnershipChange is one file whose owner or mode `up` will correct.

type Refusal

type Refusal struct {
	What   string
	Remedy string
}

Refusal is a reason `up` will not proceed, and what to do about it. Both halves are required: a refusal an operator cannot act on is a dead end.

func (Refusal) Error

func (r Refusal) Error() string

type RepairTarget

type RepairTarget struct {
	Name string
	Dir  bool
}

RepairTarget is one entry a privileged billet command created inside a state directory, named relative to it.

Dir says the entry is a DIRECTORY rather than a file, and it is not cosmetic: a root-owned 0700 directory cannot even be TRAVERSED by the service account, so an authority restored under one is unreachable however its files are owned — and a directory's link count is always above one, which is the check that refuses a hard-linked file.

type Report

type Report struct {
	ConfigPath string
	Config     FileFacts
	// AppKeys is one entry per target's key path, in the order given.
	AppKeys []FileFacts

	// Binary is the running billet's path, for display. The identity judgments
	// deliberately do not use it: a pathname cannot survive being replaced.
	Binary    string
	BinaryErr error

	Server ServiceFacts
	Node   ServiceFacts
}

Report is what one inspection found.

type RunningFacts

type RunningFacts struct {
	Name string
	// Active is whether a PROCESS IS RUNNING for this service — not whether the
	// manager considers it healthy, and not whether it is settled. A service
	// that is starting up or shutting down has a live process, and for every
	// question this type is used to answer that is the same situation as one
	// that is running.
	//
	// Three-valued because "billet could not tell" is an ordinary answer — a
	// manager that did not reply, a state this build has no rule for — and it is
	// NOT the same fact as "nothing is running". Collapsing it to a bool made
	// the identity refusal skip a service it could not see, which is the
	// direction that stops somebody else's job.
	Active Tristate
	// IsThisBuild is whether the process currently executing is the same build
	// as the billet asking. Three-valued because "could not tell" is an ordinary
	// answer — a process that exited between the two reads, a manager that
	// reported no pid — and it is not the same fact as "no, it is different".
	IsThisBuild Tristate
	// Why explains the judgment, for a diagnostic that has to be actionable.
	Why string
}

RunningFacts is what ANY service manager can say about a service that is running right now: which service, whether it is running at all, and whether the process it is running is this build of billet.

A DELIBERATELY NARROW SET, and the narrowness is the point. ServiceFacts carries a dozen properties that only systemd has — FragmentPath, DropInPaths, ReloadPending, ExecStartFlags, Namespace, Elevation — and every one of them exists because a systemd refusal is computed from it. Promoting those to a shared type gives a struct half of whose fields are permanently empty under the other manager, and a reader who has to know which half. These four are facts both managers genuinely answer, which is what makes them shareable.

type ServiceFacts

type ServiceFacts struct {
	Name string

	LoadState     string
	ActiveState   string
	SubState      string
	UnitFileState string
	Result        string
	Type          string
	User          string
	Group         string
	FragmentPath  string
	DropInPaths   []string
	MainPID       int
	NRestarts     int

	// ExecStart is the executable systemd WILL run, taken from systemd's own
	// parse rather than from the unit file's text.
	ExecStart string
	// ExecStartCount is how many ExecStart directives the unit carries. More
	// than one makes "which binary does this unit run" ambiguous, which a
	// caller must treat as a refusal rather than reading the first.
	ExecStartCount int
	// ExecStartArgv is the whole command line systemd would run. The path alone
	// does not say which ROLE a unit starts, and a billet that runs `server`
	// under the node unit's name is a control plane nothing authorised.
	ExecStartArgv string

	// ExecExtra is every OTHER command the unit would run — ExecStartPre and
	// its relatives. They are not covered by the ExecStart checks and several
	// of them run before the main process, which for the server unit is the
	// difference between an unprivileged control plane and a root one.
	ExecExtra map[string]string
	// Namespace is the set of properties that can replace the filesystem the
	// unit sees. A RootDirectory or a bind mount makes the path in ExecStart
	// name a different file than the one billet compared against.
	Namespace map[string]string
	// Elevation is the set of properties that change WHO the process is, past
	// the User= and Group= the unit names. Measured on the packaged units: each
	// of these is empty on both, and SupplementaryGroups is the sharpest —
	// adding `docker` to the unprivileged server is root by another route.
	//
	// Environment is deliberately NOT here: an operator behind a proxy has a
	// real reason to set it, and it cannot by itself change what runs or as
	// whom.
	Elevation map[string]string

	// ExecStartFlags is what systemd's EXTENDED answer says about the command's
	// prefixes, and it is the only trustworthy source for them.
	//
	// Measured on systemd 255: `ExecStart=+/usr/bin/billet server …` runs with
	// full privileges whatever User= says, and `systemctl show -p ExecStart`
	// renders it byte-identically to the unprefixed form — the prefix is simply
	// not in that answer. `-p ExecStartEx` carries `flags=privileged` for the
	// same unit and `flags=` for an ordinary one.
	ExecStartFlags string
	// ExecStartExCount is how many extended entries came back. Zero means
	// systemd did not answer at all — the property landed in v246 — which is
	// uncertainty about privilege rather than an absence of it.
	ExecStartExCount int
	// ExecStartFlagsKnown says whether the flags field was found at all.
	ExecStartFlagsKnown bool

	// Names is every name this unit answers to. An Alias= makes a second name
	// resolve to the same unit, which is a way for `enable` to write links
	// billet did not ask for.
	Names []string
	// Actions is the set of job settings that can act on the HOST or on other
	// units when this one fails or finishes. Measured on the packaged units:
	// OnFailureJobMode is "replace" and every other one is "none", so each has an
	// ordinary value rather than being empty — `isolate` stops everything else,
	// and `reboot` is exactly what it says.
	Actions map[string]string

	// Transaction is what starting or stopping this unit would do to OTHER
	// units. `Conflicts=` on one billet unit stops the other as part of its own
	// start, and `Requires=` starts it — neither is visible from anything else
	// billet checks.
	Transaction map[string]string

	// StateDirectory and RuntimeDirectory are the directories systemd itself
	// creates and makes writable for this unit, relative to /var/lib and /run.
	// They are the authority on where a role may keep state: under
	// ProtectSystem=strict everything else is read-only, so a config naming a
	// different path is a service that will fail on its first write.
	StateDirectory   string
	RuntimeDirectory string

	// ReloadPending is systemd's own NeedDaemonReload: the unit file on disk has
	// changed since the manager read it, so the bytes on disk and the unit the
	// manager would run are different things.
	ReloadPending Tristate

	// ExecStartIsThisBuild answers whether the file the unit WOULD run is the
	// same FILE as the running billet — by inode, so a hardlink or bind mount is
	// recognised and a replacement at the same path is not mistaken for it.
	ExecStartIsThisBuild Tristate
	ExecStartWhy         string

	// RunningIsThisBuild answers the DIFFERENT question that matters once a
	// service is up: whether the process systemd is running right now is this
	// build. Replacing a binary without restarting leaves these two disagreeing,
	// which is exactly the state nothing else on the host reports.
	RunningIsThisBuild Tristate
	RunningWhy         string

	// MatchesPackagedUnit answers whether the effective unit file is
	// byte-identical to the one the PACKAGE ships. A difference is not an
	// accusation: the Ansible role deliberately renders its own units, so a
	// role-managed host differs here for good reasons.
	MatchesPackagedUnit Tristate
}

ServiceFacts is what systemd says about one unit, plus the judgments a caller cannot make from the raw properties alone.

func (ServiceFacts) Active

func (s ServiceFacts) Active() bool

Active reports whether systemd is running this unit right now.

func (ServiceFacts) Installed

func (s ServiceFacts) Installed() bool

Installed reports whether systemd knows this unit at all.

func (ServiceFacts) PulledUnits

func (s ServiceFacts) PulledUnits() []string

PulledUnits lists what starting this unit would pull in with it, so a caller can ask each of those what IT would do.

ONE LEVEL, DELIBERATELY. The full closure is systemd's to compute through units billet has never heard of, and trying to reproduce it was wrong in four successive attempts. One level covers the shape anyone has actually constructed — billet's own unit naming an ordinary-looking helper that conflicts with the other service — and what it cannot cover is caught after the fact by comparing the other service before and after.

func (ServiceFacts) Running

func (s ServiceFacts) Running() RunningFacts

Running renders the subset of these facts that describes what the service is executing at this moment.

ONLY TWO STATES PROVE NOTHING IS RUNNING, and this is an allowlist for the same reason `StopAndProve` uses one. A denylist here was a defect: it read "not exactly active" as No, which made `deactivating` — a unit still stopping, with its process ALIVE — a definite "nothing is running". `down` waves a definite No through its identity refusal, so a foreign build caught mid-reload or mid-stop would have been stopped without anybody being asked, and stopping the server destroys the leases its listener holds.

An empty answer is systemd declining to say, and a state a future systemd adds is one billet has no rule for. Both are Unknown, which refuses.

type StopResult

type StopResult struct {
	// Gone is whether the service's process is PROVED gone.
	//
	// THREE-VALUED BECAUSE BOTH MANAGERS REALLY HAVE THE THIRD. An empty
	// systemd ActiveState and a launchctl reply billet cannot parse are both
	// "the manager did not tell us", which is uncertainty rather than absence.
	// A two-valued type loses exactly that arm, and it is the one a caller must
	// refuse on.
	Gone Tristate
	// How is the manager's own account of the ending, printed verbatim by the
	// caller. systemd fills it from ActiveState and Result; launchd from whether
	// the service is still in its domain and whether the pid it named is alive.
	How string
}

StopResult is what a service manager can say about a service it was asked to stop, in terms EVERY service manager can answer honestly.

IT IS NOT systemd's VOCABULARY, deliberately. `ActiveState` and `Result` are systemd's words for systemd's states, and a second manager has no honest value for them: after a launchd `bootout` the service is simply GONE FROM ITS DOMAIN, which is a different fact from "inactive (success)". Rendering one as the other is not a translation, it is a fabricated proof — and it would be read as one by the code whose whole job is to refuse anything that is not proof. A NON-NIL ERROR NEVER ACCOMPANIES Gone: Yes. The two halves are one answer: a backend that proved the process gone reports no error, and a backend that returns an error has not proved anything. Callers act on the error first, so a `Yes` alongside one would be discarded — and a backend that did that would have its successful stops silently reported as failures.

type Tristate

type Tristate int

Tristate is an answer that may be UNKNOWN, because "could not tell" is not "no". Collapsing the third state is how a refusal becomes a silent pass on the one host where the check mattered — a stat that fails on permissions is not evidence a file is absent, and an unreadable unit is not evidence it matches.

const (
	// Unknown is the zero value deliberately: a field nobody filled in reads as
	// uncertain rather than as a pass.
	Unknown Tristate = iota
	No
	Yes
)

func (Tristate) String

func (t Tristate) String() string

String names the state, so a diagnostic reads as a fact rather than an int.

type UnitPlan

type UnitPlan struct {
	Name string
	// Start is false when the unit is already active: `up` never restarts a
	// running service, because a restart is a drain and a drain destroys jobs.
	Start bool
	// Enable is false when the unit is already enabled.
	Enable bool
	// EnableBeforeStart inverts the order of the two steps above, because some
	// service managers cannot start a service that is not enabled.
	//
	// systemd can: `systemctl start` works on a disabled unit, which is exactly
	// what lets `up` prove a service runs BEFORE committing it to every future
	// boot. launchd cannot — a label carrying a disabled override refuses to
	// bootstrap at all — so on that manager there is no order in which the proof
	// comes first.
	//
	// WHERE THIS IS SET, THE PROOF NO LONGER PRECEDES THE COMMITMENT, and what
	// protects the host instead is the unwinding: a run that fails afterwards
	// undoes exactly the enablement it performed. That is a weaker guarantee
	// than the Linux one, so `up` says which one it is giving rather than
	// letting the two read alike.
	EnableBeforeStart bool
	// Detail is this manager's own phrase for what enabling a service does,
	// printed verbatim.
	Detail string
}

UnitPlan is what `up` will do with one service.

type UpPlan

type UpPlan struct {
	Refusals  []Refusal
	Ownership []OwnershipChange
	Units     []UnitPlan
	// ServerState is the directory the server unit keeps its ledger in, taken
	// from the unit's own StateDirectory=. Empty when no server is wanted.
	ServerState string
}

UpPlan is everything `up` would do, computed before anything is touched so that --dry-run and the real run are the same decision.

type UpRequest

type UpRequest struct {
	ConfigPath string
	// KeyPaths are every target's App key files. A deployment serving several
	// GitHub targets holds one key per target, and each is inspected, contained
	// and owned the same way.
	KeyPaths []string
	// ServiceUser and ServiceGroup are the identity the packaged units run as.
	ServiceUser  string
	ServiceGroup string
	// ServerStateDir and NodeStateDir are what the CONFIG says, so they can be
	// checked against what the units actually make writable.
	ServerStateDir string
	NodeStateDir   string
	NodeLockDir    string
	// WantServer and WantNode follow the config's own sections: a host converges
	// the roles its configuration defines, not a fixed pair.
	WantServer bool
	WantNode   bool
	// BinaryDir is the directory the services' executable lives in, which the
	// updater the services start has to be able to rename into. Only a manager
	// whose services run as an unprivileged account checks it; empty means no
	// such check.
	BinaryDir string
}

UpRequest is what the caller wants converged.

Directories

Path Synopsis
Package launchd drives macOS launch agents for billet's local lifecycle commands, as internal/lifeops drives systemd for Linux.
Package launchd drives macOS launch agents for billet's local lifecycle commands, as internal/lifeops drives systemd for Linux.

Jump to

Keyboard shortcuts

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