Skip to content

install

install

OCI hook file generation and installation.

Writes two role-specific entrypoint scripts (nft-hook and reader-hook) plus a shared _oci_state.py ballast module to the target hooks directory, alongside the JSON descriptors that tell podman to invoke each one at createRuntime and poststop.

Scripts and descriptors both land in namespace_state_dir("shield") / "hooks" under the operator's paths.root. containers.conf is patched so podman scans that path. Each sibling package owns its own subtree under paths.root the same way (see terok_sandbox.supervisor.install).

Public entry points:

  • HooksInstaller — global installation lifecycle (install + uninstall).
  • install_hooks — per-container install used by HookMode.pre_start.

Pure file I/O — no runtime container interaction.

HooksInstaller(target_dir=_default_target_dir()) dataclass

Persistent installation of terok-shield's OCI hook pair.

The createRuntime/poststop hook pair must persist across container restarts: podman ≥ 5.x drops per-container --hooks-dir on stop/start (containers/podman#17935), so global hooks are the only reliable activation path until that upstream regression is fixed.

Scripts, ballast, and JSON descriptors all land in target_dir (default: namespace_state_dir("shield") / "hooks"). containers.conf is patched to register that path so podman discovers the descriptors on the next container start.

Symmetric lifecycle: install writes, uninstall removes. Both are idempotent.

target_dir = field(default_factory=_default_target_dir) class-attribute instance-attribute

Directory the hook scripts, ballast, and JSON descriptors all live in.

install()

Write entrypoints, ballast, and descriptors to target_dir.

Both hook pairs (nft + reader) and the shared ballast are written unconditionally — the reader hook soft-fails on missing clearance, so installing it on a shield-only host costs nothing and removes a configuration knob. The standalone NFLOG reader resource is copied to its canonical per-user path.

containers.conf is patched to list target_dir in hooks_dir so podman discovers the descriptors.

Source code in src/terok_shield/hooks/install.py
def install(self) -> None:
    """Write entrypoints, ballast, and descriptors to ``target_dir``.

    Both hook pairs (nft + reader) and the shared ballast are
    written unconditionally — the reader hook soft-fails on
    missing clearance, so installing it on a shield-only host
    costs nothing and removes a configuration knob.  The
    standalone NFLOG reader resource is copied to its canonical
    per-user path.

    ``containers.conf`` is patched to list ``target_dir`` in
    ``hooks_dir`` so podman discovers the descriptors.
    """
    install_reader_resource()
    self.target_dir.mkdir(parents=True, exist_ok=True)
    _write_role_files(self.target_dir, self.target_dir)
    ensure_user_hooks_dir_configured(self.target_dir)

uninstall()

Remove every hook file install would write.

Idempotent — missing files are tolerated. containers.conf is left untouched: other terok packages may still register their own hooks_dir entries the operator wants to keep.

Source code in src/terok_shield/hooks/install.py
def uninstall(self) -> None:
    """Remove every hook file [`install`][terok_shield.hooks.install.HooksInstaller.install] would write.

    Idempotent — missing files are tolerated.  ``containers.conf``
    is left untouched: other terok packages may still register
    their own ``hooks_dir`` entries the operator wants to keep.
    """
    for name in (*_SCRIPT_FILES, *_DESCRIPTOR_FILES):
        (self.target_dir / name).unlink(missing_ok=True)

is_installed()

True when target_dir carries the canonical createRuntime hook JSON.

A presence probe, not a version check — the Shield.check_environment path compares the ballast's BUNDLE_VERSION separately.

Source code in src/terok_shield/hooks/install.py
def is_installed(self) -> bool:
    """True when ``target_dir`` carries the canonical createRuntime hook JSON.

    A presence probe, not a version check — the
    [`Shield.check_environment`][terok_shield.Shield.check_environment]
    path compares the ballast's ``BUNDLE_VERSION`` separately.
    """
    return (self.target_dir / _nft_hook_json("createRuntime")).is_file()

install_hooks(*, hook_entrypoint, hooks_dir)

Write OCI hook entrypoints, ballast, and JSON descriptors.

Lays down both role scripts (nft + reader) plus the shared OCI ballast in hooks_dir. hook_entrypoint names both the target directory and the on-disk filename for the nft script — callers that pin a non-default name (per-container installs, future test scaffolding) get exactly the path they asked for in the JSON descriptors. The reader entrypoint and _oci_state.py ballast land in the same parent directory under their canonical names.

WORKAROUND(hooks-dir-persist): currently only used for global hooks because podman does not persist per-container --hooks-dir across stop/start. The per-container code path is kept for near-future use.

Parameters:

Name Type Description Default
hook_entrypoint Path

Where to write the nft entrypoint script. The reader entrypoint and _oci_state.py ballast land in the same parent directory.

required
hooks_dir Path

Directory for hook JSON descriptors.

required
Source code in src/terok_shield/hooks/install.py
def install_hooks(*, hook_entrypoint: Path, hooks_dir: Path) -> None:
    """Write OCI hook entrypoints, ballast, and JSON descriptors.

    Lays down both role scripts (nft + reader) plus the shared OCI
    ballast in ``hooks_dir``.  ``hook_entrypoint`` names both the
    target directory **and** the on-disk filename for the **nft**
    script — callers that pin a non-default name (per-container
    installs, future test scaffolding) get exactly the path they
    asked for in the JSON descriptors.  The reader entrypoint and
    ``_oci_state.py`` ballast land in the same parent directory under
    their canonical names.

    WORKAROUND(hooks-dir-persist): currently only used for global
    hooks because podman does not persist per-container
    ``--hooks-dir`` across stop/start.  The per-container code path is
    kept for near-future use.

    Args:
        hook_entrypoint: Where to write the nft entrypoint script.
            The reader entrypoint and ``_oci_state.py`` ballast land
            in the same parent directory.
        hooks_dir: Directory for hook JSON descriptors.
    """
    hook_entrypoint.parent.mkdir(parents=True, exist_ok=True)
    hooks_dir.mkdir(parents=True, exist_ok=True)
    _write_role_files(hook_entrypoint.parent, hooks_dir, nft_entrypoint_name=hook_entrypoint.name)

ensure_user_hooks_dir_configured(hooks_dir=None)

Ensure ~/.config/containers/containers.conf lists hooks_dir.

The canonical SSOT for the rootless OCI hooks directory across every terok package: shield calls it at setup time; other installers (e.g. terok-sandbox's per-container supervisor) call it before dropping their own descriptors so they don't have to re-implement the containers.conf patcher. Idempotent.

hooks_dir defaults to namespace_state_dir("shield") / "hooks" — shield's canonical install location under paths.root.

Creates the conf file if absent. Inserts hooks_dir into the existing [engine] section or appends a new section if none exists. Skips silently when hooks_dir is already listed. When a different hooks_dir is configured, appends ours to the list rather than failing — the operator owns containers.conf and may have intentionally pinned other locations.

Pure line-based editing — comments and formatting are preserved.

Source code in src/terok_shield/hooks/install.py
def ensure_user_hooks_dir_configured(hooks_dir: Path | None = None) -> None:
    """Ensure ``~/.config/containers/containers.conf`` lists *hooks_dir*.

    The canonical SSOT for the rootless OCI hooks directory across
    every terok package: shield calls it at ``setup`` time; other
    installers (e.g. terok-sandbox's per-container supervisor) call
    it before dropping their own descriptors so they don't have to
    re-implement the containers.conf patcher.  Idempotent.

    *hooks_dir* defaults to ``namespace_state_dir("shield") / "hooks"``
    — shield's canonical install location under ``paths.root``.

    Creates the conf file if absent.  Inserts ``hooks_dir`` into the
    existing ``[engine]`` section or appends a new section if none
    exists.  Skips silently when *hooks_dir* is already listed.  When
    a different ``hooks_dir`` is configured, appends ours to the list
    rather than failing — the operator owns containers.conf and may
    have intentionally pinned other locations.

    Pure line-based editing — comments and formatting are preserved.
    """
    if hooks_dir is None:
        hooks_dir = _default_target_dir()
    conf_path = _user_containers_conf()
    hooks_str = str(hooks_dir)
    hooks_line = f'hooks_dir = ["{hooks_str}"]'

    if not conf_path.is_file():
        conf_path.parent.mkdir(parents=True, exist_ok=True)
        conf_path.write_text(f"[engine]\n{hooks_line}\n")
        return

    existing = _parse_hooks_dir_from_conf(conf_path)
    if not existing:
        _insert_hooks_line(conf_path, hooks_line)
        return

    if hooks_str in existing or str(hooks_dir.expanduser()) in existing:
        return  # already configured
    _append_to_hooks_dir(conf_path, hooks_str)