mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat(fleet): move bulk Remote OTA updates to Community tier (#1151)
Drops `requirePaid` from `POST /api/fleet/update-all` so Community admins can dispatch bulk node updates. Per-node OTA was already Community- reachable (admin-only); this completes the move so the full Remote OTA surface ships at Community. Frontend mirrors the backend: removes `canBulkUpdate` from NodeUpdatesSheet so the "Update all (N)" affordance is purely data- driven on `updatableRemoteCount > 0`. Docs realigned to drop fence-spec and Skipper-only phrasing on the Update all bulk action: - features/licensing.mdx: Community line now lists Remote OTA (per-node and Update all); Skipper Fleet Actions parenthetical drops "bulk update all" - features/remote-updates.mdx: Note rewritten to role-only requirement - features/fleet-view.mdx: Update all (n) bullet drops the tier clause - features/overview.mdx: Fleet View and Remote updates blurbs drop the Skipper/Admiral fences - operations/upgrade.mdx: Note rephrased without naming tiers Test coverage: - fleet.test.ts: tier-gating spec flipped to assert Community access - fleet-pilot-update.test.ts: bulk-OTA dispatch suite now spies tier to Community so it doubles as a regression guard
This commit is contained in:
@@ -184,15 +184,15 @@ describe('POST /api/fleet/nodes/:nodeId/update (pilot-agent)', () => {
|
||||
});
|
||||
|
||||
describe('POST /api/fleet/update-all (pilot-agent mixed fleet)', () => {
|
||||
// /update-all is requirePaid; spy the license tier so the test DB does not
|
||||
// need a real activation row.
|
||||
function mockPaidTier() {
|
||||
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid');
|
||||
vi.spyOn(LicenseService.getInstance(), 'getVariant').mockReturnValue('admiral');
|
||||
// Bulk OTA is admin-only and runs at every tier; spy the tier to Community
|
||||
// so this suite doubles as a regression guard that the gate has not been
|
||||
// re-introduced.
|
||||
function mockCommunityTier() {
|
||||
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('community');
|
||||
}
|
||||
|
||||
it('includes the pilot node in the candidate set and dispatches through its target', async () => {
|
||||
mockPaidTier();
|
||||
mockCommunityTier();
|
||||
mockTargetForPilot();
|
||||
mockMeta(META_ONLINE_OUTDATED);
|
||||
const postedUrls: string[] = [];
|
||||
@@ -213,7 +213,7 @@ describe('POST /api/fleet/update-all (pilot-agent mixed fleet)', () => {
|
||||
});
|
||||
|
||||
it('skips remotes whose target resolves to null and never calls /api/system/update on them', async () => {
|
||||
mockPaidTier();
|
||||
mockCommunityTier();
|
||||
mockTargetUnreachable();
|
||||
// /update-all also calls api.github.com to compute the compare target;
|
||||
// pin the assertion to the route's own dispatch surface.
|
||||
|
||||
@@ -157,13 +157,12 @@ describe('GET /api/fleet/overview', () => {
|
||||
describe('Fleet tier gating', () => {
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('POST /api/fleet/update-all returns 403 on community tier (bulk update is Skipper+)', async () => {
|
||||
it('POST /api/fleet/update-all is accessible on community tier', async () => {
|
||||
mockTier('community');
|
||||
const res = await request(app)
|
||||
.post('/api/fleet/update-all')
|
||||
.set('Authorization', authHeader);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('PAID_REQUIRED');
|
||||
expect(res.body.code).not.toBe('PAID_REQUIRED');
|
||||
});
|
||||
|
||||
it('GET /api/fleet/update-status is accessible on community tier', async () => {
|
||||
|
||||
@@ -937,7 +937,6 @@ fleetRouter.post('/nodes/:nodeId/update', authMiddleware, async (req: Request, r
|
||||
});
|
||||
|
||||
fleetRouter.post('/update-all', authMiddleware, async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requirePaid(req, res)) return;
|
||||
if (!requireAdmin(req, res)) return;
|
||||
try {
|
||||
const db = DatabaseService.getInstance();
|
||||
|
||||
@@ -215,7 +215,7 @@ The sheet has four sections from top to bottom.
|
||||
### Header actions
|
||||
|
||||
- **Recheck** re-queries every node's `/api/meta` endpoint and re-resolves the latest available Sencho version from GitHub Releases (with a Docker Hub fallback if the GitHub API is unreachable). The button disables and shows a spinner while the check is in flight.
|
||||
- **Update all (n)** ships only on Skipper or Admiral. The label carries the count of remote nodes with a pending update. Clicking it dispatches the update to every remote with a known current and latest version.
|
||||
- **Update all (n)** carries the count of remote nodes with a pending update. Clicking it dispatches the update to every remote with a known current and latest version.
|
||||
|
||||
### Summary cards
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ For larger deployments, an **Enterprise** tier is available with custom pricing,
|
||||
- Real-time container stats, global logs, the interactive network topology graph, and stack labels
|
||||
- Git sources for compose stacks
|
||||
- Multi-node management in both Proxy and Pilot Agent modes
|
||||
- Manual fleet snapshots (create, browse, restore, delete) and per-node Sencho updates
|
||||
- Manual fleet snapshots (create, browse, restore, delete) and Remote OTA node updates (per-node and **Update all**)
|
||||
- Custom S3-compatible backup target (bring your own AWS S3, Cloudflare R2, MinIO, Backblaze B2, or Wasabi bucket)
|
||||
- Vulnerability scanning: install, update, and uninstall Trivy, on-demand scans for vulnerabilities, secrets, and misconfigurations, plus scan comparison
|
||||
- CVE suppressions
|
||||
@@ -49,8 +49,7 @@ For larger deployments, an **Enterprise** tier is available with custom pricing,
|
||||
- Scan policies with `block_on_deploy` deploy enforcement, SBOM (SPDX, CycloneDX), and SARIF export
|
||||
- Auto-update of the managed Trivy binary
|
||||
- Bulk actions on a label (deploy, stop, or restart every stack tagged with it)
|
||||
- Remote OTA node updates from the control instance
|
||||
- Fleet Actions (bulk update all, bulk restart Sencho)
|
||||
- Fleet Actions (bulk restart Sencho)
|
||||
- Blueprints and Fleet Secrets
|
||||
- Preset SSO for Google, GitHub, and Okta
|
||||
- Viewer accounts (1 admin plus 3 viewers)
|
||||
|
||||
@@ -109,7 +109,7 @@ Connect containers across nodes by hostname over the Pilot tunnel so multi-node
|
||||
|
||||
### Fleet View
|
||||
|
||||
Monitor your entire infrastructure from a single screen. The fleet dashboard shows all nodes with health metrics, container counts, and resource usage. Search, sort, filtering, stack drill-down, the Grid / Topology toggle, and critical-node detection are available on every tier; the bulk **Update All** action inside the Node Updates modal is Skipper or Admiral. [Learn more →](/features/fleet-view)
|
||||
Monitor your entire infrastructure from a single screen. The fleet dashboard shows all nodes with health metrics, container counts, and resource usage. Search, sort, filtering, stack drill-down, the Grid / Topology toggle, and critical-node detection are available on every tier. [Learn more →](/features/fleet-view)
|
||||
|
||||
### Fleet Federation
|
||||
|
||||
@@ -133,7 +133,7 @@ Create point-in-time snapshots of every compose file and environment file across
|
||||
|
||||
### Remote updates
|
||||
|
||||
Check for outdated nodes and trigger over-the-air updates from the Fleet View. When the gateway is running a newer version than a remote node, a one-click update pulls the latest image and recreates the container automatically. Per-node updates and the Check Updates view are available on every tier; the bulk **Update All** action is Skipper or Admiral. [Learn more →](/features/remote-updates)
|
||||
Check for outdated nodes and trigger over-the-air updates from the Fleet View. When the gateway is running a newer version than a remote node, a one-click update pulls the latest image and recreates the container automatically. Both per-node updates and the bulk **Update all** action are available on every tier. [Learn more →](/features/remote-updates)
|
||||
|
||||
### Scheduled operations
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Sencho can update any node in the fleet to the latest published release without
|
||||
This page covers the mechanism: prerequisites, what happens on a node during an update, how completion and failure are detected, and how to recover. The full UI tour for the Node updates sheet itself lives in [Fleet View](/features/fleet-view#node-updates).
|
||||
|
||||
<Note>
|
||||
Per-node updates and the Check Updates view are available on every tier. The bulk **Update all** action is a Skipper or Admiral feature. Triggering an update (per-row or bulk) requires the **admin** role; viewer and operator roles can read update status but cannot dispatch updates.
|
||||
Triggering an update (per-row or **Update all**) requires the **admin** role. Viewer and operator roles can read update status but cannot dispatch updates.
|
||||
</Note>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -23,7 +23,7 @@ docker stop sencho && docker rm sencho
|
||||
Sencho will apply any necessary database migrations automatically on startup. No manual steps are required.
|
||||
|
||||
<Note>
|
||||
Skipper and Admiral users can also update remote nodes directly from the Fleet View. See [Remote Updates](/features/remote-updates) for details.
|
||||
Remote nodes can be updated directly from the Fleet View without dropping to a shell. See [Remote Updates](/features/remote-updates) for details.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
@@ -242,7 +242,6 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
|
||||
retryNodeUpdate={updateStatus.retryNodeUpdate}
|
||||
dismissNodeUpdate={updateStatus.dismissNodeUpdate}
|
||||
triggerUpdateAll={updateStatus.triggerUpdateAll}
|
||||
canBulkUpdate={isPaid}
|
||||
/>
|
||||
|
||||
<LocalUpdateConfirmDialog
|
||||
|
||||
@@ -23,13 +23,11 @@ interface NodeUpdatesSheetProps {
|
||||
retryNodeUpdate: (nodeId: number) => void;
|
||||
dismissNodeUpdate: (nodeId: number) => void;
|
||||
triggerUpdateAll: () => Promise<void>;
|
||||
canBulkUpdate: boolean;
|
||||
}
|
||||
|
||||
export function NodeUpdatesSheet({
|
||||
open, onOpenChange, checkingUpdates, updateStatuses, updatingNodeId,
|
||||
fetchUpdateStatus, triggerNodeUpdate, retryNodeUpdate, dismissNodeUpdate, triggerUpdateAll,
|
||||
canBulkUpdate,
|
||||
}: NodeUpdatesSheetProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [recheckingUpdates, setRecheckingUpdates] = useState(false);
|
||||
@@ -71,7 +69,7 @@ export function NodeUpdatesSheet({
|
||||
? undefined
|
||||
: (gatewayLabel ? `Latest version ${gatewayLabel}` : `${available} update${available === 1 ? '' : 's'} available`);
|
||||
|
||||
const secondaryActions = canBulkUpdate && updatableRemoteCount > 0
|
||||
const secondaryActions = updatableRemoteCount > 0
|
||||
? [{
|
||||
label: `Update all (${updatableRemoteCount})`,
|
||||
icon: Download,
|
||||
|
||||
Reference in New Issue
Block a user