Documentation
¶
Overview ¶
Package fleetagent implements the fleet-agent CLI: enrollment, the daemon itself, and registration with the platform's service manager.
Index ¶
- Constants
- Variables
- func Main(args []string, out io.Writer) int
- func MainContext(ctx context.Context, args []string, out io.Writer) int
- func NewRootCommand(out io.Writer) *cobra.Command
- func TaskXMLBytes(xml string) []byte
- type Confinement
- type Hardening
- type Mechanism
- type UnitParams
- func (p UnitParams) Arguments() []string
- func (p UnitParams) LaunchdPlist() string
- func (p UnitParams) ScheduledTaskXML() string
- func (p UnitParams) ServiceConfig() *service.Config
- func (p UnitParams) ServiceConfigWithPassword(password string) *service.Config
- func (p UnitParams) SystemdUnit() string
Constants ¶
const ServiceName = "fleet-agent"
ServiceName is the identifier the daemon is registered under with every platform's service manager: the systemd unit, the launchd label, and the Windows service name.
const TaskPath = `\` + ServiceName
TaskPath is the Task Scheduler path the Windows Scheduled Task registers under. Tasks live in a folder tree; the root folder is where an operator looking for it in taskschd.msc will look first.
Variables ¶
var ErrNotInstalled = errors.New("service is not installed")
ErrNotInstalled reports that the service is not registered with the platform's service manager.
var ErrUnusable = errors.New("the agent is running but cannot run the operator's toolchain; see the report above")
ErrUnusable reports an agent that is installed, running, and confined in a way that stops it doing the one thing it exists for.
It is what `service status` exits with, and it is separate from every other failure because the operator's experience of it is not a failure at all: the daemon is up, health checks pass, and every command a model runs through it cannot find a toolchain. Something has to say so.
Functions ¶
func MainContext ¶
MainContext is Main with a cancellable context.
serve is a long-running command, and cancelling the context is how a caller that is not a signal — a test, or an embedding process — stops it. The daemon derives its own signal handling from this context, so the two paths converge rather than competing.
func NewRootCommand ¶
NewRootCommand builds the command tree, writing all output to out.
func TaskXMLBytes ¶
TaskXMLBytes encodes a rendered task definition the way schtasks.exe insists on reading one: UTF-16, little-endian, with a byte-order mark.
Not a detail worth discovering at an operator's install. schtasks rejects a UTF-8 file with "The task XML contains a value which is incorrectly formatted or out of range", which names neither the encoding nor the file, and the declaration at the top of the document has to agree with the bytes under it.
Types ¶
type Confinement ¶
type Confinement struct {
// Summary replaces "running" on the status headline.
Summary string
// Detail says what is wrong, in the terms an operator would use.
Detail []string
// RemedyIntro introduces Remedy. It varies because the remedy does: for a
// confinement this command can name a command for, it is one line to copy;
// for one it cannot, promising a command would be a lie.
RemedyIntro string
// Remedy is what to do instead. Printed verbatim, one line each.
Remedy []string
}
Confinement is a way in which a running agent cannot do the job it exists for: the process is up, it answers health checks, and every command a model asks it to run either fails to find its toolchain or resolves the wrong one.
It exists because `service status` used to have no vocabulary for that. A daemon installed as NT AUTHORITY\NetworkService reports "running" and is useless, and the operator finds out one failed command at a time. Status is the tool they ask; it should be the thing that tells them.
type Hardening ¶
type Hardening string
Hardening selects how much the platform's service manager is asked to constrain the daemon.
The agent's job is running arbitrary commands and writing files under its roots, so this is genuinely easy to overtighten: a directive that looks obviously correct on a daemon that serves HTTP breaks a daemon whose whole purpose is `go build`.
const ( // HardeningStandard is the default: the baseline issue #18 asks for, minus // anything that would stop a build from running. HardeningStandard Hardening = "standard" // HardeningStrict adds ProtectSystem=strict with the allowed roots as // ReadWritePaths. It is opt-in because a toolchain that writes outside the // roots — ~/.cache/go-build, ~/.npm, ~/.cargo — stops working under it // unless those directories are roots too. HardeningStrict Hardening = "strict" // HardeningNone emits no confinement directives. HardeningNone Hardening = "none" )
func ParseHardening ¶
ParseHardening validates a --hardening value.
type Mechanism ¶
type Mechanism string
Mechanism is how the agent is registered to start on this host.
Everywhere but Windows there is one answer — the platform's service manager — and this type is a formality. On Windows there are two, they are not interchangeable, and the difference is the whole of #74.
A Windows service runs in session 0, which has been isolated from every interactive session since Vista. Under a built-in service identity it has no operator profile at all, so it sees none of nvm, rustup, pyenv, cargo, scoop, npm globals, or the credentials in %APPDATA% that git and the package registries read. On a developer machine that is most of PATH, and an agent whose entire purpose is running the commands the operator would type cannot run them.
A logon-triggered Scheduled Task runs in the operator's own session, with their profile and their PATH, and needs no password. It also stops at logout, which is the trade.
const ( // MechanismAuto lets install pick, which is what an operator who has never // heard of session 0 should get. MechanismAuto Mechanism = "auto" // MechanismService registers with the platform's service manager: systemd, // launchd, or the Windows SCM. MechanismService Mechanism = "service" // MechanismTask registers a logon-triggered Scheduled Task. Windows only. MechanismTask Mechanism = "task" )
func ParseMechanism ¶
ParseMechanism validates a --mechanism value.
type UnitParams ¶
type UnitParams struct {
// Executable is the absolute path to the fleet-agent binary.
Executable string
// ConfigPath is the agent config the unit passes to `serve`.
ConfigPath string
// User is the account the daemon runs as. Never defaulted to a superuser:
// every command the sandbox runs inherits this identity.
User string
// Group is the account's primary group. Empty means the platform default.
Group string
// AllowedRoots are the config's allowed roots, needed as ReadWritePaths
// under strict hardening.
AllowedRoots []string
// StateDir and LogDir are created at install time and must remain
// writable under any hardening level.
StateDir string
LogDir string
// RestartDelay is how long the service manager waits before restarting a
// failed daemon.
RestartDelay time.Duration
// StopTimeout is how long the service manager waits for a graceful stop.
// It must exceed the daemon's own drain deadline, or the drain is cut
// short by a SIGKILL from outside.
StopTimeout time.Duration
// Hardening selects the confinement directive set.
Hardening Hardening
}
UnitParams is everything that varies between installs. It is the input to every unit-file renderer, and is deliberately a plain value: rendering is a pure function of it, so the parts of `service install` that decide what the unit says are testable without a service manager or root.
func (UnitParams) Arguments ¶
func (p UnitParams) Arguments() []string
Arguments is the argv the service manager starts the daemon with.
func (UnitParams) LaunchdPlist ¶
func (p UnitParams) LaunchdPlist() string
LaunchdPlist renders the complete launchd job.
func (UnitParams) ScheduledTaskXML ¶
func (p UnitParams) ScheduledTaskXML() string
ScheduledTaskXML renders the complete Task Scheduler definition for a logon-triggered task running in the operator's own session.
This is the Windows answer to the systemd unit and the launchd job, and it is rendered rather than assembled for the same reason they are: what gets registered is then a pure function of UnitParams, so the decisions in it are assertable from every runner instead of only from a Windows one with an elevated token.
The settings that are not boilerplate:
- LogonType InteractiveToken is the whole point. It runs the agent in the session the operator is logged into, with their profile and their PATH, and it needs no password. A service cannot do this: every Windows service runs in session 0.
- RunLevel LeastPrivilege. The task inherits the operator's ordinary token, not an elevated one. Every command the agent runs runs as them, and this project's position is that handing the model an administrator is the same mistake as handing it root.
- ExecutionTimeLimit PT0S disables the three-day default kill. A daemon is not a batch job.
- The battery settings are inverted from the Task Scheduler defaults, which refuse to start on battery and stop when a laptop unplugs. A workstation is the case this mechanism exists for.
- Priority 5 is NORMAL_PRIORITY_CLASS. The default for a scheduled task is 7, which is below normal, and the agent's children are builds.
- MultipleInstancesPolicy IgnoreNew, so a second logon does not start a second daemon against the same state directory.
func (UnitParams) ServiceConfig ¶
func (p UnitParams) ServiceConfig() *service.Config
ServiceConfig builds the kardianos/service configuration for these parameters, including the per-platform options that the rendered unit files do not cover.
func (UnitParams) ServiceConfigWithPassword ¶
func (p UnitParams) ServiceConfigWithPassword(password string) *service.Config
ServiceConfigWithPassword is ServiceConfig for a Windows service registered under a named account, which the SCM will not create without credentials.
The password is a parameter rather than a field on UnitParams because UnitParams is the value that gets rendered, compared and printed — including into a test failure — and the one thing this password must never do is end up anywhere but the LSA secret the SCM stores it in. It is handed to CreateService and then goes out of scope.
func (UnitParams) SystemdUnit ¶
func (p UnitParams) SystemdUnit() string
SystemdUnit renders the complete systemd unit file.
It is passed to kardianos/service as a fully expanded SystemdScript rather than assembled from its options, because the two directives that matter most here — KillMode and the hardening set — are not expressible through them.