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).

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.

check_setup(*, live=False)

Check Shield's receipt and hooks, optionally probing launch prerequisites.

Source code in src/terok_shield/hooks/install.py
def check_setup(self, *, live: bool = False) -> tuple[SetupCheck, ...]:
    """Check Shield's receipt and hooks, optionally probing launch prerequisites."""
    checks = [self._receipt().check(), *self._check_artifacts()]
    if live:
        checks.extend(self._check_tools())
    return tuple(checks)

install()

Install global standalone hooks after preflight; certify only verified work.

Source code in src/terok_shield/hooks/install.py
@setup_lock()
def install(self) -> None:
    """Install global standalone hooks after preflight; certify only verified work."""
    require_no_downgrade(self.check_setup())
    require_setup(self._check_tools())
    receipt = self._receipt()
    receipt.clear()
    install_reader_resource()
    self.target_dir.mkdir(parents=True, exist_ok=True)
    _write_role_files(self.target_dir)
    ensure_user_hooks_dir_configured(self.target_dir)
    require_setup(self._check_artifacts())
    receipt.write()

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
@setup_lock()
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.
    """
    self._receipt().clear()
    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.

Use check_setup for receipt, interpreter, and artifact validation.

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

    Use ``check_setup`` for receipt, interpreter, and artifact validation.
    """
    return (self.target_dir / _nft_hook_json("createRuntime")).is_file()

user_hooks_dir_configured(hooks_dir)

Whether the user's containers.conf registers this package-owned hook directory.

Source code in src/terok_shield/hooks/install.py
def user_hooks_dir_configured(hooks_dir: Path) -> bool:
    """Whether the user's containers.conf registers this package-owned hook directory."""
    return str(hooks_dir.expanduser()) in {
        str(Path(entry).expanduser())
        for entry in _parse_hooks_dir_from_conf(_user_containers_conf())
    }

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
@setup_lock()
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)