# `Linx.Netlink.Rtnl.Reconcile`
[🔗](https://github.com/oshlabs/linx/blob/v0.2.0/lib/linx/netlink/rtnl/reconcile.ex#L1)

Single-shot declarative reconciliation for rtnetlink — observe the kernel,
diff against a desired state, and apply the minimal change, in one
caller-driven pass scoped to the socket's network namespace.

This is the mechanism half of declarative networking: it holds no long-lived
state and owns no process. The cadence, persistence, and supervision are the
consumer's (see the reconcile design notes). It composes
`Linx.Netlink.Rtnl.Diff` (the per-resource diffs) and the `Route`/`Address`
actuation verbs into an ordered apply pass.

## Scope

This pass reconciles **addresses and routes** on interfaces that already
exist in the namespace. Link lifecycle (creating veth/ipvlan, MTU, admin
state) is kind-specific and belongs to the composite/consumer; rules and
neighbours reuse the same `Diff` engine and slot in the same way. Interfaces
are observed (to resolve names to indices) but not created or deleted here.

## Desired state

A map authored by interface **name** (indices are ephemeral, so you never
write them down — they are resolved against a fresh link list each pass):

    %{
      addresses: [
        {"eth0", "10.0.0.2", 24},
        {"eth0", "fc00::1", 64}
      ],
      routes: [
        {"10.50.0.0", 24, "10.0.0.1"},
        {"10.60.0.0", 24, "10.0.0.1", table: 100, metric: 50},
        {:default, "10.0.0.1"}
      ]
    }

Address entries are `{interface, ip, prefixlen}`. Route entries are
`{dst, prefix, gateway}` / `{dst, prefix, gateway, opts}` /
`{:default, gateway}` / `{:default, gateway, opts}`, where `opts` is
`Route`'s `:table`/`:metric` (the `:protocol` is forced to this
reconciler's ownership tag).

## Ownership

Routes are owned by `rtm_protocol`: every route this reconciler installs is
tagged with `:protocol` (default `default_protocol/0`), and the route diff
considers only observed routes carrying that tag — connected routes and
other writers are invisible to it, and never deleted. Addresses have no
ownership field, so they are owned three-way via the `last_applied` set
threaded between passes (foreign addresses that merely appeared are left
alone). See `Linx.Netlink.Rtnl.Diff`.

## Strategy and ordering

Apply order follows the kernel's layering: address creates first (so a
route's gateway is reachable), then route creates/updates, then route
deletes, then address deletes. The pass is **fail-fast** — it stops at the
first error and reports the rest as pending; the next pass re-converges.

## Example

    {:ok, sock} = Rtnl.open()

    desired = %{
      addresses: [{"eth0", "10.0.0.2", 24}],
      routes: [{:default, "10.0.0.1"}]
    }

    {:ok, r} = Reconcile.reconcile(sock, desired)
    r.converged?
    {:ok, r2} = Reconcile.reconcile(sock, desired, r.last_applied)  # idempotent

# `address_spec`

```elixir
@type address_spec() :: {String.t(), binary() | Linx.IP.t(), non_neg_integer()}
```

Address entry: `{interface_name, ip, prefixlen}`.

# `desired`

```elixir
@type desired() :: %{
  optional(:addresses) =&gt; [address_spec()],
  optional(:routes) =&gt; [route_spec()]
}
```

Desired state, authored by interface name.

# `last_applied`

```elixir
@type last_applied() :: %{optional(:addresses) =&gt; MapSet.t()}
```

Reconciler-held ownership, threaded between passes.

# `route_spec`

```elixir
@type route_spec() ::
  {binary() | Linx.IP.t(), non_neg_integer(), binary() | Linx.IP.t()}
  | {binary() | Linx.IP.t(), non_neg_integer(), binary() | Linx.IP.t(),
     keyword()}
  | {:default, binary() | Linx.IP.t()}
  | {:default, binary() | Linx.IP.t(), keyword()}
```

Route entry — see the moduledoc.

# `default_protocol`

```elixir
@spec default_protocol() :: pos_integer()
```

The default route-ownership `rtm_protocol` (76, ASCII 'L').

# `reconcile`

```elixir
@spec reconcile(Linx.Netlink.Socket.t(), desired(), last_applied(), keyword()) ::
  {:ok, Linx.Netlink.Rtnl.Reconcile.Report.t()} | {:error, term()}
```

Runs one reconcile pass against `desired` in the socket's namespace.

Returns `{:ok, %Report{}}` — per-op kernel failures live in the report's
`:failed`, since a partial apply is a normal transient state the next pass
corrects. Returns `{:error, {:normalize, reason}}` if the desired state is
invalid (an unknown interface, an unparseable address, a family mismatch) —
nothing is applied in that case. Returns `{:error, term}` if observing the
kernel fails.

## Options

  * `:protocol` — the route-ownership tag (integer or a `Route` protocol
    atom). Default `default_protocol/0`.

---

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