feat: add Docker label audit across Fleet and Stack views (#1531)

This commit is contained in:
Anso
2026-07-03 18:26:09 -04:00
committed by GitHub
parent 10fb93dcb1
commit 4a350e7a0a
27 changed files with 3099 additions and 1 deletions
+1
View File
@@ -108,6 +108,7 @@
"features/compose-doctor",
"features/compose-networking",
"features/environment-guardrails",
"features/docker-label-audit",
"features/compose-storage",
"features/stack-labels",
"features/sidebar"
+55
View File
@@ -0,0 +1,55 @@
---
title: "Docker Label Audit"
description: "Audit Docker and Compose labels that drive external automation across the fleet and inside each stack."
---
Docker labels are metadata declared on Compose services or attached to running containers. They are different from Sencho Stack Labels used for organizing stacks and Node Labels used for Blueprint placement.
Sencho surfaces two read-only audit views for these labels:
- **Fleet · Docker Labels** for estate-wide visibility across every node
- **Stack · Compose Labels** for per-stack reconciliation between declared Compose labels and labels present on running containers
## Fleet Docker Labels tab
Open **Fleet** and select the **Docker Labels** tab (after **Map**). The panel is titled **Docker label audit** and offers two layouts:
| View | What it shows |
|------|----------------|
| **By container** | Each container with its label count. Expand a row to see every key, value, and provenance badge. |
| **By label** | Each unique `key=value` pair and every container that carries it, with node name chips on multi-node fleets. |
Use the search box to filter by label key, value, container name, stack, or node. When a container belongs to a known stack, **Open stack** jumps to that stack in the editor.
Every label carries a provenance badge so you can tell where it came from:
- **Image** for labels inherited from the container image (for example OCI `org.opencontainers.image.*` metadata)
- **Present at runtime** for other labels set on the running container
- **Docker Compose system label** for keys starting with `com.docker.compose.`
- **Unknown** when a container or its image could not be inspected
The fleet view reads container-level metadata and does not open the compose file, so it cannot tell a Compose-declared label from any other label set on the container: labels you declared in Compose appear here as **Present at runtime**. To see which labels come from the compose file, use the per-stack **Compose Labels** tab below. When a container or its image cannot be inspected, the affected labels show as **Unknown** and the panel names the nodes it could not fully inspect.
In the **By label** layout, open the **Filters** popover next to the search box. The **Defined by** section lists toggle pills for each provenance type present in the data (**Image**, **Runtime**, **System**, and **Unknown** when present). Turn a pill off to hide that source; the button shows a count badge while any filter is active, and **Clear filters** resets the popover. Automation tools such as Watchtower, Diun, and Traefik read these labels, so the audit makes it easy to confirm which containers are opted in or out.
Runtime labels are static until the container is recreated. Changes declared in Compose require save and redeploy before they appear on running containers.
## Stack Compose Labels tab
Inside the stack editor, open the **Compose Labels** tab in the anatomy strip. For each service you see:
- **Declared in Compose** labels from the effective rendered compose model
- **Present at runtime** labels read from each running replica, each with its provenance badge (Compose, Image, or runtime)
- Reconciliation hints: **only in Compose**, **only on running container**, **present in both**, or **value changed** when a key is declared and running but the values differ
Because this tab renders the compose model, it can identify Compose-declared labels accurately. A toolbar at the top combines a **search box** with a **Filters** popover. The search matches label keys and values as well as service and container names. The popover has a **Defined by** section with toggle pills for each provenance present (**Compose File**, **Image**, **Runtime**, **System**) and a **Services** section to show or hide individual service cards when the stack has more than one service. The Filters button shows a count badge for active facet and service filters; **Clear filters** resets both sections. Matching a service or container name reveals that parent's labels even when the text does not match a specific key or value. Reconciliation counts always reflect only the labels currently visible.
When Compose cannot be fully rendered, the panel warns that declared labels may be incomplete but still shows whatever runtime data is available. If a replica cannot be inspected, it is flagged with **Runtime labels unavailable** and the panel notes that provenance may be incomplete.
## Sensitive values
Some label keys look like secrets (for example keys containing `token`, `password`, or `auth`). Their values are redacted by default. Admins can reveal full values with the **Reveal** control, which re-fetches the inventory with elevated read access.
## Editing labels
Compose label editing from these panels is not available yet. To change labels today, edit the compose file directly and redeploy the stack.
+1
View File
@@ -80,6 +80,7 @@ The tab row always shows four tabs: **Anatomy**, **Activity**, **Dossier**, and
| **Dossier** | Yes | Exportable Markdown of the anatomy combined with operator notes. See [Stack Dossier](/features/stack-dossier). |
| **Drift** | Yes | Live comparison of the declared compose against the running containers. See [Stack Drift](/features/stack-drift). |
| **Environment** | When `env-inventory` capability is present | Variable inventory across all env files, with status for each variable. See [Environment Guardrails](/features/environment-guardrails). |
| **Compose Labels** | When `container-label-inventory` capability is present | Declared Compose labels vs runtime container labels per service. See [Docker Label Audit](/features/docker-label-audit). |
| **Networking** | When `compose-networking` capability is present | Port exposure summary per service with intent classification. See [Compose Networking](/features/compose-networking). |
| **Doctor** | When `compose-doctor` capability is present | Preflight check results grouped by severity. The tab gains a red dot for blocker findings and an amber dot for high-risk findings. See [Compose Doctor](/features/compose-doctor). |
| **Storage** | When `compose-storage` capability is present | Mount inventory with portability assessment and snapshot coverage. See [Compose Storage](/features/compose-storage). |
+243
View File
@@ -149,6 +149,148 @@ components:
type: boolean
example: true
LabelSource:
type: string
description: Provenance of a label. `unknown` when a container or image could not be inspected.
enum: [compose, runtime, image, compose-system, unknown]
LabelValue:
type: object
required: [key, value, source]
properties:
key: { type: string }
value:
type: string
description: Redacted to `[redacted]` for secret-like keys unless the caller is an admin and passes `reveal=1`.
source: { $ref: "#/components/schemas/LabelSource" }
redacted: { type: boolean }
LabelIndexContainerRef:
type: object
required: [id, name, stack, service]
properties:
id: { type: string }
name: { type: string }
stack: { type: string, nullable: true }
service: { type: string, nullable: true }
nodeId: { type: integer }
nodeName: { type: string }
LabelIndexRow:
type: object
description: One unique key/value/source and every container carrying it.
required: [key, value, source, containers]
properties:
key: { type: string }
value: { type: string }
source: { $ref: "#/components/schemas/LabelSource" }
redacted: { type: boolean }
containers:
type: array
items: { $ref: "#/components/schemas/LabelIndexContainerRef" }
ContainerLabelRow:
type: object
required: [id, name, stack, service, state, labels]
properties:
id: { type: string }
name: { type: string }
stack: { type: string, nullable: true }
service: { type: string, nullable: true }
state: { type: string }
labels:
type: array
items: { $ref: "#/components/schemas/LabelValue" }
StackLabelReplica:
type: object
required: [id, name, state, runtimeLabels, onlyInCompose, onlyOnContainer, inBoth, changed]
properties:
id: { type: string }
name: { type: string }
state: { type: string }
runtimeLabels:
type: array
items: { $ref: "#/components/schemas/LabelValue" }
onlyInCompose: { type: array, items: { type: string } }
onlyOnContainer: { type: array, items: { type: string } }
inBoth: { type: array, items: { type: string } }
changed:
type: array
items: { type: string }
description: Keys declared in Compose and present at runtime but with a different value.
inspectFailed:
type: boolean
description: Runtime labels could not be read for this replica; reconciliation was skipped.
StackServiceLabelRow:
type: object
required: [service, declaredLabels, replicas]
properties:
service: { type: string }
declaredLabels:
type: array
items: { $ref: "#/components/schemas/LabelValue" }
replicas:
type: array
items: { $ref: "#/components/schemas/StackLabelReplica" }
StackLabelInventory:
type: object
required: [stackName, renderable, services, partial, generatedAt]
properties:
stackName: { type: string }
renderable:
type: boolean
description: False when the Compose model could not be rendered; declared provenance is then unknown.
partial:
type: boolean
description: A replica or its image could not be fully inspected.
generatedAt: { type: integer }
services:
type: array
items: { $ref: "#/components/schemas/StackServiceLabelRow" }
NodeLabelInventory:
type: object
required: [nodeId, containers, byLabel, partial, generatedAt]
properties:
nodeId: { type: integer }
partial: { type: boolean }
generatedAt: { type: integer }
containers:
type: array
items: { $ref: "#/components/schemas/ContainerLabelRow" }
byLabel:
type: array
items: { $ref: "#/components/schemas/LabelIndexRow" }
FleetLabelInventory:
type: object
required: [nodes, aggregatedByLabel, nodeErrors, generatedAt]
properties:
generatedAt: { type: integer }
nodes:
type: array
items:
type: object
required: [nodeId, nodeName, status, inventory, error]
properties:
nodeId: { type: integer }
nodeName: { type: string }
status: { type: string, enum: [ok, error] }
inventory:
nullable: true
allOf: [{ $ref: "#/components/schemas/NodeLabelInventory" }]
error: { type: string, nullable: true }
aggregatedByLabel:
type: array
items: { $ref: "#/components/schemas/LabelIndexRow" }
nodeErrors:
type: object
additionalProperties: { type: string }
description: Map of node id to error for nodes that were unreachable or returned a malformed payload.
FailureClassification:
type: object
description: Classified cause of a failed deploy or update, with a suggested next step.
@@ -947,6 +1089,107 @@ paths:
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/label-inventory:
get:
operationId: getStackLabelInventory
tags: [Stacks]
summary: Get Docker label inventory for a stack
description: >-
Returns declared Compose labels and runtime container labels per service,
with reconciliation hints (only in Compose, only on running container,
present in both, or value changed when the values differ) and provenance
for each runtime label (compose, image, runtime, compose-system, or unknown
when a container or image cannot be inspected). Secret-like label values are
redacted unless the caller is an admin and passes `reveal=1`. Requires
`stack:read` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
- name: reveal
in: query
required: false
schema:
type: string
enum: ['1', 'true']
responses:
"200":
description: Stack label inventory.
content:
application/json:
schema:
$ref: "#/components/schemas/StackLabelInventory"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: Stack not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/system/container-labels:
get:
operationId: getNodeContainerLabels
tags: [Fleet]
summary: Get the Docker label inventory for the active node
description: >-
Returns every container on the node with its labels and provenance
(compose, image, runtime, compose-system, or unknown), plus an inverted
`byLabel` index. Marks `partial` when a container or image inspect fails.
Secret-like values are redacted unless the caller is an admin and passes
`reveal=1`. Requires `node:read` permission.
parameters:
- $ref: "#/components/parameters/nodeId"
- name: reveal
in: query
required: false
schema:
type: string
enum: ['1', 'true']
responses:
"200":
description: Node container label inventory.
content:
application/json:
schema:
$ref: "#/components/schemas/NodeLabelInventory"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/fleet/container-labels:
get:
operationId: getFleetContainerLabels
tags: [Fleet]
summary: Get the Docker label inventory aggregated across the fleet
description: >-
Fans out to every node's container label inventory and aggregates the
inverted index by key, value, and source. Unreachable or malformed nodes
degrade into `nodeErrors` rather than failing the whole request. Secret-like
values are redacted unless the caller is an admin and passes `reveal=1`.
Requires `node:read` permission.
parameters:
- name: reveal
in: query
required: false
schema:
type: string
enum: ['1', 'true']
responses:
"200":
description: Fleet-wide container label inventory.
content:
application/json:
schema:
$ref: "#/components/schemas/FleetLabelInventory"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/env:
get:
operationId: getStackEnv