Commit Graph

9 Commits

Author SHA1 Message Date
Anso bb98cba1f2 fix(compose-doctor): recognize Docker socket proxy topologies (#1791)
* fix(compose-doctor): recognize Docker socket proxy topologies

Classify dedicated socket proxies separately from direct docker.sock mounts so Doctor no longer recommends adopting a proxy the stack already uses. Closes #1790.

* fix(compose-doctor): widen socket proxy detection and flag writable proxy sockets

Close the remaining gaps in socket proxy topology handling: a service that
points at a proxy through a tcp:// endpoint on its command line (how Traefik
and friends do it) now gets the client note, proxy API group flags are read
for any truthy value rather than a literal 1, and underscore or dot separated
proxy names are recognized.

Two cases that previously slipped through now surface: a service classified
as a proxy purely by name or image but mounting docker.sock read-write is
reported as high, and a proxy on the implicit default network or on a network
the rendered model does not describe counts as non-internal. A direct socket
mount alongside an existing proxy now names that proxy in its fix.

* fix(compose-doctor): require corroboration before a service name classifies a socket proxy

A service name is free text the author controls, so on its own it could move a
writable docker.sock mount out of the high direct-mount finding. A known proxy
image is an artifact identity and still stands alone; a proxy-shaped name now
counts only alongside an observable fact, a read-only socket or a scoped API
group key.

* fix(compose-doctor): tighten socket-proxy detection against live upstream behavior

Require proxy API flags to be exactly 1 (matching tecnativa and linuxserver
images), count only those enabled flags when classifying a proxy, extract
tcp hosts from DOCKER_HOST instead of treating key presence as a proxy client,
and correlate each client note to one proxy instance by both name and shared
network. Soften the published-port finding so it claims reachability rather
than Docker API exposure for unrelated ports.
2026-08-08 01:32:26 -04:00
Anso 3c934066f3 fix(networking): ignore verified Mesh attachments in drift (#1729) 2026-07-29 12:55:30 -04:00
Anso 78475d96ef fix(compose-doctor): resolve effective healthcheck coverage (#1713)
* fix(compose-doctor): resolve effective healthcheck coverage

Compose Doctor now classifies healthcheck coverage from the Compose model, running containers, and local images so image-provided HEALTHCHECKs are not false positives. Update Guard shares the same presence helper so test NONE is not treated as active.

* fix(compose-doctor): fix healthcheck project label and empty compose HC

Use the Compose project name for runtime container listing so stacks whose name: differs from the directory still get runtime evidence. Treat empty or timing-only healthcheck objects as absent rather than active.

* fix(compose-doctor): treat inherited healthcheck as All Clear note

Inherited image healthchecks no longer block All Clear; they surface under a notes section and cannot be acknowledged.
2026-07-28 14:26:00 -04:00
Anso 79914fe750 fix: recognize clean one-shot completions in health gate and drift (#1691)
* fix: recognize clean one-shot completions in health gate and drift

Treat exit 0 with restart policy no/absent as successful completion so
init and migration jobs no longer fail post-update observation or show as
service-missing, while long-running restart policies still fail closed.

* fix: ignore residual health on clean one-shots and honor deploy.restart_policy

Completed exit-0 jobs with no-restart intent no longer fail the health gate on leftover starting/unhealthy state, and Drift treats deploy.restart_policy with Compose precedence so any/on-failure services are not mistaken for one-shots.

* fix: require explicit Compose restart no for one-shot recognition

Docker inspect reports restart no for both intentional jobs and bare services that omit restart, so Health Gate and Drift now require declared restart:""no"" (or deploy.restart_policy condition none) and load Compose intent once per gate.
2026-07-24 09:41:21 -04:00
Anso 4123793e68 fix(drift): resolve explicit network names that equal compose keys (#1588)
Compare runtimeResourceName against the project-prefixed default instead
of the compose key so networks like tailscale: { name: tailscale } are
not mis-resolved as network_tailscale in Drift, Fleet summary, and preflight.

Fixes #1581
2026-07-07 14:41:18 -04:00
Anso 04f3a55e5a fix(drift): resolve image tags via effective compose model (#1574)
Drift compared raw compose YAML to runtime, so ${VAR:-default} image
expressions false-positive as image-mismatch. Source the declared side
from docker compose config instead, matching deploy-time resolution.

Fixes #1572
2026-07-06 01:47:26 -04:00
Anso b611f41872 fix(drift): stop flagging declared external networks as drift (#1402)
The spatial drift engine reported a service attached to a declared
external network (top-level networks: { foo: { external: true } }) as
"attached to a network not declared in compose", marking the stack
permanently drifted. runtimeResourceName project-prefixed every declared
network to <project>_<key>, but Docker never prefixes an external
network: it references a pre-existing network by its real name. The
phantom <project>_<key> never matched the runtime name, so the
attachment fell through to a foreign-network finding.

Resolve external networks to their real name (the key, or a name:
override) without the project prefix, so the raw declared adapter agrees
with the rendered model the Network Inspector already used. Add unit,
adapter, and engine-level regression tests, and extend the adapter
equivalence test to cover an external network with no name override.
2026-06-21 17:58:00 -04:00
Anso 77f1611971 feat: Compose Network Inspector and exposure intent guard (#1360)
* feat: add Compose Network Inspector facts engine

Render a stack's authored effective model and pair it with the live
Docker snapshot to derive per-stack networking facts: project networks
with external and internal flags, service-to-network membership and
aliases, published ports with host-binding scope, network_mode, and
extra_hosts, plus runtime drift (runtime-only attachments, foreign
networks, and declared-but-unused or missing networks).

Extend the effective-model parser with service network membership,
extra_hosts, and label keys (key names only, never values), and add a
key-space normalized network model with adapters from both the rendered
model and the raw declared compose so the Inspector and drift share one
comparison. Expose GET /api/stacks/:stackName/networking: advisory and
read-only, it renders the authored model only and never returns or logs
raw stderr, env values, or label values.

* feat: store and edit per-stack and per-service exposure intent

Add a stack_exposure_intent table (intent values constrained by a CHECK,
unique per node, stack, and service) with DAO methods to read, upsert,
clear one row, and clear all rows for a stack. The classification is
stored independently of the generated networking facts so a later
mismatch stays detectable; service rows are kept separately from the
stack-level row (service '').

Expose GET and PUT /api/stacks/:stackName/exposure: GET requires read
access, PUT requires edit access and validates the intent against the
allowed set. Sending intent null clears that row, returning the scope to
unset so a service inherits the stack intent again. Intent rows are
cleared when the stack is deleted and when the owning node is removed,
so a later same-named stack never picks up stale classification.

* feat: add exposure-aware Compose Doctor findings

Feed the Compose Doctor's effective-model context with the stored
exposure intent (resolved into a stack-level value plus per-service
overrides) and the dossier's documented access-URL ports, read fail-soft
so a metadata read error skips these checks rather than failing the
preflight. Add five deterministic findings on top of that context:

- a service classified internal or same-node that publishes a host port
  (same-node tolerates a loopback bind),
- a sensitive database or admin image published on all interfaces,
- a port-publishing stack with no exposure intent set,
- a published port not reflected in the documented access URLs,
- reverse-proxy labels with no documented URL or reverse-proxy intent.

The rules stay pure functions over the preflight context; the registry
completeness test pins the new rule set.

* feat: detect compose network drift in the drift ledger

Extend the spatial drift engine with two network-level findings: a
running container attached to a stack-owned or foreign network that
compose does not declare (one finding per service), and a declared
network that no running service uses or that is absent from the runtime
(one stack-level finding, every network named by its resolved runtime
name). The comparison reuses the same helper the Network Inspector uses,
so the two surfaces never disagree.

Network drift runs only when the stack has running containers and the
runtime is reachable, preserving the existing missing-runtime,
parse-error, and unreachable behavior. The findings persist through the
existing drift ledger and surface on the Drift tab, which now labels the
two new kinds.

* feat: link a Docker network back to its owning stack

Add a cross-component open-stack event and make the owning-stack badge on
a managed network in Resources a link: clicking it loads that stack on
its node and opens the editor, reusing the existing fleet navigation. A
latest-ref keeps the window listener current without re-subscribing each
render. Image and volume badges are unchanged; only a managed network
opts in via the new optional handler.

* feat: add the Networking tab to the stack detail panel

Add a capability-gated Networking tab that reads the per-stack networking
facts and exposure intent. It shows the project networks (with external,
internal, and created-by-stack flags), per-service network membership and
aliases, published ports with their host-binding scope, network_mode and
extra_hosts, and runtime drift, degrading to the declared model when the
runtime is unavailable. Users can classify the stack and each service
(internal, LAN, reverse proxy, public, and so on) or clear a row to
inherit; the controls are read-only when the user cannot edit, and a
broken exposure response never tears down the facts view.

A new compose-networking capability is added to both registries so older
nodes hide the tab, and the tab cross-links to the Doctor for the deploy
and security findings.

* docs: document the Compose Networking tab

Add a feature page covering the Networking tab: the network facts,
published ports and host bindings, the exposure-intent classification
and inheritance, the exposure-aware Doctor findings, runtime drift, and
a troubleshooting section. Register it in the docs navigation next to
Compose Doctor.

* feat: add a redacted network summary to the Stack Dossier export

Append a network exposure section to the dossier Markdown: the stack and
per-service exposure intents, the networks with their external and
internal flags, and each service's published ports with their binding
scope. It carries only names, intents, port numbers, and scope, never an
env value or a label value.

The summary is fetched only when the user exports (copy or download), so
opening the panel costs nothing, and it degrades to omitting the section
when the data is unavailable. The whole-fleet dossier export collects the
same summary per stack, rethrowing the unauthorized sentinel like the
sibling loaders.

* feat: add a Fleet networking filter for exposure and drift

Add a per-node networking summary that classifies a node's stacks as
exposed (a host port published beyond loopback), unknown-exposure
(publishes ports with no exposure intent set), or network-drift. It
reads each stack's compose with the light dependency parser and one
Docker snapshot, so it stays cheap across a node's full stack set, and
it skips drift when the runtime is unreachable rather than inventing it.

Serve it node-locally at GET /api/networking/summary, and aggregate it
fleet-wide at GET /api/fleet/networking-summary: the hub computes its own
summary in-process and reaches each remote through its node-local route,
degrading an unreachable or older node to a skip. Because the aggregate
lives under the proxy-exempt /api/fleet prefix it is never wrongly
proxied. The Fleet overview gains a networking filter chip backed by that
aggregate, fetched fail-soft and detached so it never gates the grid.

* fix: spin the Networking refresh button while it reloads

The refresh button silently refetched the same data, so a click gave no
feedback. Track a refreshing state and spin the icon while the load is in
flight, disabling the button, matching the Compose Doctor preflight
button.

* fix: apply effective per-service exposure intent to unclassified checks

The "unclassified exposure" decisions only consulted the stack-level intent
row, so a service classified directly (with no stack row) was still reported
as unclassified, and a service explicitly marked unknown over a classified
stack was missed.

Both the exposure-unclassified preflight rule and the networking summary's
unknown-exposure bucket now resolve the effective intent per publishing
service (service row overrides stack row), matching the precedence already
used by the exposure-internal-published rule.

* fix: resolve drift network names via the compose top-level name

When a compose file sets a top-level name:, Docker prefixes resource names
with that project name instead of the stack directory. The light dependency
parser dropped name:, so network-drift normalization compared runtime
networks against directory-prefixed names and reported false
network-undeclared / network-missing findings.

Carry the parsed project name through DeclaredCompose and use it when
normalizing declared networks for drift, while still filtering containers by
the stack directory.
2026-06-12 02:15:11 -04:00
Anso 421177e4a6 feat(stacks): add compose-vs-runtime drift detection (#1329)
* feat(stacks): add compose-vs-runtime drift engine

Add a read-only engine that compares a stack's on-disk compose model
against the live Docker runtime and reports where the two diverge.

GET /api/stacks/:stackName/drift returns a per-stack report with a
status (in-sync, drifted, missing-runtime, unreachable) and typed,
service-scoped findings: a declared service with no running container,
a running container not declared in compose, an image mismatch, and a
published-port mismatch. The report is computed at request time with no
persistence and is available on every tier.

The check reuses the existing compose parser and Docker dependency
snapshot; the compose parser now also captures each service's declared
image. Boundaries fail closed: an unreadable compose file reports
drifted and an unreachable Docker daemon reports unreachable, never a
false in-sync.

* fix(stacks): keep drift hasContainers accurate on compose parse error

assembleStackDrift hardcoded hasContainers: false on the parse-error
path, contradicting the field's contract when the runtime actually has
running containers. Compute it once from the container set and reuse it
across all return paths.

Also add a route test that exercises the successful 200 path for an
existing stack on the Community tier (stubbing only the Docker boundary),
so a tier gate or handler regression after the existence check is caught.

* feat(stacks): add a drift detection tab to the stack view

Surface the compose-vs-runtime drift report on the per-stack Anatomy
panel as a read-only Drift tab. It shows the stack's status (in sync,
drifted, not running, unreachable) and, when drifted, the specific
service-scoped reasons with the declared and running values side by
side. A re-check action reruns the comparison.

The tab lives in the shared anatomy panel, so it appears on both the
desktop stack view and the mobile stack detail. Available on every tier.

* fix(stacks): sanitize the logged error in the drift report builder

The compose-read and Docker-snapshot catch blocks logged the raw error
object, whose message can embed the user-controlled stack path (e.g. an
ENOENT path). Log the error through the existing sanitizer so a crafted
stack name cannot forge log lines, matching the pattern used elsewhere
in the stacks router.
2026-06-07 15:48:04 -04:00