mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 03:06:54 +00:00
feat: add Docker label audit across Fleet and Stack views (#1531)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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). |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user