mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat: add service-scoped Compose update and restore (#1648)
* feat: add service-scoped Compose update and restore Allow updating or rebuilding one declared Compose service on multi-service stacks without recreating siblings, with recovery snapshots, health-gate observation, and prune holds for rollback images. Full-stack update paths and single-service UX stay unchanged. * fix: sanitize service-scoped update log messages for CodeQL * fix: address service-scoped update audit findings B-01 through B-07 * fix: complete service-scoped update audit metadata and surfaces * test: wrap Updates readiness tests for deploy-feedback context * fix: keep service recovery reachable without Deploy Progress Make failed service-gate recovery discoverable when Deploy Progress is disabled or dismissed, suppress stale image-scan notification side effects, normalize ComposeService line endings, and add focused regression coverage. * fix: resurface ContainersHealth density and expand on multi-service stacks Service grouping hid the summary strip and Compact/Detailed/Expand controls that still applied to multi-container stacks.
This commit is contained in:
@@ -338,7 +338,7 @@ The entire host threshold evaluation can be silenced per node from **Settings ·
|
||||
|
||||
### Image update availability
|
||||
|
||||
`info`/`image_update_available`: `Stack "<name>" has image updates available.` By default, checks every two hours in interval mode, with a two-minute startup delay. The cadence can be changed or replaced with a cron schedule. Manual refresh carries a two-minute cooldown. Notifies on **state transition only**; the first run also emits catch-up notifications for stacks already known to have updates.
|
||||
`info`/`image_update_available`: `Stack "<name>" has image updates available.` On multi-service stacks the message can name the services that have updates. By default, checks every two hours in interval mode, with a two-minute startup delay. The cadence can be changed or replaced with a cron schedule. Manual refresh carries a two-minute cooldown. Notifies on **state transition only**; the first run also emits catch-up notifications for stacks already known to have updates.
|
||||
|
||||
### Auto-update execution
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ Once the cause is resolved, the next check (on the interval, or via **Recheck**)
|
||||
2. Skim the card grid. The badge tells you the risk at a glance: `Safe · patch` is green, `Review · minor` is amber, `Blocked · major` is red, and a digest-only rebuild on a non-semver tag shows the gray `Digest rebuild` badge.
|
||||
3. For a safe update, click **Apply now** on the card to pull and recreate the stack immediately.
|
||||
4. For a major bump, review the changelog preview and the upstream release notes. **Apply now** is disabled on the readiness board for blocked cards; to apply a major bump after review, use the stack's lifecycle **Update** action (right-click the stack in the sidebar, or open the kebab menu and choose **Update**, or click **Deploy** in the stack editor).
|
||||
|
||||
On multi-service stacks, the Updates view can also apply a single service when that service has a confirmed image update. Scheduled auto-update, webhook pull, and bulk update always refresh the full stack.
|
||||
5. Use **Recheck** in the hero to force an immediate registry poll across every reachable node. A 2-minute per-node cooldown applies, and the toast tells you how many nodes were triggered, rate-limited, or failed.
|
||||
|
||||
## Risk badges
|
||||
|
||||
@@ -62,7 +62,7 @@ A mono table of every stack discovered in the active node's `COMPOSE_DIR`, sorte
|
||||
| Column | Description |
|
||||
|--------|-------------|
|
||||
| **Status dot** | Green when the stack is running and its 10-minute peak CPU is under 80%, amber when peak CPU is at or above 80%, rose when any container has exited or peak CPU is at or above 90% |
|
||||
| **STACK** | Stack name with an orange "Update available" badge when a newer image has been detected. The badge appears regardless of the sidebar indicator setting. |
|
||||
| **STACK** | Stack name with an orange update badge when a newer image has been detected. When per-service status is known, the badge names the outdated service (or a count for several). The badge appears regardless of the sidebar indicator setting. |
|
||||
| **HOST** | Active node this stack belongs to |
|
||||
| **UP** | How long the oldest running container has been up, in compact units (`s` / `m` / `h` / `d`); a stopped or never-started stack reads `--` |
|
||||
| **CPU** | Latest aggregate CPU across the stack's containers |
|
||||
|
||||
@@ -34,6 +34,10 @@ Admins also see whether a [fleet snapshot](/features/fleet-backups) covers this
|
||||
|
||||
Every verdict keeps the **Update now** button enabled. The dialog informs the decision; it does not make it for you.
|
||||
|
||||
On stacks with more than one Compose service, you can also open readiness for a single service from that service's header. The dialog still checks stack-wide guardrails (Compose Doctor, disk, backup, Docker), but container health, pending change, and recovery signals focus on the selected service. Sibling and dependency issues stay advisory. The toolbar **Update** action and the Updates view's stack **Apply** always update the full stack.
|
||||
|
||||
When the selected service is build-backed and has no registry update, the proceed button reads **Rebuild** instead of **Update**. A per-service update badge appears only when a registry update is confirmed for that service.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/health-gated-updates/readiness-dialog.png" alt="Update readiness dialog showing the verdict chip, signal rows for Compose Doctor, Drift, Current containers, Healthcheck coverage, Pending update, Rollback backup and Node disk, the fleet snapshot row with its checkbox, and the Cancel and Update now buttons" />
|
||||
</Frame>
|
||||
@@ -50,6 +54,8 @@ During the window, the gate checks every container that came up with the stack:
|
||||
|
||||
A clear failure ends the observation immediately. Passing requires the full window, and a healthcheck still in its start period when the window ends records the verdict as unknown rather than claiming success.
|
||||
|
||||
After a **service-scoped** update or restore, the gate still watches the whole project, but it attributes failures: a problem on the updated service is a primary failure; a previously healthy sibling that regresses during the window is a collateral failure. Siblings that were already unhealthy before the update do not fail the gate on their own.
|
||||
|
||||
The deploy progress modal treats the gate verdict as the real result: while the gate observes, the status reads **Verifying health** rather than claiming success, and the auto-close waits. A passed gate shows the success state; a failed or unknown gate becomes the headline result with its reason. Closing the modal never stops the observation; the gate runs server-side and its result appears on the stack timeline either way, as **health gate passed** or **health gate failed** events alongside the **update started** marker.
|
||||
|
||||
The live verifying and recovery view is part of the deploy progress panel. If you have turned that panel off, the in-browser gate view does not appear, but the gate still runs on the node and records its verdict on the stack timeline.
|
||||
|
||||
@@ -79,6 +79,7 @@ Every Sencho release ships with a static list of capabilities. The current list
|
||||
| `compose-doctor` | Compose Doctor preflight |
|
||||
| `compose-networking` | Stack Networking tab and node Networking overview |
|
||||
| `guided-external-network-preflight` | Guided missing-external-network check before deploy |
|
||||
| `service-scoped-update` | Per-service update, rebuild, and restore on multi-service stacks |
|
||||
|
||||
<Note>
|
||||
`vulnerability-scanning` is advertised only when the Trivy binary is present on the node. If a node does not have Trivy installed, it omits this capability and the scanning UI is replaced by a lock card.
|
||||
|
||||
@@ -68,7 +68,7 @@ Each row gives you everything you need to read the stack at a glance, in a fixed
|
||||
- **Stack name** in mono type, truncated with an ellipsis when the row gets tight.
|
||||
- **Label dots** to the right of the name. Up to three colored dots represent the stack's labels. If a stack carries more than three labels, a **+N** counter appears for the extras.
|
||||
- **Trailing indicator** in its own fixed-width slot at the far right of the name area, showing the highest-priority signal present:
|
||||
- Pulsing orange dot: an image update is available. Hover for the "Update available" tooltip.
|
||||
- Pulsing orange dot: an image update is available. Hover for the update tooltip; when per-service status is known, the tooltip names the outdated service(s).
|
||||
- Muted circle-alert icon: the last registry check failed (registry unreachable, missing credentials, or rate-limited). Hover to read the failure reason.
|
||||
- Git branch icon: a Git source update is pending and no image update is available.
|
||||
- **Hover kebab** on the right edge. On desktop, hover the row to reveal a vertical three-dot menu that opens the same actions as right-clicking. On touch viewports the kebab is always visible.
|
||||
|
||||
@@ -220,7 +220,15 @@ Each row includes:
|
||||
- **Meta line.** Uptime (`up 12 hours`) and the primary port mapping (`8989 → 8989/tcp`).
|
||||
- **Open link.** When the container publishes a port, the mapping itself is a link (`8989 → 8989/tcp ↗`) that opens the service in a new tab, with a **Copy URL** button beside it. The address uses the active node's host and switches to `https` for port 443. Recognised multi-port apps open their web path automatically (for example, Plex opens `/web`).
|
||||
- **Live stat tiles.** Three tiles show CPU, memory, and network I/O with a rolling sparkline. The sparkline uses the cyan data color and refreshes roughly every 1.5 seconds.
|
||||
- **Action icons.** The image source links button (see above), plus shortcuts to **View logs**, open a bash shell, and the per-service kebab.
|
||||
- **Action icons.** The image source links button (see above), plus shortcuts to **View logs**, open a bash shell, and (on single-service stacks) the per-container Start / Stop / Restart kebab.
|
||||
|
||||
### Multi-service stacks
|
||||
|
||||
When a stack declares more than one Compose service, each service gets its own header above its container cards. That header owns **Update service** or **Rebuild service** (when eligible), the update badge when a registry update is confirmed for that service, and **Start / Stop / Restart** for every replica of the service. Child container cards keep logs, shell, ports, and metrics only.
|
||||
|
||||
Build-backed services without a registry update show **Rebuild** wording and no update badge. The editor toolbar **Update** action still updates the entire stack.
|
||||
|
||||
Single-service stacks keep the existing flat container layout; service headers do not appear.
|
||||
|
||||
## Logs viewer
|
||||
|
||||
|
||||
+270
-2
@@ -1772,10 +1772,21 @@ paths:
|
||||
now, from the stored preflight result, open drift findings, live
|
||||
container health, the pending image change, the rollback backup slot,
|
||||
and node disk headroom. Advisory only: no verdict blocks an update.
|
||||
Requires `stack:read` permission.
|
||||
Requires `stack:read` permission. Pass `service` to scope the verdict
|
||||
to one Compose service on a multi-service stack.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stackName"
|
||||
- $ref: "#/components/parameters/nodeId"
|
||||
- name: service
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: |
|
||||
When set, readiness is scoped to this Compose service (multi-service
|
||||
stacks only). Stack-wide guardrails still apply; sibling and
|
||||
dependency signals become advisory. Single-service stacks reject
|
||||
this parameter with 400.
|
||||
responses:
|
||||
"200":
|
||||
description: Readiness report.
|
||||
@@ -1810,6 +1821,12 @@ paths:
|
||||
type: string
|
||||
affectsVerdict:
|
||||
type: boolean
|
||||
"400":
|
||||
description: Invalid service scope.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
@@ -1817,6 +1834,238 @@ paths:
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/api/stacks/{stackName}/effective-services:
|
||||
get:
|
||||
operationId: getStackEffectiveServices
|
||||
tags: [Stacks]
|
||||
summary: List effective Compose services
|
||||
description: |
|
||||
Returns the fail-closed effective service model for the stack (declared
|
||||
image, build presence, expected replicas, depends_on, healthcheck).
|
||||
Requires `stack:read` permission.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stackName"
|
||||
- $ref: "#/components/parameters/nodeId"
|
||||
responses:
|
||||
"200":
|
||||
description: Effective model result.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [renderable]
|
||||
properties:
|
||||
renderable:
|
||||
type: boolean
|
||||
services:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [name, declaredImage, hasBuild, expectedReplicas, dependsOn, hasHealthcheck]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
declaredImage:
|
||||
type: string
|
||||
nullable: true
|
||||
hasBuild:
|
||||
type: boolean
|
||||
expectedReplicas:
|
||||
type: integer
|
||||
dependsOn:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
hasHealthcheck:
|
||||
type: boolean
|
||||
code:
|
||||
type: string
|
||||
error:
|
||||
type: string
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/api/stacks/{stackName}/services/{serviceName}/update:
|
||||
post:
|
||||
operationId: updateStackService
|
||||
tags: [Stacks]
|
||||
summary: Update or rebuild one Compose service
|
||||
description: |
|
||||
Manually pulls or builds one declared service, then recreates only that
|
||||
service with `--no-deps --force-recreate`. Multi-service stacks only.
|
||||
Requires `stack:deploy`. Automation paths continue to update the full stack.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stackName"
|
||||
- name: serviceName
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- $ref: "#/components/parameters/nodeId"
|
||||
responses:
|
||||
"200":
|
||||
description: Service update completed.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [serviceName, healthGateId, observing, recoveryAvailable]
|
||||
properties:
|
||||
serviceName:
|
||||
type: string
|
||||
healthGateId:
|
||||
type: string
|
||||
nullable: true
|
||||
observing:
|
||||
type: boolean
|
||||
recoveryId:
|
||||
type: string
|
||||
nullable: true
|
||||
recoveryAvailable:
|
||||
type: boolean
|
||||
recheckWarning:
|
||||
type: string
|
||||
"400":
|
||||
description: Service update rejected (single-service stack, missing service, or unavailable capability).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"500":
|
||||
description: Service update failed.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/api/stacks/{stackName}/services/{serviceName}/recovery:
|
||||
get:
|
||||
operationId: getStackServiceRecovery
|
||||
tags: [Stacks]
|
||||
summary: Latest active service recovery snapshot
|
||||
description: |
|
||||
Returns the newest active, unexpired recovery row for one service, or
|
||||
`recovery: null` when none is available. Used so Restore stays
|
||||
discoverable when Deploy Progress is disabled or dismissed.
|
||||
Requires `stack:deploy`.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stackName"
|
||||
- name: serviceName
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- $ref: "#/components/parameters/nodeId"
|
||||
responses:
|
||||
"200":
|
||||
description: Recovery lookup completed.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [recovery]
|
||||
properties:
|
||||
recovery:
|
||||
oneOf:
|
||||
- type: "null"
|
||||
- type: object
|
||||
required: [id, status, expiresAt, createdAt]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
healthGateId:
|
||||
type: string
|
||||
nullable: true
|
||||
expiresAt:
|
||||
type: integer
|
||||
createdAt:
|
||||
type: integer
|
||||
majorityImageId:
|
||||
type: string
|
||||
declaredImageRef:
|
||||
type: string
|
||||
"400":
|
||||
description: Capability unavailable.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
/api/stacks/{stackName}/services/{serviceName}/restore:
|
||||
post:
|
||||
operationId: restoreStackService
|
||||
tags: [Stacks]
|
||||
summary: Restore one Compose service from a recovery snapshot
|
||||
description: |
|
||||
Retags the captured majority image onto the declared tag and recreates
|
||||
only that service. Body must include `recoveryId` from a prior service
|
||||
update. Requires `stack:deploy`.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stackName"
|
||||
- name: serviceName
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- $ref: "#/components/parameters/nodeId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [recoveryId]
|
||||
properties:
|
||||
recoveryId:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Service restore completed.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [serviceName, healthGateId, observing, recoveryAvailable]
|
||||
properties:
|
||||
serviceName:
|
||||
type: string
|
||||
healthGateId:
|
||||
type: string
|
||||
nullable: true
|
||||
observing:
|
||||
type: boolean
|
||||
recoveryId:
|
||||
type: string
|
||||
nullable: true
|
||||
recoveryAvailable:
|
||||
type: boolean
|
||||
"400":
|
||||
description: Restore rejected (missing recoveryId or invalid binding).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"500":
|
||||
description: Service restore failed.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/api/stacks/{stackName}/rollback-readiness:
|
||||
get:
|
||||
operationId: getStackRollbackReadiness
|
||||
@@ -1908,7 +2157,7 @@ paths:
|
||||
trigger:
|
||||
type: string
|
||||
nullable: true
|
||||
enum: [update, deploy, null]
|
||||
enum: [update, deploy, service_update, service_restore, null]
|
||||
reason:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1937,6 +2186,25 @@ paths:
|
||||
nullable: true
|
||||
restarts:
|
||||
type: integer
|
||||
service:
|
||||
type: string
|
||||
nullable: true
|
||||
role:
|
||||
type: string
|
||||
nullable: true
|
||||
enum: [primary, collateral, null]
|
||||
targetScope:
|
||||
type: string
|
||||
enum: [stack, service]
|
||||
description: Defaults to stack for legacy runs.
|
||||
serviceName:
|
||||
type: string
|
||||
nullable: true
|
||||
failureSource:
|
||||
type: string
|
||||
nullable: true
|
||||
enum: [primary, collateral, null]
|
||||
description: Set on failed service-scoped runs; null for stack and successful runs.
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
|
||||
Reference in New Issue
Block a user