agent

package
v0.3.8 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2026 License: Apache-2.0 Imports: 64 Imported by: 0

Documentation

Overview

Package agent is myownpki-agent on a host (docs/PHASE3.md): it enrolls with proof of control (ADR-18), keeps its 7-day agent certificate, and – from block C on – gets certificates by ACME and deploys them.

The agent's only source for what it deploys, where it writes and what it runs is its local agent.yaml: the server never sends a path, a command or a hook name.

Index

Constants

View Source
const (
	DefaultConfigPath = "/etc/myownpki-agent/agent.yaml"
	DefaultStateDir   = "/var/lib/myownpki-agent"
)

DefaultConfigPath and DefaultStateDir are the Linux defaults.

View Source
const (
	ChallengeStandalone = "standalone"
	ChallengeWebroot    = "webroot"
	ChallengeDNS01      = "dns-01"
)

Challenge modes: http-01 standalone or through a webroot (owner 2026-10-24), and dns-01 through a DNS provider (docs/PHASE3.md §13).

View Source
const (
	ProfileNginx = "nginx"
	ProfileFile  = "file"
)

Profiles (docs/PHASE3.md step 5).

View Source
const (
	DNSTypeRFC2136    = "rfc2136"
	DNSTypeCloudflare = "cloudflare"

	PropagationAuthoritative = "authoritative"
	PropagationResolver      = "resolver"
	PropagationWait          = "wait"
)

DNS provider types and propagation modes of docs/PHASE3.md §13.2.

View Source
const (
	DefaultSetupExampleDir = "/usr/share/myownpki-agent/examples"

	// DefaultDropinDir is the drop-in directory of the unit setup installs.
	DefaultDropinDir = defaultUnitPath + ".d"
)

Variables

View Source
var ErrACMEAccountRegistration = errors.New("ACME account registration requires operator action")

ErrACMEAccountRegistration requires operator action, not a retry with the same key. In particular, a new enrollment has a new agent identity.

View Source
var ErrEnrolled = errors.New("this agent is enrolled and its certificate is valid")

ErrEnrolled: enroll on an agent whose certificate is still valid.

View Source
var ErrInvalid = errors.New("invalid agent configuration")

ErrInvalid wraps every problem of agent.yaml.

Functions

func AddDeployment added in v0.3.5

func AddDeployment(ctx context.Context, opts AddOptions) error

AddDeployment adds one deployment to agent.yaml and makes the systemd sandbox match, in this order: validate everything, write the drop-ins, keep agent.yaml.bak, write agent.yaml. It never restarts the agent unless asked.

func ApplyRecipe added in v0.3.5

func ApplyRecipe(spec *DeploymentSpec, name string) error

ApplyRecipe fills the empty fields of spec from the recipe called name (docs/PHASE3.md §11.2): what the operator set explicitly wins. Lists are added to (sandbox directories), a hook of the recipe is defined only when the deployment ends up referencing it, and --low-ports is on when either the recipe or the operator wants it.

func DescribeDropins added in v0.3.5

func DescribeDropins(cfg *Config, dir string) (string, error)

DescribeDropins is the sandbox paragraph of "config check": what the configuration implies and whether the drop-ins in dir already match it. A host without the unit (a laptop) is not an error.

func DryRunDeploymentAdd added in v0.3.5

func DryRunDeploymentAdd(ctx context.Context, cmd DeploymentAddCommand, host DryRunHost) (string, error)

DryRunDeploymentAdd runs a parsed "deployment add" through AddDeployment as a dry run: the same validation as the command, against the files in host, and it writes nothing. It skips only the root check, which is meaningless for a run that cannot write, and it maps absolute paths below host.Root (a ".." stays visible, so the check that refuses it still sees it). It returns what the command would print.

func ListDeployments added in v0.3.5

func ListDeployments(w io.Writer, configPath string) error

ListDeployments prints the deployments of agent.yaml. It reads only, so it also runs without root and on hosts that are not Linux.

func ParseFlagsAnywhere added in v0.3.5

func ParseFlagsAnywhere(fs *flag.FlagSet, args []string) ([]string, bool)

ParseFlagsAnywhere parses flags that may stand before and after the positional arguments (deployment add NAME --dir X, or --dir X NAME).

func RemoveDeployment added in v0.3.5

func RemoveDeployment(ctx context.Context, opts RemoveOptions) error

RemoveDeployment removes one deployment from agent.yaml. It keeps paths unless PrunePaths is set and never touches deployed files or the state.

func RenewAt

func RenewAt(leaf *x509.Certificate, r float64) time.Time

RenewAt is when the agent renews its certificate: at 2/3 of the lifetime, up to 10 % of it earlier (r in [0,1) from the caller), so agents enrolled together don't renew together.

func ScrubProviderEnvironment added in v0.3.7

func ScrubProviderEnvironment() []string

ScrubProviderEnvironment removes the variables lego and its providers read (LEGO_*, CLOUDFLARE_*, CF_*, RFC2136_*) from the process environment, so that nothing outside agent.yaml and the credentials file can configure a DNS provider or turn on lego's debug output. It also removes SSL_CERT_FILE and SSL_CERT_DIR: the Cloudflare client trusts the system roots, and on Linux those two variables decide which CAs that is, so they would decide who may receive the API token. Nothing else of the agent reads them: the server channel and the ACME client use the CA bundle of agent.yaml, the check of the served certificate compares the exact leaf, and the hooks are fixed commands that then see the default trust, too. It returns the names it removed, never the values.

func Setup added in v0.3.1

func Setup(ctx context.Context, opts SetupOptions) error

Setup installs local configuration and units, enrolls, and only then enables or starts the service. It never changes host state before preflight succeeds.

func SplitCommand added in v0.3.5

func SplitCommand(s string) ([]string, error)

SplitCommand splits "CMD ARG ..." into an argument vector: white space separates, single quotes keep everything, double quotes keep everything except \" and \\. There is no shell: no expansion, no pipes, no variables.

func SyncSandbox added in v0.3.5

func SyncSandbox(ctx context.Context, opts SyncOptions) error

SyncSandbox recomputes both drop-ins from agent.yaml (what a Salt run calls after it rendered the file). Entries the file does not explain are kept and reported; Adopt records them under sandbox.write, Prune drops them.

func Under

func Under(dirs []string, p string) bool

Under reports whether p lies in one of dirs (lexically, after Clean). At write time the agent also resolves symlinks (see resolveUnder).

Types

type AddOptions added in v0.3.5

type AddOptions struct {
	EditOptions
	Spec       DeploymentSpec
	CreateDirs bool
}

AddOptions are the inputs of AddDeployment.

type Agent

type Agent struct {

	// Random returns a value in [0,1); tests inject stable independent streams.
	Random func() float64

	// Version, OS, Arch and PackageFormat are reported to the server.
	Version, OS, Arch string
	PackageFormat     string
	// StandaloneAddr is where challenge: standalone listens (":80").
	StandaloneAddr string
	// Now and Sleep: the clock and the wait; tests replace them.
	Now   func() time.Time
	Sleep func(ctx context.Context, d time.Duration) error
	// contains filtered or unexported fields
}

Agent is myownpki-agent.

func New

func New(cfg *Config, log *slog.Logger, version string) (*Agent, error)

New prepares the agent: the state directory, the trust anchor and the channel; the identity on disk, if there is one, becomes the client certificate.

func (*Agent) Enroll

func (a *Agent) Enroll(ctx context.Context, token string) (*Result, error)

Enroll enrolls this host: a new key – or, when its certificate expired, the key of the last certificate (the server knows it then) – a CSR, the check file, the decision. A pending enrollment is kept for `run`.

func (*Agent) Identity

func (a *Agent) Identity() *tls.Certificate

Identity returns the agent's current certificate, or nil.

func (*Agent) Obtain

func (a *Agent) Obtain(ctx context.Context, d Deployment) (*Material, error)

Obtain gets a new certificate by the ordinary ACME path (ADR-19). Scheduled and operator-requested renewals use this same operation.

func (*Agent) Poll

func (a *Agent) Poll(ctx context.Context) (*Result, error)

Poll asks for the decision on the kept enrollment; nil result: none open.

func (*Agent) Recover

func (a *Agent) Recover(ctx context.Context) ([]DeploymentResult, error)

Recover restores one interrupted group before any new ACME/deploy pass. Reload/check failures are reported once; they never start a retry loop.

func (*Agent) Run

func (a *Agent) Run(ctx context.Context) error

Run maintains the agent identity, reports deployment results and follows desired state. Certificate and deployment renewals keep their own schedules.

func (*Agent) RunPass

func (a *Agent) RunPass(ctx context.Context, names []string) ([]DeploymentResult, error)

RunPass renews the named deployments through their local configuration. nil selects all deployments. No request can supply paths or hook names. The runtime calls the same operation for scheduled and requested renewals.

func (*Agent) SetACMEDial

func (a *Agent) SetACMEDial(dial func(context.Context, string, string) (net.Conn, error))

SetACMEDial replaces the ACME network edge for tests. TLS still verifies the directory's host against ca_bundle, with the ordinary HTTP client.

func (*Agent) SetDNSTest added in v0.3.7

func (a *Agent) SetDNSTest(t DNSTest)

SetDNSTest points the dns-01 path at test doubles – for tests only.

func (*Agent) SetDial

func (a *Agent) SetDial(dial func(ctx context.Context, network, addr string) (net.Conn, error))

SetDial replaces how the agent connects to the server – for tests only (the name in server still is what TLS verifies).

func (*Agent) Status

func (a *Agent) Status() Status

Status reads the state directory.

type Config

type Config struct {
	// Server is agent.base_url of the server, e.g. https://pki.homelab.lan:8444.
	Server string `yaml:"server"`
	// CABundle is the trust anchor (PEM) for the agent listener and ACME.
	CABundle string `yaml:"ca_bundle"`
	// Hostname is the FQDN the agent proves at enrollment; "" = the host's name.
	Hostname string `yaml:"hostname"`
	// ACMEDirectory is the directory URL of any ACME CA.
	ACMEDirectory string `yaml:"acme_directory"`
	// Proxy is an http(s) proxy for both connections; environment variables
	// (HTTPS_PROXY, HTTP_PROXY, NO_PROXY) are never used (owner 2026-10-24).
	Proxy string `yaml:"proxy"`
	// StateDir holds the agent's key, certificate and enrollment.
	StateDir string `yaml:"state_dir"`
	// LogLevel: debug, info, warn, error.
	LogLevel string `yaml:"log_level"`

	Enrollment Enrollment `yaml:"enrollment"`
	// DNSProviders are the dns-01 settings a deployment refers to by name
	// (docs/PHASE3.md §13.2). They never hold a secret, only the path of the
	// credentials file.
	DNSProviders map[string]DNSProvider `yaml:"dns_providers"`
	Hooks        map[string][]string    `yaml:"hooks"`
	Paths        []string               `yaml:"paths"`
	Sandbox      Sandbox                `yaml:"sandbox"`
	Deployments  []Deployment           `yaml:"deployments"`
}

Config is agent.yaml.

func LoadConfig

func LoadConfig(path string) (*Config, error)

LoadConfig reads and validates agent.yaml.

func ParseConfig

func ParseConfig(name string, data []byte) (*Config, error)

ParseConfig decodes and validates; name is for messages.

func (*Config) DNSProviderStatuses added in v0.3.7

func (c *Config) DNSProviderStatuses() []DNSProviderStatus

DNSProviderStatuses loads the credentials file of every provider once to see whether it can be used, wipes the result at once, and never reports content. The providers come in the order of their names.

func (*Config) Validate

func (c *Config) Validate() error

Validate checks every rule of docs/PHASE3.md §1.3 and returns all problems at once.

func (*Config) Warnings added in v0.3.5

func (c *Config) Warnings() []string

Warnings are things "config check" should mention although the file is valid: they never stop the agent (an old configuration keeps working).

type DNSProvider added in v0.3.7

type DNSProvider struct {
	// Type is rfc2136 or cloudflare.
	Type string `yaml:"type"`
	// CredentialsFile is an absolute path, read at the start of every issuance.
	CredentialsFile string `yaml:"credentials_file"`
	// Nameserver is host or host:port (default port 53); rfc2136 only.
	Nameserver string `yaml:"nameserver"`
	// Zone is the zone to update; rfc2136 only, "" = found by an SOA lookup.
	Zone string `yaml:"zone"`
	// TTL of the TXT record in seconds (default 120).
	TTL int `yaml:"ttl"`
	// Resolver is IP or IP:port used for SOA, CNAME and propagation queries.
	Resolver string `yaml:"resolver"`
	// Propagation is authoritative (default), resolver or wait.
	Propagation string `yaml:"propagation"`
	// PropagationWait is a pause in seconds before the first check (default 0).
	PropagationWait int `yaml:"propagation_wait"`
	// PropagationTimeout is how long the agent polls, in seconds (default 120).
	PropagationTimeout int `yaml:"propagation_timeout"`
}

DNSProvider is one entry of dns_providers: how and where the agent writes the TXT record of a dns-01 challenge. It holds no secret; CredentialsFile names the root-only file with the TSIG key or the API token (see dns_creds.go).

type DNSProviderStatus added in v0.3.7

type DNSProviderStatus struct {
	Name   string
	Type   string
	Path   string
	State  string   // ok, missing, wrong mode, ...
	Detail string   // what to change; never a value
	Users  []string // deployments that use the provider
}

DNSProviderStatus is the state of one provider's credentials file, for `config check`. It holds no content of the file.

func (DNSProviderStatus) OK added in v0.3.7

func (s DNSProviderStatus) OK() bool

OK reports whether the credentials file can be used.

type DNSTest added in v0.3.7

type DNSTest struct {
	// CloudflareURL is the API base of the stub, "" for the real API.
	CloudflareURL string
	// CloudflareRoots trusts the stub's certificate.
	CloudflareRoots *x509.CertPool
	// Poll replaces the 2 s interval of the propagation check (0 = unchanged).
	Poll time.Duration
}

DNSTest is what an end-to-end test of dns-01 replaces from outside this package, like SetDial and SetACMEDial do for the connections: the address of a Cloudflare stub, the roots that vouch for it, and the polling interval of the propagation check. Production never sets it and agent.yaml has no key for it.

type Deployment

type Deployment struct {
	Name      string   `yaml:"name"`
	Profile   string   `yaml:"profile"`
	Names     []string `yaml:"names"`
	Challenge string   `yaml:"challenge"`
	Webroot   string   `yaml:"webroot"`
	// DNSProvider names an entry of dns_providers; required for dns-01.
	DNSProvider string `yaml:"dns_provider"`
	Dir         string `yaml:"dir"`
	Files       Files  `yaml:"files"`
	Owner       string `yaml:"owner"`
	Group       string `yaml:"group"`
	KeyMode     string `yaml:"key_mode"`
	TestHook    string `yaml:"test_hook"`
	ReloadHook  string `yaml:"reload_hook"`
	Check       string `yaml:"check"`
	// CheckName is the TLS server name (SNI) of the local check; "" = the
	// first name without "*". A wildcard is not a host name, so a deployment
	// of wildcard names only needs it when it has a check.
	CheckName string `yaml:"check_name"`
}

Deployment is one certificate the agent keeps in place (block C).

type DeploymentAddCommand added in v0.3.5

type DeploymentAddCommand struct {
	ConfigPath string
	DryRun     bool
	Restart    bool
	CreateDirs bool
	Spec       DeploymentSpec
}

DeploymentAddCommand is a parsed "myownpki-agent deployment add": the options of AddDeployment, with the recipe already applied.

func ParseDeploymentAdd added in v0.3.5

func ParseDeploymentAdd(fs *flag.FlagSet, configPath *string, args []string) (DeploymentAddCommand, error)

ParseDeploymentAdd defines the flags of "deployment add" on fs and parses args with them. It is the one place that knows the flags: the command and the tests of the Add host assistant (which must prove that a generated line means what the page says) both call it. configPath is the --config flag the caller defined on fs. A failure to parse is a *UsageError.

type DeploymentResult

type DeploymentResult struct {
	Name                  string    `json:"name"`
	State                 string    `json:"state"`
	Serial                string    `json:"serial,omitempty"`
	NotAfter              time.Time `json:"not_after,omitempty"`
	Fingerprint           string    `json:"fingerprint,omitempty"`
	WrittenFingerprint    string    `json:"written_fingerprint,omitempty"`
	CheckConfigured       bool      `json:"check_configured"`
	DeployedAt            time.Time `json:"deployed_at,omitempty"`
	LastError             string    `json:"last_error,omitempty"`
	AttemptedAt           time.Time `json:"attempted_at,omitempty"`
	Failures              int       `json:"failures,omitempty"`
	NextAttempt           time.Time `json:"next_attempt,omitempty"`
	HandledRenewalVersion int64     `json:"handled_renewal_version,omitempty"`
}

DeploymentResult contains only reportable metadata, never key material, hook output, paths or commands. The runtime sends it over Report.

type DeploymentSpec added in v0.3.5

type DeploymentSpec struct {
	Name          string
	Profile       string
	Names         []string
	Challenge     string
	Webroot       string
	Dir           string
	Files         Files
	Owner         string
	Group         string
	KeyMode       string
	Hooks         []HookSpec
	TestHook      string
	ReloadHook    string
	Check         string
	SandboxWrite  []string
	LowPorts      bool
	ACMEDirectory string

	// CheckName is the TLS server name of Check (check_name).
	CheckName string
	// DNSProvider names the dns_providers entry of a dns-01 deployment. The
	// DNS fields below define that entry when it is new: they are written
	// only for a name that agent.yaml does not have yet, and they must match
	// the definition of one that it has (docs/PHASE3.md §13.1, step 42).
	DNSProvider    string
	DNSType        string
	DNSCredentials string
	DNSNameserver  string
	DNSZone        string
	DNSResolver    string

	// Recipe names the recipe the spec was filled from ("" = none); it only
	// changes what is printed.
	Recipe string
	// contains filtered or unexported fields
}

DeploymentSpec is what "deployment add" asks for, before defaults.

type DropinOptions added in v0.3.5

type DropinOptions struct {
	// LowPorts wants 20-low-ports.conf although the configuration does not
	// imply it (setup --low-ports, deployment add --low-ports).
	LowPorts bool
	// Prune drops entries of 10-paths.conf that agent.yaml does not explain.
	Prune bool
	// Drop names entries to drop although Prune is off: paths this very
	// operation took out of agent.yaml. Entries that are still derived stay.
	Drop []string
}

DropinOptions are the inputs besides agent.yaml and the files as found.

type DropinPlan added in v0.3.5

type DropinPlan struct {
	// Paths is the content of 10-paths.conf; it equals the file as found
	// when PathsChanged is false.
	Paths        []byte
	PathsChanged bool
	// Added and Removed are the entries this plan puts into or takes out of
	// the file; Kept are the entries found that agent.yaml does not explain.
	Added, Removed, Kept []string

	LowPorts     LowPortsAction
	LowPortsData []byte
	// Notes are one-line remarks for the operator.
	Notes []string
}

DropinPlan is the result of RenderDropins.

func RenderDropins added in v0.3.5

func RenderDropins(cfg *Config, existing DropinState, lowPortData []byte, opts DropinOptions) (*DropinPlan, error)

RenderDropins derives both drop-ins. lowPortData is the packaged 20-low-ports.conf. It reads nothing from disk and writes nothing.

func (*DropinPlan) InSync added in v0.3.5

func (p *DropinPlan) InSync() bool

InSync reports whether the files on disk already match the configuration.

type DropinState added in v0.3.5

type DropinState struct {
	Paths        []byte
	HavePaths    bool
	LowPorts     []byte
	HaveLowPorts bool
}

DropinState is what is on disk in the unit's .service.d directory.

func ReadDropins added in v0.3.5

func ReadDropins(dir string) (DropinState, error)

ReadDropins reads both drop-ins; a missing file or directory is not an error.

type DryRunHost added in v0.3.5

type DryRunHost struct {
	// Root is a directory that stands for "/": every absolute path of the
	// command (directories, files, hook commands) is looked for below it.
	Root string
	// ConfigPath and UnitPath are the real files of the host, already below
	// Root (agent.yaml and the unit; the drop-ins are the unit's .d directory).
	ConfigPath string
	UnitPath   string
	// Stderr receives warnings and hints; nil discards them.
	Stderr io.Writer
}

DryRunHost describes the host a parsed "deployment add" is tried on. It exists for the tests of the Add host assistant (and any other generator of command lines): they must prove that a line they print is accepted by the agent and means what the page says, on a development machine that is not a Linux host and is not root.

type EditOptions added in v0.3.5

type EditOptions struct {
	ConfigPath string
	UnitPath   string
	DropinDir  string
	ExampleDir string
	DryRun     bool
	Restart    bool
	Stdout     io.Writer
	Stderr     io.Writer
	// contains filtered or unexported fields
}

EditOptions are the inputs every editing command shares.

type Enrollment

type Enrollment struct {
	Challenge string `yaml:"challenge"`
	Webroot   string `yaml:"webroot"`
}

Enrollment says how the enrollment check is answered.

type Files

type Files struct {
	Certificate string `yaml:"certificate"`
	Chain       string `yaml:"chain"`
	Fullchain   string `yaml:"fullchain"`
	Key         string `yaml:"key"`
}

Files are the targets of the file profile.

type HookSpec added in v0.3.5

type HookSpec struct {
	Name    string
	Command []string
}

HookSpec is one hook a deployment brings: a name and an argument vector.

func ParseHookFlag added in v0.3.5

func ParseHookFlag(v string) (HookSpec, error)

ParseHookFlag parses NAME='CMD ARGS'.

type LowPortsAction added in v0.3.5

type LowPortsAction int

LowPortsAction says what to do with 20-low-ports.conf.

const (
	LowPortsKeep   LowPortsAction = iota // nothing to do (file as found)
	LowPortsWrite                        // needed and missing: write LowPortsData
	LowPortsRemove                       // sandbox.low_ports: false and the file exists
)

The actions of 20-low-ports.conf.

type Material

type Material struct {
	Certificate []byte            `json:"-"`
	Chain       []byte            `json:"-"`
	Fullchain   []byte            `json:"-"`
	Key         []byte            `json:"-"`
	Leaf        *x509.Certificate `json:"-"`
	// Notice is a problem that does not make the issuance fail, for the
	// deployment's status (a dns-01 record that could not be removed). It is
	// free of secrets and of provider text.
	Notice string `json:"-"`
}

Material is an issued certificate and its host-generated key. It is never sent over the agent channel or encoded into a report.

func (*Material) LogValue

func (*Material) LogValue() slog.Value

func (*Material) String

func (*Material) String() string

type RemoveOptions added in v0.3.5

type RemoveOptions struct {
	EditOptions
	Name       string
	PrunePaths bool
}

RemoveOptions are the inputs of RemoveDeployment.

type Result

type Result struct {
	State   agentv1.EnrollState
	Message string
	AgentID string
}

Result is where an enrollment stands, for the operator.

func (*Result) Admitted

func (r *Result) Admitted() bool

Admitted reports whether the agent has its certificate now.

type Sandbox added in v0.3.5

type Sandbox struct {
	// Write lists directories that hooks (not the agent) write, for example
	// /var/log/nginx for "nginx -t". Only ReadWritePaths gets them.
	Write []string `yaml:"write"`
	// LowPorts is the 20-low-ports.conf drop-in (CAP_NET_BIND_SERVICE).
	// Unset: written when any challenge is standalone, otherwise left as
	// found. false removes it; true writes it.
	LowPorts *bool `yaml:"low_ports"`
}

Sandbox is the part of the systemd sandbox that agent.yaml cannot derive by itself (docs/PHASE3.md §11.2). Together with state_dir and paths it decides the drop-ins 10-paths.conf and 20-low-ports.conf.

type SetupOptions added in v0.3.1

type SetupOptions struct {
	Server        string
	CABundle      string
	RootSHA256    string
	TokenFile     string
	Webroot       string
	LowPorts      bool
	Hostname      string
	ConfigPath    string
	StateDir      string
	UnitPath      string
	DropinDir     string
	ExampleDir    string
	Version       string
	PackageFormat string
	ACMEDirectory string // https URL of the ACME directory, written into a new agent.yaml
	Stdout        io.Writer
	Stderr        io.Writer
}

SetupOptions are the local inputs of myownpki-agent setup.

type Status

type Status struct {
	Hostname    string
	AgentID     string
	Enrolled    bool
	NotAfter    time.Time
	Serial      string
	Enrollment  string // the open enrollment's last message
	Deployments []DeploymentResult
}

Status describes the agent for `myownpki-agent status`.

type SyncOptions added in v0.3.5

type SyncOptions struct {
	EditOptions
	Adopt bool
	Prune bool
}

SyncOptions are the inputs of SyncSandbox.

type UsageError added in v0.3.5

type UsageError struct{ Message string }

UsageError is a command line that could not be used. Message is "" when the flag package has already said why.

func (*UsageError) Error added in v0.3.5

func (e *UsageError) Error() string

Jump to

Keyboard shortcuts

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