# `Tank`
[🔗](https://github.com/oshlabs/tank/blob/v0.2.0/lib/tank.ex#L1)

Tank — an opinionated, declarative container orchestrator built on Linx.

You describe the pods that should run as Elixir data; Tank persists that
desired state in Khepri and a level-triggered loop converges the machine to
it. This module is the **runtime write API** over the desired state:

    Tank.apply(%{
      name: "web",
      containers: [%{name: "app", image: "nginx:1.27"}]
    })

    Tank.list()        #=> [%Tank.Pod{name: "web", …}]
    Tank.delete("web")

`apply/1` accepts a `%Tank.Pod{}` or a plain spec map (validated via
`Tank.Pod.new/1`); it writes to `[:tank, :pods, name]` in the store. You never
imperatively start a container — you state intent and the reconciler converges.

## Architecture

  * `Tank.Pod` and friends — the typed desired-state model.
  * `Tank.Store` — the Khepri seam (the source of truth) + an ETS projection.
  * `Tank.Runtime` — the per-container actuator (`Linx.Process` + `Rtnl`),
    the M2 proof of concept that M4 grows into the pod actuator.

Tank is a separate mix app with a *path* dependency on Linx, so it reaches
**only Linx's public API**; a gap in the primitives surfaces here, early.

## Bootstrap vs. runtime

Khepri is the source of truth. `config/runtime.exs` only *seeds* pods
create-if-absent on a fresh store, so the boot seed never clobbers state
changed at runtime via `apply/1` / `delete/1`.

# `spec`

```elixir
@type spec() :: Tank.Pod.t() | map() | keyword()
```

# `apply`

```elixir
@spec apply(spec()) :: :ok | {:error, term()}
```

Declare a pod's desired state — create it or replace it. Accepts a
`%Tank.Pod{}` or a spec map/keyword list (validated via `Tank.Pod.new/1`).

# `attach`

```elixir
@spec attach(String.t()) ::
  {:ok, {:exited, non_neg_integer()} | {:signaled, pos_integer()} | :detached}
  | {:error, term()}
```

Attach to a `tty: true` pod's main process — `docker attach`.

Where `exec/3` runs a *second* process inside the pod, `attach/1` takes over
the pod's **main** process's terminal: the container *is* the interactive
program (declare its container with `tty: true`). Because ending that program
stops the container, leave without killing it by pressing the detach sequence
— `Ctrl-P` `Ctrl-Q` — which returns `{:ok, :detached}` with the pod still
running, ready to re-attach.

    Tank.apply(%{
      name: "console",
      containers: [%{name: "sh", image: "debian:13",
                     command: ["/bin/bash"], tty: true}]
    })

    Tank.attach("console")   #=> your terminal becomes the pod's bash

Returns the session's terminal result — `{:ok, {:exited, code}}` /
`{:ok, {:signaled, signum}}` (the program ended — the pod stops and the
reconciler applies its restart policy), `{:ok, :detached}` (you detached), or
`{:error, reason}` (`:not_running` if the pod has no live workload,
`:not_a_tty` if its container wasn't declared `tty: true`).

Like `exec/3`, this runs in and blocks the caller's process and routes the PTY
through it — call it straight from `iex`.

# `delete`

```elixir
@spec delete(String.t() | Tank.Pod.t()) :: :ok | {:error, term()}
```

Remove a pod's desired state, by name or by `%Tank.Pod{}`.

# `exec`

```elixir
@spec exec(String.t(), [String.t()], keyword()) ::
  {:ok, {:exited, non_neg_integer()} | {:signaled, pos_integer()} | :detached}
  | {:error, term()}
```

Run an interactive command *inside* a running pod — `docker exec -it`.

Resolves the pod's running workload, starts a **second** process that enters
the container's namespaces (mount → its rootfs, pid → its procs, net/uts/ipc)
with a PTY, and hands the caller's terminal to it. Typing `exit` ends only
this exec session; the pod's main process keeps running. Exec again, or run
several at once.

    Tank.exec("web", ["/bin/bash"])
    Tank.exec("web", ["/bin/sh", "-c", "ps aux"], cwd: "/tmp")

`argv` is the command to run (its first element is the program). `opts`:

  * `:cwd` — working directory inside the container. Defaults to the
    container's `working_dir` (the image `WorkingDir`).
  * `:env` — extra environment as `["KEY=VAL", …]`, merged *over* the
    container's own environment. By default the exec session inherits the
    container's resolved env (image `Env` + the spec's), exactly like
    `docker exec` — so `PATH` resolves inside the rootfs — plus a default
    `TERM=xterm` when the container set none, for a usable shell.

Returns the exec's terminal result — `{:ok, {:exited, code}}` /
`{:ok, {:signaled, signum}}` — or `{:error, reason}` (`:not_running` when the
pod has no live workload, or a `Linx.Process` / `Linx.Tty` setup error).

> #### Runs in the caller's process {: .info}
>
> `exec/3` blocks the calling process for the life of the session and routes
> the PTY through it, so call it straight from iex (or a process that owns a
> terminal). It is deliberately *not* a cast into another process — the byte
> pump must live where the terminal is.

# `get`

```elixir
@spec get(String.t()) :: {:ok, Tank.Pod.t()} | {:error, :not_found}
```

Fetch one declared pod by name.

# `list`

```elixir
@spec list() :: [Tank.Pod.t()]
```

Every declared pod (a fast read through the store's projection).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
