fix(fleet): drop tier gate from stack and container list endpoints (#1012)

Community tier opened the Fleet Overview drilldown (node card → stack
list → container list) but two read-only metadata endpoints in
fleet.ts kept their requirePaid gate. Stack names still rendered
because they ride on the un-gated /api/fleet/overview payload, but
expanding a stack always re-fetched containers and hit the orphan
gate, surfacing as a "Failed to load containers for X" toast on every
node card on Community.

Drop requirePaid from GET /node/:nodeId/stacks and
GET /node/:nodeId/stacks/:stackName/containers. Both handlers stay
behind authMiddleware, validate inputs, and proxy to the per-node
api_token for remote nodes. Paid mutations (bulk update, scheduled
snapshots, label bulk actions, Fleet Actions cards) keep their gates
in fleetActions.ts. Add positive Community-tier tests in fleet.test.ts
and clear the stale "Requires Skipper or Admiral license" line from
the OpenAPI descriptions.
This commit is contained in:
Anso
2026-05-09 01:55:56 -04:00
committed by GitHub
parent 412d3696c8
commit 693f9b4495
3 changed files with 18 additions and 6 deletions
+16
View File
@@ -205,6 +205,22 @@ describe('Fleet tier gating', () => {
.set('Authorization', authHeader);
expect(res.body.code).not.toBe('PAID_REQUIRED');
});
it('GET /api/fleet/node/:nodeId/stacks is accessible on community tier', async () => {
mockTier('community');
const res = await request(app)
.get('/api/fleet/node/1/stacks')
.set('Authorization', authHeader);
expect(res.body.code).not.toBe('PAID_REQUIRED');
});
it('GET /api/fleet/node/:nodeId/stacks/:stackName/containers is accessible on community tier', async () => {
mockTier('community');
const res = await request(app)
.get('/api/fleet/node/1/stacks/db-compose/containers')
.set('Authorization', authHeader);
expect(res.body.code).not.toBe('PAID_REQUIRED');
});
});
// ─── Update Endpoint Input Validation ───
-4
View File
@@ -595,8 +595,6 @@ fleetRouter.get('/configuration', authMiddleware, async (req: Request, res: Resp
});
fleetRouter.get('/node/:nodeId/stacks', authMiddleware, async (req: Request, res: Response): Promise<void> => {
if (!requirePaid(req, res)) return;
try {
const nodeId = parseIntParam(req, res, 'nodeId', 'node ID');
if (nodeId === null) return;
@@ -635,8 +633,6 @@ fleetRouter.get('/node/:nodeId/stacks', authMiddleware, async (req: Request, res
});
fleetRouter.get('/node/:nodeId/stacks/:stackName/containers', authMiddleware, async (req: Request, res: Response): Promise<void> => {
if (!requirePaid(req, res)) return;
try {
const nodeId = parseIntParam(req, res, 'nodeId', 'node ID');
if (nodeId === null) return;
+2 -2
View File
@@ -1888,7 +1888,7 @@ paths:
operationId: getFleetNodeStacks
tags: [Fleet]
summary: List stacks on a fleet node
description: Returns stack names from a specific fleet node. Requires Skipper or Admiral license.
description: Returns stack names from a specific fleet node.
parameters:
- name: nodeId
in: path
@@ -1928,7 +1928,7 @@ paths:
operationId: getFleetNodeStackContainers
tags: [Fleet]
summary: List containers in a fleet node stack
description: Returns containers for a specific stack on a specific fleet node. Requires Skipper or Admiral license.
description: Returns containers for a specific stack on a specific fleet node.
parameters:
- name: nodeId
in: path