InfraCanvas #12: LXD/Incus terminal exec and lifecycle actions
InfraCanvas draws a live map of your infrastructure (containers, pods, volumes and networks) in the browser. A small agent on each machine reports what it finds, and you get a terminal and one-click actions for what is on the canvas, with no VPN and no inbound ports.
My second open source contribution, pull request #12, was merged on September 14. It closes issue #9, a good first issue for adding terminal and action support to LXC/LXD/Incus containers.
The problem
InfraCanvas already discovered LXD and Incus containers and drew them on the canvas with their name, status, memory and network. But the terminal and the start, stop and restart actions only worked for Docker containers and Kubernetes pods. Clicking an LXD container gave you information and nothing to act on, and the README listed it as a known gap.
What I built
An LXD executor in the agent
The new pkg/actions/lxd.go talks to the LXD (or Incus) REST API over its local Unix socket. It finds the socket and builds its HTTP client through the existing discovery code, so discovery and actions always agree on which daemon they are talking to.
- Lifecycle actions: start, stop and restart send
PUT /1.0/instances/{name}/statewith a 30-second timeout, then wait on the operation LXD returns, so the result reflects what actually happened. Stop and restart are forced, so a container that ignores the shutdown signal can’t hang the action. - Interactive shells: a terminal session calls
POST /1.0/instances/{name}/execwithinteractiveandwait-for-websocketset, then attaches to the operation’s data websocket with the secret LXD hands back. - Resizing: LXD opens a second, control websocket for each session. The executor keeps it per session and sends
window-resizemessages on it, so the shell reflows when you resize the terminal panel. Closing a session closes its control channel too. - Reusing the existing terminal code: the data websocket is wrapped in a
net.Conn, so the agent’s existing session handling and output coalescing treat an LXD shell exactly like a Docker one.
Wiring it into the agent
ActionExecutorcreates the LXD executor at startup and routes any action withlayer: "lxd"to it.LXDExec,LXDExecResizeandLXDCloseExecgive the websocket agent the same three hooks it already had for Docker.handleExecStartinpkg/agent/ws_agent.gogained an LXD path that reuses the existing session lifecycle instead of duplicating it.- The frontend’s action names
lxd_start_container,lxd_stop_containerandlxd_restart_containernow map to backend actions.
Runtime-aware routing in the UI
- Container actions and the terminal button now route by
node.metadata.runtime: Docker containers keep their existing paths, andlxdorincuscontainers use the new ones. - The terminal panel and websocket manager accept
lxdas a layer. - The Docker-only “update image” action stays scoped to Docker containers.
- Discovery exposes its LXD client and socket path so the executor can reuse them, and the README’s LXD section now lists terminal and lifecycle support.
Testing
- New unit tests cover parsing operation IDs, extracting the data and control websocket secrets from LXD’s exec response, and mapping the new frontend action names.
make test,make lintandnpm run buildpass.
Review and merge
I opened the pull request late on September 13, and it was approved and merged the next afternoon. The maintainer built and tested it independently, and pointed out that it follows the existing Docker and Kubernetes exec pattern and comes with real test coverage.
The change came to 611 lines added and 22 removed across 13 files: Go in the agent and actions packages, and TypeScript in the canvas and terminal components.
If you run LXD or Incus, give InfraCanvas a try. The project welcomes contributions.