Skip to content

Zester v0.4.2

Choose a tag to compare

@github-actions github-actions released this 10 Jul 05:49
· 66 commits to main since this release

Fleet-wide state-module convergence audit. After the pkg.latest
stale-index bug shipped in the field, every state module (44 states, 36
files) was audited against the Check/Apply contract; 48 adversarially
verified defects across 25 states are fixed in this release. Three defect
classes dominated: Check blind to a facet only Apply enforces (drift never
converges, reported compliant forever), destructive or dishonest Revert
paths, and read errors conflated with "file absent" (data-loss paths).

Fixed

  • pkg.latest refreshes the package cache BEFORE checking upgradability
    (Salt parity). Refresh (default on) ran only in Apply, but Check consulted
    the stale index and short-circuited "already at latest" — so Apply, and
    with it the refresh, never executed: any release published after the box's
    last cache refresh was invisible (field symptom: the first fleet-wide
    pkg.latest zester-peel after the 0.4.1 repo publish was a silent
    changed: 0 no-op on every apt host). Check and Apply now each run their
    own refresh independently. A FAILED refresh (one rotted third-party repo
    fails apt-get update while reachable repos still updated) warns and
    proceeds in both phases instead of failing the state; --test dry runs
    refresh the index too (metadata-only).
  • pkg.installed honors a declared version: pin in Check. Any
    installed version used to satisfy a pinned state, so version drift was
    compliant forever. Check now compares the installed version against the
    pin (got/want diff). Pinned downgrades converge on every provider: apt
    passes --allow-downgrades, yum verifies the pin landed and falls back
    to yum downgrade (plain yum install pkg-<older> silently no-ops),
    dnf handles explicit version downgrades natively. Undeclared version:
    is unchanged.
  • pkg.purged treats a removed-but-not-purged Debian package (dpkg rc
    state) as needing a purge
    — converged only when no package record
    exists at all; previously rc reported converged and leftover conffiles
    were never removed. The apt probes also fail loudly when the probe never
    ran (spawn failure/context death) instead of reporting "not installed",
    and parse multi-arch dpkg output correctly.
  • cron.present, sysctl.present, and mount.mounted now actually work
    on real peels.
    Their exec providers (crontab/procfs/fstab) were never
    wired into provider detection — every real-peel run failed with "no
    cron/sysctl/mount provider available" (unit tests wired providers
    manually, masking it).
  • file.managed enforces the mode: facet on pre-existing files.
    Apply wrote content via a create-time-perm-only write and never chmodded,
    so mode drift on an existing file was reported by Check forever but never
    fixed — permanent churn firing watch dependents every highstate. Apply
    now chmods after writing; Revert restores the CAPTURED prior mode, not
    the desired one. Octal modes with setuid/setgid/sticky digits
    ("4755", "1777") are translated to the Go FileMode flags — previously the
    special bit was silently dropped by chmod and the state re-applied forever
    without ever setting it — and ownership is applied BEFORE mode (chown on
    an executable clears setuid). Applies to file.managed, file.directory,
    and file.recurse modes.
  • Debian rc-state packages (removed, conffiles remain) no longer count
    as installed.
    The apt probe requires dpkg status installed
    (dpkg-query -W -f='${db:Status-Status}') instead of the dpkg -s exit
    code — pkg.installed could never reinstall a previously-removed
    conffile-bearing package, and pkg.removed re-applied (and fired watch
    dependents) on every highstate after its own successful removal.
  • pkgrepo.managed verifies the Debian signing key. The key_url key
    now lands persistently at /etc/apt/keyrings/zester-<name>.gpg (was a
    volatile /tmp download) and Check reports drift when it is missing —
    previously a never-imported/deleted key left apt-get update failing
    NO_PUBKEY while the state reported converged. Presence-only (in-place key
    rotation at the same URL is not detected); Revert removes the artifact.
  • File states detect ownership drift. file.managed, file.directory,
    and file.recurse compare on-disk owner/group against declared
    user:/group: in Check (declared facets only — undeclared ownership
    never churns). file.recurse Check also flags clean: true extra files
    (previously the clean feature could never fire once the managed set
    converged) and dir_mode drift on every managed dir, and all its
    filesystem walks now go through the injected file provider
    (new FileExec.Walk).
  • service.running/service.dead compare a declared enable: facet in
    Check
    — running-but-disabled (enable: true) and stopped-but-enabled
    (enable: false, resurrects at reboot) were compliant forever.
    service.running with enable: false now actually disables; an Apply
    reached only for enable drift never restarts the running service.
  • user.present converges password and name-based primary group. A
    declared password: hash is compared against the shadow hash (new
    UserExec.PasswordHash); a name-based gid:/primary_group: is compared
    and enforced on existing users via usermod -g — both facets were
    silently unenforced outside user creation.
  • mount.mounted fstab comparison now includes dump/pass, Apply
    no-ops honestly when converged, and Revert no longer claims an unmount it
    never performed. (Comparing the LIVE mount — a wrong device/options
    serving the mountpoint — is deferred: it needs Salt-style
    option/fstype/device normalization to avoid remount churn on
    kernel-normalized values; that audit finding stays open on the backlog.)
  • sysctl.present with persist: true verifies the drop-in file entry,
    not just the runtime value — a manual sysctl -w match silently died at
    the next reboot.
  • cron.present entries are keyed on the label (identifier comment,
    Salt semantics) instead of the exact command string — editing a state's
    command replaces the old line instead of orphaning it to run forever.
    Managed entries carry a # ZESTER_CRON_ID: <label> marker; pre-existing
    same-command lines are adopted (a human descriptive comment neither
    blocks adoption nor duplicates the job) and stamped on the next apply.
    Converged Apply is a no-op, so watch-forced runs no longer rewrite the
    crontab. Crontab edits are LINE-PRESERVING: Set/Remove splice only
    the targeted entry and its marker — MAILTO=/PATH= environment lines,
    human comments, blank lines, and @reboot/@daily nickname entries survive
    untouched (the previous whole-crontab reconstruction silently destroyed
    them), and a Remove that matches nothing does not rewrite the crontab at
    all. Commands are compared whitespace-normalized, so a declared command
    with consecutive spaces converges instead of rewriting every run.
  • git.cloned/git.latest with a symbolic rev: (tag) converge
    the rev was compared against the HEAD sha by string prefix, which never
    matches a tag name, so every run re-applied (needless fetches, watch
    cascades, service restarts every highstate). Symbolic revs resolve locally
    via git rev-parse --verify <rev>^{commit}; sha prefixes still match
    directly.
  • locale.present verifies the /etc/locale.gen enabling line that
    Apply writes, not just locale -a membership — an out-of-band-generated
    locale was compliant until the next locales package upgrade silently
    dropped it. The facet applies only where /etc/locale.gen exists
    (Debian-family); RHEL/musl systems stay satisfied by locale -a, no
    stray locale.gen is created, and charmap spellings are normalized
    (en_US.utf8 == en_US.UTF-8).
  • Read errors are no longer conflated with "file absent" anywhere.
    host.present, ssh_auth.present, every text-editing file state
    (file.line/append/blockreplace/comment/keyvalue/replace),
    file.managed/copy/recurse, cmd.run's creates probe,
    locale.present, and the archive marker all treated ANY read error as
    "missing" — a transient EIO/EACCES/ESTALE followed by a successful write
    could truncate /etc/hosts or a user's authorized_keys down to the one
    managed line, or clobber file content that was never captured. Only
    fs.ErrNotExist selects the absent path now; anything else fails the
    phase without writing.
  • Revert contract: a fresh instance never destroys state. Revert
    consuming in-instance memos (backups, created flags, saved originals)
    treated "memo unset" as "file was new" and DELETED the target — and the
    runner builds FRESH instances for revert, so a revert run would have wiped
    /etc/hosts, authorized_keys, or any managed file. Fresh-instance
    Revert is now an explicit clean no-op across ALL modules
    ("nothing to revert (no apply recorded in this run)"); same-instance
    Apply→Revert still restores backups (with canonical permissions —
    authorized_keys restores 0600) and still removes files the same
    instance created. (tolerating an already-externally-removed file). Related honesty fixes: sysctl.present Revert no
    longer writes an EMPTY value into the kernel and persist file;
    mount.mounted Revert no longer claims an unmount it never performed;
    group.present Revert really restores membership (it only restored the
    GID); service.running Revert reports enable-reverts.
  • Watch-forced applies are safe on guarded/absent states. cmd.run's
    creates guard now also gates Apply (a watch trigger used to re-run
    creates-guarded one-shots like initdb); user.absent/group.absent
    Apply no-op cleanly when already absent instead of failing userdel/
    groupdel; service.dead/mount.mounted/sysctl.present/user.present
    Apply can now honestly report changed: false when converged.
    archive.extracted Apply re-evaluates its if_missing/source_hash
    guard itself, so a watch trigger no longer re-downloads and re-extracts a
    converged archive.

Added

  • archive.extracted gains source_hash: recorded in a marker after a
    successful extraction and compared by Check, so bumping
    source/source_hash re-extracts instead of no-oping forever. Opaque
    string comparison (not byte verification); the marker is only written
    after success, so a failed extract no longer latches a makedirs-created
    dir as done. Without source_hash, existing marker semantics are
    unchanged and now documented.
  • Exec-layer convergence probes (internal API): status-aware apt
    installed-probe, PackageExec.InstalledVersion, FileExec.Owner/Walk,
    UserExec.PasswordHash, UserModifyOpts.PrimaryGroup; test fakes wrap
    fs.ErrNotExist for missing files and support read-error injection.

Changed

  • pkg.removed Revert is an explicit clean no-op — it previously
    reinstalled the repo's latest candidate driven by a never-populated memo.
    Reinstall explicitly with pkg.installed.
  • pkgrepo.managed Debian key artifact moved from
    /tmp/zester-repo-<name>.gpg to /etc/apt/keyrings/zester-<name>.gpg;
    existing fleets show ONE pending change per keyed repo on the next
    highstate (idempotent re-import).