Skip to content

feat(cli): hosts describe reads the container live - #166

Merged
paolomainardi merged 4 commits into
mainfrom
feat/164-hosts-describe-live
Sep 1, 2026
Merged

feat(cli): hosts describe reads the container live#166
paolomainardi merged 4 commits into
mainfrom
feat/164-hosts-describe-live

Conversation

@paolomainardi

@paolomainardi paolomainardi commented Sep 1, 2026

Copy link
Copy Markdown
Member

User description

🤖 This was written by an AI agent on behalf of @paolomainardi.

Closes #164. The layout is the one settled in the issue.

What changes

hosts describe <hostname> reads the container from Docker and the route from the proxy instead of repeating four state-file fields. hosts list is unchanged and still reads the state file alone, with no docker inspect per row.

Real output on p620:

$ spark-http-proxy hosts describe sparkdock.githuman.sparkfabrik.loc
sparkdock.githuman.sparkfabrik.loc
  container      githuman-sparkdock
  image          node:lts
  status         running, up 29 hours
  directory      ~/webapps/sparkfabrik/agents/tailcat-use-cases/sparkdock
  routed by      VIRTUAL_HOST, port 3847
  backend        http://172.17.0.3:3847
  network        bridge
  reachable      200
  mounts         ~/webapps/sparkfabrik/agents/tailcat-use-cases/sparkdock -> same path (rw)
                 githuman-npm-cache -> /cache/npm (rw)
                 githuman-data -> /data/githuman (rw)
  command        docker-entrypoint.sh bash -c git config --global --add safe.directory '...' && npx githuman@0.9.0 serve --host 0.0.0.0 --no-open --no-https --auth '<redacted>'

How each field is read

  • image, status, network, command, mounts from two docker inspect --format calls. Uptime comes from docker ps --format '{{.Status}}', so there is no date arithmetic and no BSD versus GNU split. Mount fields are separated by \x1f in the template because a bind has no name and read collapses adjacent tabs.
  • routed by, port, backend from the Traefik API: the router whose rule names this exact host, then its service's server URL. That is what Traefik actually routes to, and it is the same path for VIRTUAL_HOST and for native traefik.* labels, so the label-routed case needs no separate logic.
  • reachable is the HTTP status of a request through the proxy's published port with the Host header, with a 5s timeout. On Docker Desktop the container address is inside the VM and would report no answer from the host.
  • redaction by flag name (--auth, --token, --password, --secret, --api-key and similar, bare, quoted or --flag=value), by assignment name (*_TOKEN=, *_SECRET=, ...) and in URL userinfo. Never by the shape of a value, and the README says a value passed some other way is printed as is.

Cases

  • A container routed by native labels reports traefik.* labels, port N.
  • A record whose container Docker no longer has prints the record's fields, says the container was not found, and exits 1.
  • With the proxy stopped, backend and reachable say so instead of failing.
  • Remote hosts are unchanged and no peer is probed.

Verification

  • test/test.sh: the hosts suite gains stubs for docker and curl and asserts the live fields, the same-path bind and named volume rendering, the redaction of four planted secrets, the label-routed form, and the gone-container failure. 19/19 locally, run as the suite does (sourcing bin/lib/hosts.sh alone).
  • shellcheck -x on the CLI and libraries: the same four pre-existing findings as main.
  • Run by hand on p620 against a docker run container on bridge and a compose container on its own network.

PR Type

Enhancement, Tests, Documentation


Description

  • Inspect live containers for detailed host metadata

  • Resolve Traefik backends and probe proxy reachability

  • Redact command secrets and handle missing containers

  • Document behavior and expand CLI coverage


Diagram Walkthrough

flowchart LR
  command["hosts describe hostname"]
  state["State-file host record"]
  docker["Live Docker metadata"]
  traefik["Traefik routing API"]
  probe["Proxy reachability probe"]
  output["Detailed redacted host description"]
  command -- "finds hostname" --> state
  state -- "identifies container" --> docker
  state -- "resolves route" --> traefik
  state -- "sets Host header" --> probe
  docker -- "provides runtime details" --> output
  traefik -- "provides backend and port" --> output
  probe -- "provides HTTP status" --> output
Loading

File Walkthrough

Relevant files
Enhancement
hosts.sh
Read host container details from live services                     

bin/lib/hosts.sh

  • Reads image, status, networks, mounts, and command from Docker.
  • Resolves backend routing through the Traefik API.
  • Probes reachability through the proxy with a timeout.
  • Redacts secrets and reports missing containers safely.
+126/-7 
Tests
test.sh
Test live host description and secret redaction                   

test/test.sh

  • Adds Docker and curl stubs for live inspection.
  • Verifies runtime metadata, routing, mounts, and reachability.
  • Tests command redaction across supported secret formats.
  • Covers native labels and missing-container failures.
+88/-4   
Documentation
CHANGELOG.md
Record live hosts describe functionality                                 

CHANGELOG.md

  • Records expanded live hosts describe container reporting.
  • Notes routing, reachability, mounts, and command redaction.
+1/-0     
README.md
Document live container host descriptions                               

README.md

  • Documents the detailed live container output.
  • Explains Traefik routing and proxy-based reachability checks.
  • Describes command secret redaction boundaries.
  • Clarifies remote and missing-container behavior.
+33/-3   


Assisted-by: pr-agent/gpt-5.6-sol

hosts describe repeated four fields from the state file. It now reads
the container from Docker and the route from the proxy: image, status
with uptime from docker ps, the port and backend Traefik routes to from
its API (so VIRTUAL_HOST and traefik.* labels are treated the same),
the container's networks, whether a request through the proxy with that
Host header is answered, its mounts, and its command with secrets
redacted by flag name, by assignment name and in URL userinfo, never by
the shape of a value.

The reachability probe goes through the proxy's published port rather
than to the container address, which on Docker Desktop is inside the
VM. A record whose container is gone is reported as such and the
command fails. hosts list is unchanged and still reads the state file
alone.

Closes: #164
Assisted-by: claude-code/claude-fable-5-1
@sparkfabrik-ai-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

164 - Partially compliant

Compliant requirements:

  • Read live container details from Docker.
  • Show the requested live container, routing, backend, network, reachability, mount, and command fields.
  • Keep hosts list state-file based without per-row inspection.
  • Preserve remote-host behavior without peer probing.
  • Apply a timeout to proxy and Traefik API requests.
  • Report and fail for missing containers.
  • Support native traefik.* routing labels.
  • Use Docker status rendering and distinguish bind mounts from named volumes.
  • Probe through the proxy with a Host header.
  • Avoid claiming that secret redaction is complete.

Non-compliant requirements:

  • Reliably redact quoted secret-bearing assignment values containing spaces.
  • Recognize every valid exact-host Traefik rule representation.

Requires further human verification:

  • Verify live behavior against actual Traefik API responses and Docker Desktop networking.
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 Security concerns

Sensitive information exposure:
Quoted assignment values containing spaces are only partially redacted. For example, API_TOKEN='live secret' leaves secret' visible in the rendered command.

⚡ Recommended focus areas for review

Secret Leak

The assignment redaction stops at the first space without handling quoted values. A command such as API_TOKEN='live secret' becomes API_TOKEN=<redacted> secret', exposing part of the token in output intended for issue reports. Add quoted-assignment substitutions before the unquoted assignment rule.

-e 's#([A-Za-z0-9_]*(TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|APIKEY|ACCESS_KEY|CREDENTIALS)[A-Za-z0-9_]*=)[^ ]+#\1<redacted>#g' \
Route Miss

Router matching only recognizes backtick-delimited rules. Traefik also accepts escaped double-quoted host arguments, which appear in API JSON as rules such as Host(\"example.test\"); those valid native-label routes are missed, causing backend to report no route and omitting the routed port.

router="$(curl -s --max-time 5 "http://127.0.0.1:${api_port}/api/http/routers?search=${hostname}" 2>/dev/null |
  sed 's/},{/}\n{/g' | grep -F "Host(\`${hostname}\`)" | head -n 1)"

Assisted-by: pr-agent/gpt-5.6-sol

@sparkfabrik-ai-bot

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Prevent partial secret disclosure

Do not redact after .Args has been flattened, because argument boundaries are lost
and a value containing spaces can be only partially redacted. Parse and redact the
command as structured arguments before rendering it; until that is implemented, omit
it to prevent secret disclosure.

bin/lib/hosts.sh [183]

-command="$(sed -n '4,$p' <<<"${info}" | hosts_redact_command)"
+command="<omitted: argument-safe redaction unavailable>"
Suggestion importance[1-10]: 9

__

Why: Flattening .Args destroys argument boundaries, so secrets containing spaces can be only partially redacted and disclosed. Omitting command is a safe mitigation until argument-aware redaction is implemented.

High
Possible issue
Avoid duplicate provider qualification

Traefik commonly returns an already-qualified service such as app@docker; appending
@${provider} then requests app@docker@docker and loses the backend. Qualify service
only when it does not already contain a provider suffix.

bin/lib/hosts.sh [160-162]

 [[ -z "${service}" || -z "${provider}" ]] && return 0
-curl -s --max-time 5 "http://127.0.0.1:${api_port}/api/http/services/${service}@${provider}" 2>/dev/null |
+[[ "${service}" == *@* ]] || service="${service}@${provider}"
+curl -s --max-time 5 "http://127.0.0.1:${api_port}/api/http/services/${service}" 2>/dev/null |
   grep -o '"url":"[^"]*"' | head -n 1 | cut -d'"' -f4
Suggestion importance[1-10]: 7

__

Why: Traefik can return an already-qualified service, making the current URL contain a duplicated provider suffix. The proposed conditional qualification correctly supports both qualified and unqualified service names.

Medium
General
Parse backend ports correctly

The current string slicing reports values such as //container when the backend URL
has no explicit port and misparses IPv6 URLs. Extract only a terminal numeric port
and use the scheme's default otherwise.

bin/lib/hosts.sh [194-195]

-port="${backend##*:}"
-port="${port%%/*}"
+port=""
+if [[ "${backend}" =~ :([0-9]+)(/|$) ]]; then
+  port="${BASH_REMATCH[1]}"
+elif [[ "${backend}" == http://* ]]; then
+  port="80"
+elif [[ "${backend}" == https://* ]]; then
+  port="443"
+fi
Suggestion importance[1-10]: 4

__

Why: The existing slicing mishandles some URLs, particularly IPv6 backends without explicit ports, and using scheme defaults is reasonable. However, the proposed regex is not limited to the URL authority and could mistake a path segment such as :123/ for the backend port.

Low

Assisted-by: pr-agent/gpt-5.6-sol

…, parse the port from the authority

A quoted assignment such as API_TOKEN='live secret' was redacted up to
the first space only, leaving part of the value visible. Quoted forms
now get their own substitutions, as the flag forms already had. A rule
written as Host("x") reaches the API with escaped double quotes and was
not matched, so a label-routed container could show no backend. An
already provider-qualified service name is no longer qualified twice.
The routed port is parsed from the URL authority, so a backend without
an explicit port yields no port rather than a fragment of the host.

Refs: #164
Assisted-by: claude-code/claude-fable-5-1
Refs: #164
Assisted-by: claude-code/claude-fable-5-1
@paolomainardi
paolomainardi merged commit d7947f4 into main Sep 1, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hosts describe should read the container, not the state file

1 participant