From 0fbc3f121d9c603c9ef51ce41a7c367df878dea9 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:55:34 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(api):=20return=20explicit=20?= =?UTF-8?q?501=20for=20agent=20containers=20without=20lifecycle=20transpor?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit executeAction (POST /:id/start|stop|restart) returned a bare 404 NO_DOCKER_TRIGGER_FOUND_ERROR whenever the docker-trigger lookup missed, indistinguishable from "container not found" — for agents that don't advertise usesControllerDockerTransport this was the only signal surfaced to the UI. Agent-owned containers now get a 501 naming the missing capability; non-agent containers keep the existing 404. OpenAPI specs for start/stop/restart document the new 501 response. Fixes: #637 --- CHANGELOG.md | 2 +- app/agent/AgentClient.test.ts | 20 +++ app/agent/AgentClient.ts | 9 ++ app/api/backup.test.ts | 183 +++++++++++++++++++++++ app/api/backup.ts | 12 +- app/api/container-actions.test.ts | 126 ++++++++++++++++ app/api/container-actions.ts | 12 +- app/api/docker-trigger.ts | 30 ++++ app/api/openapi/paths/containers.test.ts | 3 + app/api/openapi/paths/containers.ts | 10 ++ 10 files changed, 404 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 587bf659a..8cdfe10e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - **WebSocket log streams no longer reject anonymous-auth sessions** ([#636](https://github.com/CodesWhat/drydock/issues/636)). Both WS upgrade paths — the system log stream and the container log stream — gated on `isAuthenticatedSession()` requiring `session.passport.user`, which `passport-anonymous` never sets, so under `DD_ANONYMOUS_AUTH_CONFIRM=true` the log stream WebSocket always rejected the upgrade even though every REST endpoint worked. `isAuthenticatedSession` now also accepts the session when anonymous authentication is the registered mode. +- **Container start/stop/restart/rollback return an explicit 501 instead of an ambiguous 404 for agent containers without lifecycle transport** ([#637](https://github.com/CodesWhat/drydock/issues/637)). `POST /:id/start|stop|restart` and `POST /:id/rollback` returned a bare 404 `No docker trigger found for this container` whenever the lookup missed, indistinguishable from "container not found" — for agent-owned containers this was the only signal the UI got. That lookup miss now returns 501 naming the likely cause (the agent's connection typically hasn't advertised `usesControllerDockerTransport`) when `container.agent` is set; non-agent containers still get the existing 404. This complements the native-transport support that shipped in rc.11 via [#651](https://github.com/CodesWhat/drydock/pull/651), which closed #637's core gap — this is the remaining explicit-error half. ### Security - **`brace-expansion`, `ip-address`, and `fast-uri` overrides advanced to patched releases.** `brace-expansion` moved to 5.0.9 in `app/`, `ui/`, and `e2e/` (CVE-2026-69152, [GHSA-rgw5-rvv9-x895](https://github.com/advisories/GHSA-rgw5-rvv9-x895)); `ip-address` moved to 10.3.1 in `app/` (CVE-2026-54272, CVE-2026-69192, CVE-2026-69198), pulled in transitively via `express-rate-limit` and `mqtt` → `socks`; `fast-uri` advanced from 4.1.1 to 4.1.2 in `app/` and `ui/` (host confusion via backslash authority introducer, CVE-2026-18446, [GHSA-7p8r-x3mc-p8w7](https://github.com/advisories/GHSA-7p8r-x3mc-p8w7), superseding [#658](https://github.com/CodesWhat/drydock/pull/658)). -||||||| parent of 16d6927c (🐛 fix(api): honor active anonymous auth in WS log-stream upgrades) ## [1.6.0-rc.11] — 2026-08-01 diff --git a/app/agent/AgentClient.test.ts b/app/agent/AgentClient.test.ts index 31f80086b..b26504a68 100644 --- a/app/agent/AgentClient.test.ts +++ b/app/agent/AgentClient.test.ts @@ -7869,6 +7869,26 @@ describe('AgentClient', () => { }); describe('Portwing Docker API transport', () => { + test('reports whether a watcher uses controller Docker transport', async () => { + await client.handleComponentSync( + [ + { + type: 'docker', + name: 'docker', + configuration: { + transport: 'docker-api', + execution: 'controller', + events: 'portwing', + }, + }, + ], + [], + ); + + expect(client.hasControllerDockerTransport('docker')).toBe(true); + expect(client.hasControllerDockerTransport('missing')).toBe(false); + }); + test('component sync synthesizes docker/update only for a controller Docker transport watcher', async () => { const watcher = { type: 'docker', diff --git a/app/agent/AgentClient.ts b/app/agent/AgentClient.ts index e3f0ff995..499c4ddf6 100644 --- a/app/agent/AgentClient.ts +++ b/app/agent/AgentClient.ts @@ -331,6 +331,15 @@ export class AgentClient { return this.watcherSnapshotCache.get(watcherSnapshotCacheKey(watcherType, watcherName)); } + /** + * Whether the given watcher on this agent advertises controller Docker + * transport, i.e. lifecycle actions (start/stop/restart/rollback) execute + * locally on the controller instead of being proxied to the agent. + */ + hasControllerDockerTransport(watcherName: string): boolean { + return this.controllerDockerTransportWatchers.has(watcherName); + } + private parseBaseUrl(): URL { // Validate the URL to prevent request forgery (CodeQL js/request-forgery) const parsed = new URL(this.getCandidateUrl()); diff --git a/app/api/backup.test.ts b/app/api/backup.test.ts index 66f8d42b0..e1f5d952d 100644 --- a/app/api/backup.test.ts +++ b/app/api/backup.test.ts @@ -10,6 +10,7 @@ const { mockGetAllBackups, mockGetBackup, mockGetState, + mockGetAgent, } = vi.hoisted(() => ({ mockRouter: { use: vi.fn(), get: vi.fn(), post: vi.fn() }, mockGetContainer: vi.fn(), @@ -17,6 +18,7 @@ const { mockGetAllBackups: vi.fn(), mockGetBackup: vi.fn(), mockGetState: vi.fn(), + mockGetAgent: vi.fn(), })); vi.mock('express', () => ({ @@ -40,6 +42,10 @@ vi.mock('../registry', () => ({ getState: mockGetState, })); +vi.mock('../agent/manager', () => ({ + getAgent: mockGetAgent, +})); + const { mockBackupLog } = vi.hoisted(() => ({ mockBackupLog: { info: vi.fn(), warn: vi.fn(), debug: vi.fn() }, })); @@ -263,6 +269,183 @@ describe('Backup Router', () => { }); }); + test('should return 404 when a capable agent has no docker trigger registered yet', async () => { + const handler = getHandler('post', '/:id/rollback'); + mockGetContainer.mockReturnValue({ + id: 'c1', + name: 'nginx', + agent: 'edge-1', + watcher: 'edge-1', + }); + mockGetBackupsByName.mockReturnValue([ + { + id: 'b1', + containerId: 'c1', + imageName: 'library/nginx', + imageTag: '1.24', + }, + ]); + mockGetState.mockReturnValue({ trigger: {} }); + mockGetAgent.mockReturnValue({ hasControllerDockerTransport: vi.fn(() => true) }); + + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + error: expect.stringContaining('No docker trigger found'), + }); + }); + + test('should return 404, not 501, when the agent is unknown or disconnected', async () => { + const handler = getHandler('post', '/:id/rollback'); + mockGetContainer.mockReturnValue({ + id: 'c1', + name: 'nginx', + agent: 'edge-1', + watcher: 'edge-1', + }); + mockGetBackupsByName.mockReturnValue([ + { + id: 'b1', + containerId: 'c1', + imageName: 'library/nginx', + imageTag: '1.24', + }, + ]); + mockGetState.mockReturnValue({ trigger: {} }); + mockGetAgent.mockReturnValue(undefined); + + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + error: expect.stringContaining('No docker trigger found'), + }); + }); + + test('should return 501 when the agent-owned container agent lacks controller docker transport', async () => { + const handler = getHandler('post', '/:id/rollback'); + mockGetContainer.mockReturnValue({ + id: 'c1', + name: 'nginx', + agent: 'edge-1', + watcher: 'edge-1', + }); + mockGetBackupsByName.mockReturnValue([ + { + id: 'b1', + containerId: 'c1', + imageName: 'library/nginx', + imageTag: '1.24', + }, + ]); + mockGetState.mockReturnValue({ trigger: {} }); + mockGetAgent.mockReturnValue({ hasControllerDockerTransport: vi.fn(() => false) }); + + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(res.status).toHaveBeenCalledWith(501); + expect(res.json).toHaveBeenCalledWith({ + error: expect.stringContaining("container's agent connection"), + }); + }); + + test('should return 501, not 500, when a legacy incapable AgentTrigger is registered for the container', async () => { + const handler = getHandler('post', '/:id/rollback'); + const legacyAgentTrigger = { + type: 'docker', + agent: 'edge-1', + getWatcher: vi.fn(() => { + throw new Error( + 'AgentTrigger docker.edge-1 cannot provide local Docker capability getWatcher; the agent does not advertise controller Docker transport', + ); + }), + }; + mockGetContainer.mockReturnValue({ + id: 'c1', + name: 'nginx', + agent: 'edge-1', + watcher: 'edge-1', + }); + mockGetBackupsByName.mockReturnValue([ + { + id: 'b1', + containerId: 'c1', + imageName: 'library/nginx', + imageTag: '1.24', + }, + ]); + mockGetState.mockReturnValue({ trigger: { 'docker.edge-1': legacyAgentTrigger } }); + mockGetAgent.mockReturnValue({ hasControllerDockerTransport: vi.fn(() => false) }); + + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(legacyAgentTrigger.getWatcher).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(501); + expect(res.json).toHaveBeenCalledWith({ + error: expect.stringContaining("container's agent connection"), + }); + }); + + test('should roll back an agent-owned container whose agent advertises controller docker transport', async () => { + const handler = getHandler('post', '/:id/rollback'); + const container = { + id: 'c1', + name: 'nginx', + agent: 'edge-1', + watcher: 'edge-1', + image: { registry: { name: 'hub' } }, + }; + const latestBackup = { + id: 'b1', + containerId: 'c1', + imageName: 'library/nginx', + imageTag: '1.24', + }; + + mockGetContainer.mockReturnValue(container); + mockGetBackupsByName.mockReturnValue([latestBackup]); + mockGetAgent.mockReturnValue({ hasControllerDockerTransport: vi.fn(() => true) }); + + const mockCurrentContainer = {}; + const mockContainerSpec = { State: { Running: true } }; + const mockTrigger = { + type: 'docker', + agent: 'edge-1', + getWatcher: vi.fn(() => ({ dockerApi: {} })), + pullImage: vi.fn().mockResolvedValue(undefined), + getCurrentContainer: vi.fn().mockResolvedValue(mockCurrentContainer), + inspectContainer: vi.fn().mockResolvedValue(mockContainerSpec), + stopAndRemoveContainer: vi.fn().mockResolvedValue(undefined), + recreateContainer: vi.fn().mockResolvedValue(undefined), + }; + mockGetState.mockReturnValue({ + trigger: { 'docker.edge-1': mockTrigger }, + registry: { hub: { getAuthPull: vi.fn().mockResolvedValue({}) } }, + }); + + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(mockTrigger.pullImage).toHaveBeenCalled(); + expect(mockTrigger.stopAndRemoveContainer).toHaveBeenCalled(); + expect(mockTrigger.recreateContainer).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'Container rolled back successfully', + backup: latestBackup, + }); + }); + test('should rollback successfully', async () => { const handler = getHandler('post', '/:id/rollback'); const container = { diff --git a/app/api/backup.ts b/app/api/backup.ts index 05fbf6dbd..d4de711fa 100644 --- a/app/api/backup.ts +++ b/app/api/backup.ts @@ -10,7 +10,12 @@ import { } from '../triggers/providers/docker/created-container-candidate.js'; import { recordAuditEvent } from './audit-events.js'; import { requireDestructiveActionConfirmation } from './destructive-confirmation.js'; -import { findDockerTriggerForContainer, NO_DOCKER_TRIGGER_FOUND_ERROR } from './docker-trigger.js'; +import { + AGENT_LIFECYCLE_UNSUPPORTED_ERROR, + findDockerTriggerForContainer, + isAgentLifecycleUnsupported, + NO_DOCKER_TRIGGER_FOUND_ERROR, +} from './docker-trigger.js'; import { sendErrorResponse } from './error-response.js'; import { handleContainerActionError } from './helpers.js'; @@ -82,6 +87,11 @@ async function rollbackContainer(req: Request, res: Response) { backup = backups[0]; } + if (isAgentLifecycleUnsupported(container)) { + sendErrorResponse(res, 501, AGENT_LIFECYCLE_UNSUPPORTED_ERROR); + return; + } + const trigger = findDockerTriggerForContainer(registry.getState().trigger, container); if (!trigger) { sendErrorResponse(res, 404, NO_DOCKER_TRIGGER_FOUND_ERROR); diff --git a/app/api/container-actions.test.ts b/app/api/container-actions.test.ts index 2e4ff03fd..a5aa07bce 100644 --- a/app/api/container-actions.test.ts +++ b/app/api/container-actions.test.ts @@ -8,6 +8,7 @@ const { mockUpdateContainer, mockMarkPendingFreshStateAfterManualUpdate, mockGetState, + mockGetAgent, mockInsertAudit, mockGetAuditCounter, mockGetContainerActionsCounter, @@ -19,6 +20,7 @@ const { mockUpdateContainer: vi.fn((c) => c), mockMarkPendingFreshStateAfterManualUpdate: vi.fn(), mockGetState: vi.fn(), + mockGetAgent: vi.fn(), mockInsertAudit: vi.fn(), mockGetAuditCounter: vi.fn(), mockGetContainerActionsCounter: vi.fn(), @@ -42,6 +44,10 @@ vi.mock('../registry', () => ({ getState: mockGetState, })); +vi.mock('../agent/manager', () => ({ + getAgent: mockGetAgent, +})); + vi.mock('../store/audit', () => ({ insertAudit: mockInsertAudit, })); @@ -189,6 +195,126 @@ describe('Container Actions Router', () => { }); }); + test('should start an agent-owned container whose agent advertises controller docker transport', async () => { + const container = { + id: 'c1', + name: 'nginx', + image: { name: 'nginx' }, + agent: 'edge-1', + watcher: 'edge-1', + }; + mockGetContainer.mockReturnValue(container); + const { trigger, dockerContainer } = createDockerTrigger({ agent: 'edge-1' }); + mockGetState.mockReturnValue({ trigger: { 'docker.edge-1': trigger } }); + mockGetAgent.mockReturnValue({ hasControllerDockerTransport: vi.fn(() => true) }); + + const handler = getHandler('post', '/:id/start'); + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(dockerContainer.start).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'Container started successfully', + result: expect.any(Object), + }); + }); + + test('should return 404 when a capable agent has no docker trigger registered yet', async () => { + mockGetContainer.mockReturnValue({ + id: 'c1', + name: 'nginx', + agent: 'edge-1', + watcher: 'edge-1', + }); + mockGetState.mockReturnValue({ trigger: {} }); + mockGetAgent.mockReturnValue({ hasControllerDockerTransport: vi.fn(() => true) }); + + const handler = getHandler('post', '/:id/start'); + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + error: expect.stringContaining('No docker trigger found'), + }); + }); + + test('should return 404, not 501, when the agent is unknown or disconnected', async () => { + mockGetContainer.mockReturnValue({ + id: 'c1', + name: 'nginx', + agent: 'edge-1', + watcher: 'edge-1', + }); + mockGetState.mockReturnValue({ trigger: {} }); + mockGetAgent.mockReturnValue(undefined); + + const handler = getHandler('post', '/:id/start'); + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + error: expect.stringContaining('No docker trigger found'), + }); + }); + + test('should return 501 when the agent-owned container agent lacks controller docker transport', async () => { + mockGetContainer.mockReturnValue({ + id: 'c1', + name: 'nginx', + agent: 'edge-1', + watcher: 'edge-1', + }); + mockGetState.mockReturnValue({ trigger: {} }); + mockGetAgent.mockReturnValue({ hasControllerDockerTransport: vi.fn(() => false) }); + + const handler = getHandler('post', '/:id/start'); + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(res.status).toHaveBeenCalledWith(501); + expect(res.json).toHaveBeenCalledWith({ + error: expect.stringContaining("container's agent connection"), + }); + }); + + test('should return 501, not 500, when a legacy incapable AgentTrigger is registered for the container', async () => { + const legacyAgentTrigger = { + type: 'docker', + agent: 'edge-1', + getWatcher: vi.fn(() => { + throw new Error( + 'AgentTrigger docker.edge-1 cannot provide local Docker capability getWatcher; the agent does not advertise controller Docker transport', + ); + }), + }; + mockGetContainer.mockReturnValue({ + id: 'c1', + name: 'nginx', + agent: 'edge-1', + watcher: 'edge-1', + }); + mockGetState.mockReturnValue({ trigger: { 'docker.edge-1': legacyAgentTrigger } }); + mockGetAgent.mockReturnValue({ hasControllerDockerTransport: vi.fn(() => false) }); + + const handler = getHandler('post', '/:id/start'); + const req = createMockRequest({ params: { id: 'c1' } }); + const res = createMockResponse(); + await handler(req, res); + + expect(legacyAgentTrigger.getWatcher).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(501); + expect(res.json).toHaveBeenCalledWith({ + error: expect.stringContaining("container's agent connection"), + }); + }); + test('should return 403 when feature flag is disabled', async () => { mockGetServerConfiguration.mockReturnValue({ feature: { containeractions: false } }); diff --git a/app/api/container-actions.ts b/app/api/container-actions.ts index 9ce0197e4..d0e83d7bb 100644 --- a/app/api/container-actions.ts +++ b/app/api/container-actions.ts @@ -15,7 +15,12 @@ import { UpdateRequestError, } from '../updates/request-update.js'; import { recordAuditEvent } from './audit-events.js'; -import { findDockerTriggerForContainer, NO_DOCKER_TRIGGER_FOUND_ERROR } from './docker-trigger.js'; +import { + AGENT_LIFECYCLE_UNSUPPORTED_ERROR, + findDockerTriggerForContainer, + isAgentLifecycleUnsupported, + NO_DOCKER_TRIGGER_FOUND_ERROR, +} from './docker-trigger.js'; import { sendErrorResponse } from './error-response.js'; import { handleContainerActionError } from './helpers.js'; @@ -118,6 +123,11 @@ async function executeAction( return; } + if (isAgentLifecycleUnsupported(container)) { + sendErrorResponse(res, 501, AGENT_LIFECYCLE_UNSUPPORTED_ERROR); + return; + } + const trigger = findDockerTriggerForContainer(registry.getState().trigger, container); if (!trigger) { sendErrorResponse(res, 404, NO_DOCKER_TRIGGER_FOUND_ERROR); diff --git a/app/api/docker-trigger.ts b/app/api/docker-trigger.ts index d509bdc83..930379199 100644 --- a/app/api/docker-trigger.ts +++ b/app/api/docker-trigger.ts @@ -1,9 +1,12 @@ import path from 'node:path'; +import { getAgent } from '../agent/manager.js'; import type { Container } from '../model/container.js'; import type Docker from '../triggers/providers/docker/Docker.js'; import type Trigger from '../triggers/providers/Trigger.js'; export const NO_DOCKER_TRIGGER_FOUND_ERROR = 'No docker trigger found for this container'; +export const AGENT_LIFECYCLE_UNSUPPORTED_ERROR = + "Lifecycle actions (start/stop/restart) are not supported over this container's agent connection, typically because the agent has not advertised the usesControllerDockerTransport capability."; const DEFAULT_TRIGGER_TYPES = ['docker', 'dockercompose']; const COMPOSE_DIRECTORY_FILE_CANDIDATES = new Set([ 'compose.yaml', @@ -255,6 +258,33 @@ export function isTriggerCompatibleWithContainer( return true; } +/** + * Whether lifecycle actions (start/stop/restart/rollback) are unsupported for + * an agent-owned container, decided from the agent's actual advertised + * capability rather than trigger presence/absence: + * + * - No agent on the container: never unsupported (non-agent containers are + * handled by the plain docker-trigger lookup). + * - Agent not currently connected/registered: not decided here; the caller's + * docker-trigger lookup will report the honest transient 404 instead. + * - Agent connected but its watcher hasn't advertised controller Docker + * transport: unsupported, regardless of whether a legacy AgentTrigger is + * still registered for it (that trigger's getWatcher()/rollback methods + * throw rather than working). + */ +export function isAgentLifecycleUnsupported( + container: Pick, +): boolean { + if (!container.agent) { + return false; + } + const agentClient = getAgent(container.agent); + if (!agentClient) { + return false; + } + return !agentClient.hasControllerDockerTransport(container.watcher); +} + /** * Find a docker trigger compatible with a container's agent context. */ diff --git a/app/api/openapi/paths/containers.test.ts b/app/api/openapi/paths/containers.test.ts index 4d4c3302f..4b9701d33 100644 --- a/app/api/openapi/paths/containers.test.ts +++ b/app/api/openapi/paths/containers.test.ts @@ -73,6 +73,7 @@ describe('containerPaths', () => { 200: jsonResponse('Container started', { $ref: '#/components/schemas/ContainerActionResponse', }), + 501: errorResponse("Lifecycle actions unsupported by this container's agent connection"), 401: errorResponse('Authentication required'), 403: errorResponse('Container actions feature disabled'), 404: errorResponse('Container or docker trigger not found'), @@ -94,6 +95,7 @@ describe('containerPaths', () => { 200: jsonResponse('Container stopped', { $ref: '#/components/schemas/ContainerActionResponse', }), + 501: errorResponse("Lifecycle actions unsupported by this container's agent connection"), 401: errorResponse('Authentication required'), 403: errorResponse('Container actions feature disabled'), 404: errorResponse('Container or docker trigger not found'), @@ -115,6 +117,7 @@ describe('containerPaths', () => { 200: jsonResponse('Container restarted', { $ref: '#/components/schemas/ContainerActionResponse', }), + 501: errorResponse("Lifecycle actions unsupported by this container's agent connection"), 401: errorResponse('Authentication required'), 403: errorResponse('Container actions feature disabled'), 404: errorResponse('Container or docker trigger not found'), diff --git a/app/api/openapi/paths/containers.ts b/app/api/openapi/paths/containers.ts index cfedcc5b6..70790b2f7 100644 --- a/app/api/openapi/paths/containers.ts +++ b/app/api/openapi/paths/containers.ts @@ -818,6 +818,7 @@ export const containerPaths = { 200: jsonResponse('Rollback successful', { $ref: '#/components/schemas/ContainerRollbackResponse', }), + 501: errorResponse("Lifecycle actions unsupported by this container's agent connection"), 401: errorResponse('Authentication required'), 428: errorResponse('Destructive confirmation header is required'), 404: errorResponse('Container, backup, or trigger not found'), @@ -830,18 +831,27 @@ export const containerPaths = { operationId: 'startContainer', successDescription: 'Container started', failureDescription: 'Container start failed', + additionalErrorResponses: { + 501: errorResponse("Lifecycle actions unsupported by this container's agent connection"), + }, }), '/api/v1/containers/{id}/stop': createRuntimeContainerActionPath({ summary: 'Stop container', operationId: 'stopContainer', successDescription: 'Container stopped', failureDescription: 'Container stop failed', + additionalErrorResponses: { + 501: errorResponse("Lifecycle actions unsupported by this container's agent connection"), + }, }), '/api/v1/containers/{id}/restart': createRuntimeContainerActionPath({ summary: 'Restart container', operationId: 'restartContainer', successDescription: 'Container restarted', failureDescription: 'Container restart failed', + additionalErrorResponses: { + 501: errorResponse("Lifecycle actions unsupported by this container's agent connection"), + }, }), '/api/v1/containers/{id}/update': createRuntimeContainerActionPath({ summary: 'Update container to latest available image',