mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 17:57:06 +00:00
feat(fleet): open Fleet Actions tab to Community (admin-only) (#1153)
* feat(fleet): open Fleet Actions tab to Community (admin-only) Removes the requirePaid guard from the five Fleet Actions endpoints (fleet-stop, fleet-prune, match-preview, prune/estimate, bulk-assign) and drops the matching isPaid parent gate on FleetActionsTab so Community admins can run fleet-wide bulk operations. requireAdmin stays on every endpoint; operator and viewer roles still 403 on apply. Tests flipped from "403 PAID_REQUIRED on community" to positive "reachable on community + admin" assertions. Docs (fleet-actions, fleet-view, licensing, overview, stack-labels) rewritten to state the admin-role requirement once and drop the prior Skipper framing. * fix(fleet): apply audit findings from PR #1153 review - stack-labels.mdx: fix the page intro that still framed fleet label actions as "Operators on a Skipper or Admiral license". The cards are now Community + admin, so the intro reads "Admins also get a pair of fleet-wide actions". - Collapse redundant role-rule statements on the two affected pages. fleet-actions.mdx now states the admin gate once in the lead-in Note and again only in the troubleshooting accordion (the Prerequisites row was duplicative). stack-labels.mdx trims the "Limits and rules" bullet to the value-add half (label authoring is open to every role) and drops the Fleet Actions repetition. - Strip now-no-op mockTier('paid') calls from non-tier tests across the three fleet test files, plus the test-wide default in the fleet-action-card-endpoints beforeEach. Those mocks were misleading after the routes stopped consulting tier; if a future change re-adds requirePaid the tests will fail loudly instead of silently passing.
This commit is contained in:
@@ -1028,9 +1028,8 @@ fleetRouter.delete('/update-status', authMiddleware, async (req: Request, res: R
|
||||
|
||||
// Fleet-wide stop by label name. Matches each node's labels by name and runs
|
||||
// container stops on each matching stack.
|
||||
// Tier: requirePaid + requireAdmin.
|
||||
// Tier: requireAdmin (admin-only fleet plumbing; available on every license).
|
||||
fleetRouter.post('/labels/fleet-stop', authMiddleware, async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requirePaid(req, res)) return;
|
||||
if (!requireAdmin(req, res)) return;
|
||||
const body = req.body as { labelName?: unknown; dryRun?: unknown } | undefined;
|
||||
if (!body || typeof body !== 'object') {
|
||||
@@ -1142,12 +1141,11 @@ fleetRouter.post('/labels/fleet-stop', authMiddleware, async (req: Request, res:
|
||||
// systemMaintenance.ts is safe because Docker's prune API is internally
|
||||
// serialized and idempotent (the worst case is a duplicate call returning 0
|
||||
// reclaimed bytes).
|
||||
// Tier: requirePaid + requireAdmin.
|
||||
// Tier: requireAdmin (admin-only fleet plumbing; available on every license).
|
||||
const FLEET_PRUNE_TARGETS = ['images', 'volumes', 'networks'] as const;
|
||||
type FleetPruneTarget = (typeof FLEET_PRUNE_TARGETS)[number];
|
||||
|
||||
fleetRouter.post('/labels/fleet-prune', authMiddleware, async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requirePaid(req, res)) return;
|
||||
if (!requireAdmin(req, res)) return;
|
||||
|
||||
const body = req.body as { targets?: unknown; scope?: unknown; dryRun?: unknown } | undefined;
|
||||
@@ -1295,7 +1293,6 @@ fleetRouter.post('/labels/fleet-prune', authMiddleware, async (req: Request, res
|
||||
// assignments live in the central DB even for remote nodes, populated by the
|
||||
// nodes' own UIs and synced via Distributed API.
|
||||
fleetRouter.post('/labels/match-preview', authMiddleware, async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requirePaid(req, res)) return;
|
||||
if (!requireAdmin(req, res)) return;
|
||||
const body = req.body as { labelName?: unknown } | undefined;
|
||||
if (!body || typeof body !== 'object') {
|
||||
@@ -1336,7 +1333,6 @@ fleetRouter.post('/labels/match-preview', authMiddleware, async (req: Request, r
|
||||
// fan-out shape as `/labels/fleet-prune` minus the locks (estimation is read
|
||||
// only).
|
||||
fleetRouter.post('/prune/estimate', authMiddleware, async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requirePaid(req, res)) return;
|
||||
if (!requireAdmin(req, res)) return;
|
||||
|
||||
const body = req.body as { targets?: unknown; scope?: unknown } | undefined;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router, type Request, type Response } from 'express';
|
||||
import { DatabaseService } from '../services/DatabaseService';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { requirePaid, requireAdmin, requireBody } from '../middleware/tierGates';
|
||||
import { requireAdmin, requireBody } from '../middleware/tierGates';
|
||||
import { isValidStackName } from '../utils/validation';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
|
||||
@@ -20,14 +20,14 @@ const MAX_ASSIGNMENTS = 1000;
|
||||
// Bulk label assignment for many stacks on a single node. The single-stack
|
||||
// endpoint at `PUT /api/stacks/:stackName/labels` covers one stack at a time;
|
||||
// this wrapper applies the same operation to many stacks atomically per HTTP
|
||||
// request. Tier: requirePaid + requireAdmin. The per-stack endpoint is
|
||||
// Community-tier organization metadata; this multi-stack wrapper is an
|
||||
// automation surface exposed only inside the Skipper+ Fleet Actions tab.
|
||||
// request. Tier: requireAdmin (admin-only fleet plumbing). The per-stack
|
||||
// endpoint is Community-tier organization metadata; this multi-stack wrapper
|
||||
// matches the surrounding Fleet Actions surface, which is admin-only but
|
||||
// available on every license.
|
||||
fleetActionsRouter.post(
|
||||
'/labels/bulk-assign',
|
||||
authMiddleware,
|
||||
async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requirePaid(req, res)) return;
|
||||
if (!requireAdmin(req, res)) return;
|
||||
if (!requireBody(req, res)) return;
|
||||
const { assignments } = req.body as { assignments?: unknown };
|
||||
|
||||
Reference in New Issue
Block a user