Documentation
¶
Overview ¶
Package zarfmod lets a small wrapper binary answer Ansible as a zarf module.
This is a proof of ZEP-0072 (https://github.com/zarf-dev/proposals/pull/73) built where the pattern it copies already exists. It differs from that proposal in one way that matters: the module here is not the zarf binary. ZEP-0072 proposes putting module dispatch inside zarf itself, which this repository cannot do, so each module file is a separate wrapper binary (cmd/zarf/init, cmd/zarf/deploy) that renders the zarf command line, runs the installed zarf, and reports the run. Everything else -- argv[0] dispatch, the WANT_JSON intake, the stdout guard, the single JSON response, the heartbeat side channel -- is the shape the proposal describes, and is modeled on internal/ansiblemod. See docs/agent/choice-zarf-ansible-module.md.
A wrapper does not reimplement the command it drives. It turns the module's JSON parameters into the argument vector an operator would have typed and runs that, so component selection, credential handling, signature verification, and timeouts stay on zarf's own path.
Index ¶
Constants ¶
const ( // Prefix is the basename prefix that puts a wrapper binary into module mode. Prefix = "zarf_" // AnsiballZ is the prefix Ansible puts on a module file when it copies it to the machine that // runs it. The copy is what executes, so the name the process sees is AnsiballZ_zarf_init // rather than the zarf_init the collection holds. AnsiballZ = "AnsiballZ_" // EnvModule selects a module without a symlink. It is what the action plugin sets when it // invokes the wrapper directly, and what a test or an operator reproducing a run by hand uses. EnvModule = "ZARF_ANSIBLE_MODULE" )
const ( // SignalUnknown means nothing the run reported says whether it changed anything, so the // changed field above it is a convention rather than an observation. SignalUnknown = "unknown" // SignalPartial means some of the run reported and some did not. For init that is the normal // answer: zarf says which components it deployed, and says nothing about whether a component // found the cluster already in the state it wanted. SignalPartial = "partial" // SignalComplete means every component the run reached reported whether it changed anything. // Nothing produces this yet; it is here because the field is the proposal's contract and a // value the wrapper can never emit is a thing a reviewer should see rather than guess at. SignalComplete = "complete" )
Changed signal values reported in Detail.ChangedSignal.
const EnvBinary = "ZARF_ANSIBLE_BINARY"
EnvBinary names the zarf to run, for an operator who keeps more than one version staged and does not want to repeat the path in every task.
const EnvStatusFile = "ZARF_STATUS_FILE"
EnvStatusFile names the file live progress is written to. The action plugin creates the file and exports this variable; nothing is written when it is unset, which is what makes a run by hand behave the same as a run under Ansible minus the display.
internal/heartbeat already does this for cargoship, with CARGOSHIP_STATUS_FILE and a package-global target. It is not reused here: the variable name is part of the zarf contract ZEP-0072 describes, and a process that runs one module per invocation has no reason to keep the target in a global.
const MinZarfVersion = "0.72.0"
MinZarfVersion is the oldest zarf the modules support.
It is where `zarf init` gained its positional PACKAGE_SOURCE argument: before v0.72.0 the only way to choose an init package was to run zarf in the directory holding a file named after zarf's own version, and the wrapper did exactly that. Taking the package as an argument is what lets an operator stage a package under any name, in any directory, or hold it in a registry -- so the floor buys the parameter rather than merely tidying it.
Variables ¶
var ErrZarfTooOld = errors.New("the zarf binary is too old for this module")
ErrZarfTooOld is returned when the zarf on the machine predates MinZarfVersion.
Functions ¶
func ModuleName ¶
ModuleName returns the module this process was invoked as, and whether it was invoked as one at all. The environment variable wins, so the action plugin can invoke the installed wrapper without a symlink and a test can drive module mode through the binary it already builds.
The name has to be one of the actions above, not merely carry the prefix. A build artifact named zarf_linux_amd64 carries the prefix and is not a module, and answering it with JSON nobody asked for is worse than the cost of this check: a misspelled symlink reports "not a module" instead of naming the misspelling.
Types ¶
type Args ¶
type Args struct {
Control Control
// contains filtered or unexported fields
}
Args is the arguments document Ansible writes for a WANT_JSON module.
The parameters are held undecoded so that unknown ones can be reported by name. A binary module has no argument_spec, so this is the whole of what stands between a mistyped parameter and a run that silently ignores it.
func ReadArgs ¶
ReadArgs parses the arguments document from r, which is how the action plugin delivers parameters when it pipes them rather than writing them to disk.
func ReadArgsFile ¶
ReadArgsFile reads and parses the arguments file at path, which is how a WANT_JSON caller delivers parameters.
type Control ¶
type Control struct {
CheckMode bool `json:"_ansible_check_mode"`
Diff bool `json:"_ansible_diff"`
NoLog bool `json:"_ansible_no_log"`
Verbosity int `json:"_ansible_verbosity"`
}
Control is the part of a module's arguments Ansible writes rather than the operator.
type Detail ¶
type Detail struct {
// Module is the action that ran, so a failure in a playbook that loops over several says which
// one.
Module string `json:"module,omitempty"`
// CheckMode says whether this was a dry run, so a playbook cannot mistake a check-mode result
// for a real one.
CheckMode bool `json:"checkMode,omitempty"`
// Command is the zarf command line the wrapper ran, for an operator reproducing it by hand.
// Credential flag values are replaced before this is reported; see redact.
Command []string `json:"command,omitempty"`
// Directory is where that command ran. Zarf init finds the init package relative to the
// working directory, so the directory is part of what the command means.
Directory string `json:"directory,omitempty"`
// ExitCode is zarf's exit status. It is reported for a failure, where the message alone does
// not distinguish a zarf that refused from a zarf that was killed.
ExitCode int `json:"exitCode,omitempty"`
// ComponentsRan names the init components zarf reported deploying, in the order it reported
// them. Ansible shows one result for the whole run, so this is what a playbook has in place of
// per-component tasks.
ComponentsRan []string `json:"componentsRan,omitempty"`
// ChangedSignal says how much the changed field above is worth.
ChangedSignal string `json:"changedSignal,omitempty"`
// ChangedUndeclared names what did not say. It is what makes a partial signal actionable
// rather than a warning with nothing behind it.
ChangedUndeclared []string `json:"changedUndeclared,omitempty"`
// Version is what `zarf version` printed, when the wrapper was able to read it. The modules
// have a floor -- see MinZarfVersion -- so a run that failed on an older zarf has the version
// attached to the result rather than only in whatever zarf said.
Version string `json:"version,omitempty"`
// StatusFile is the heartbeat file the run wrote progress to, when one was asked for. It is
// reported so a failed run leaves a trail even if the monitor that was watching it is gone.
StatusFile string `json:"statusFile,omitempty"`
}
Detail is the wrapper's own report on the run.
type ExecRunner ¶
type ExecRunner struct{}
ExecRunner runs zarf as a child process.
func (ExecRunner) Run ¶
func (ExecRunner) Run(ctx context.Context, in Invocation) (Result, error)
Run executes the invocation and streams its output.
Both of zarf's streams are wired to the wrapper's stderr, not inherited. Stdout belongs to the module result, and zarf writes progress rendering to its own stdout: letting the child inherit fd 1 would put that rendering in the middle of the JSON object Ansible parses. Ansible shows stderr as module output, so nothing is lost by moving it there.
type Heartbeat ¶
type Heartbeat struct {
// contains filtered or unexported fields
}
Heartbeat writes progress to a status file for an external monitor to read.
Every write is a whole file written beside the target and renamed into place, because the reader is a Python thread polling on its own clock and a reader that can see half a document is a reader that has to be taught to ignore one.
func NewHeartbeat ¶
NewHeartbeat returns a heartbeat writing to path, or to the file named by ZARF_STATUS_FILE when path is empty. A heartbeat with no target still accepts every call and writes nothing.
func (*Heartbeat) Clear ¶
func (h *Heartbeat) Clear()
Clear removes the status file. The action plugin removes it too, in a finally block; this is for the invocation that was not driven by one.
type Invocation ¶
type Invocation struct {
// Binary is the zarf to run.
Binary string
// Args is the argument vector after the binary.
Args []string
// Dir is the working directory. Zarf init resolves the init package relative to it, so this is
// load-bearing rather than cosmetic.
Dir string
// Env is added to the wrapper's own environment, as KEY=VALUE.
Env []string
// OnLine is called with each line zarf writes, if set. It is how progress is read back out of
// the log stream.
OnLine func(string)
}
Invocation is one zarf command line to run.
type Response ¶
type Response struct {
Changed bool `json:"changed"`
Failed bool `json:"failed,omitempty"`
// Skipped is how a module says it did not run at all. It is set when an operator asked for
// check mode, because zarf init has no dry run to answer with.
Skipped bool `json:"skipped,omitempty"`
Msg string `json:"msg,omitempty"`
Zarf *Detail `json:"zarf,omitempty"`
}
Response is the single JSON object a module writes.
The field names outside the zarf key are Ansible's, not ours: changed, failed, and msg are what every module returns and what a playbook's conditionals are written against. Everything the wrapper has to say beyond those is nested under one key, so it cannot collide with a field Ansible gives a meaning to later.
type Runner ¶
type Runner interface {
Run(ctx context.Context, in Invocation) (Result, error)
}
Runner runs a zarf command line. It is an interface so a test can prove the argument vector, the working directory, and the progress reading without a zarf binary on the machine.
type Status ¶
type Status struct {
Phase string `json:"phase"`
Index int `json:"index"`
Total int `json:"total"`
Status string `json:"status"` // running, completed, failed
Timestamp time.Time `json:"timestamp"`
Error string `json:"error,omitempty"`
}
Status is one heartbeat. The shape is the one ZEP-0072 specifies, so the monitor written against the proposal reads what this writes.