zarfmod

package
v0.30.0 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

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

View Source
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"
)
View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
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

func ModuleName(argv0 string) (string, bool)

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.

func Modules

func Modules() []string

Modules returns the actions this wrapper answers as, sorted.

func Run

func Run(ctx context.Context, name string, argv []string, run Runner) int

Run executes module mode and returns the process exit status.

argv is the process argument vector. Ansible invokes a WANT_JSON module with one argument, the path of a file holding the parameters as JSON.

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

func ReadArgs(r io.Reader) (*Args, error)

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

func ReadArgsFile(path string) (*Args, error)

ReadArgsFile reads and parses the arguments file at path, which is how a WANT_JSON caller delivers parameters.

func (*Args) Params

func (a *Args) Params(out any) error

Params decodes the module's own parameters into out, which must be a pointer to a struct whose JSON tags name them.

A parameter the struct does not name is an error naming it, and the error lists what the module does accept.

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

func NewHeartbeat(path string) *Heartbeat

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.

func (*Heartbeat) Path

func (h *Heartbeat) Path() string

Path is the file being written, empty when there is none.

func (*Heartbeat) Update

func (h *Heartbeat) Update(phase string, index, total int, state string, cause error)

Update writes one heartbeat.

Failures are dropped on purpose. This is a side channel for a progress display: a run that is deploying a cluster must not fail because the directory holding its status file went away.

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 Result

type Result struct {
	ExitCode int
}

Result is what running zarf produced.

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.

Jump to

Keyboard shortcuts

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