feat(fleet-secrets): graduate encrypted fleet-wide environment bundles to Community (#1758)

* feat(fleet-secrets): graduate encrypted fleet-wide environment bundles to Community

* fix(fleet-secrets): update reachability test for Secrets community graduation

* fix(fleet-secrets): address review findings

* fix(fleet-secrets): add HTTP-level Community admin push/import tests and non-admin tab-hidden test
This commit is contained in:
Anso
2026-08-03 08:42:46 -04:00
committed by GitHub
parent 5240957ff3
commit 5f5cd5ea51
11 changed files with 183 additions and 73 deletions
+6 -16
View File
@@ -72,7 +72,6 @@ export function FleetView({
const containerLabelsEnabled = hasCapability('container-label-inventory');
// Visual fail-closed while /meta loads; paid/admin gates still apply when on.
const canDiscoverRouting = experimentalReady && experimental && isPaid;
const canDiscoverSecrets = experimentalReady && experimental && isPaid && isAdmin;
const { prefs, updatePrefs } = useFleetPreferences();
const updateStatus = useFleetUpdateStatus();
@@ -104,9 +103,9 @@ export function FleetView({
if (controlledTab === undefined) setInternalTab(tab);
};
// Fall back only after experimental readiness settles. When experimental is
// on, also wait for license (and admin for secrets) so a paid deep link is
// not rewritten to Overview while isPaid is still the cold-load false.
// Fall back Routing deep links when experimental/license gates resolve false.
// Wait for license during cold load so a paid deep link is not rewritten to
// Overview while isPaid is still the cold-load false.
useEffect(() => {
if (!experimentalReady) return;
if (activeTab === 'routing') {
@@ -116,19 +115,10 @@ export function FleetView({
}
if (licenseStatus !== 'ready') return;
if (!isPaid) setActiveTab('overview');
return;
}
if (activeTab === 'secrets') {
if (!experimental) {
setActiveTab('overview');
return;
}
if (licenseStatus !== 'ready') return;
if (!isPaid || !isAdmin) setActiveTab('overview');
}
// setActiveTab closes over onFleetActiveTabChange; listing deps explicitly.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [experimentalReady, experimental, licenseStatus, isPaid, isAdmin, activeTab]);
}, [experimentalReady, experimental, licenseStatus, isPaid, activeTab]);
useEffect(() => {
if (fleetUpdatesIntent) {
@@ -217,7 +207,7 @@ export function FleetView({
<Wrench className="w-4 h-4 mr-1.5" />Actions
</TabsTrigger>
</TabsHighlightItem>
{canDiscoverSecrets && (
{isAdmin && (
<TabsHighlightItem value="secrets">
<TabsTrigger value="secrets">
<KeyRound className="w-4 h-4 mr-1.5" />Secrets
@@ -347,7 +337,7 @@ export function FleetView({
unfiltered node list rather than the overview-filtered view. */}
<FleetActionsTab nodes={overview.nodes} />
</TabsContent>
{canDiscoverSecrets && (
{isAdmin && (
<TabsContent value="secrets">
<SecretsTab />
</TabsContent>
@@ -8,10 +8,11 @@ vi.mock('@/hooks/useExperimental', () => ({
}));
vi.mock('@/context/LicenseContext', () => ({
useLicense: () => ({ isPaid: true }),
useLicense: () => ({ isPaid: true, licenseStatus: 'ready' as const }),
}));
const useAuthMock = vi.fn(() => ({ isAdmin: true as boolean, can: () => true as boolean }));
vi.mock('@/context/AuthContext', () => ({
useAuth: () => ({ isAdmin: true, can: () => true }),
useAuth: () => useAuthMock(),
}));
vi.mock('@/context/NodeContext', () => ({
useNodes: () => ({ hasCapability: () => false }),
@@ -100,22 +101,23 @@ vi.mock('../fleet/DependencyMapTab', () => ({ DependencyMapTab: () => null }));
vi.mock('../fleet/ContainerLabelsTab', () => ({ ContainerLabelsTab: () => null }));
vi.mock('../PaidGate', () => ({ PaidGate: ({ children }: { children: React.ReactNode }) => <>{children}</> }));
describe('FleetView experimental discovery', () => {
describe('FleetView tab discovery and deep-link fallback', () => {
beforeEach(() => {
useExperimentalMock.mockReturnValue({ experimental: true, experimentalReady: true });
useAuthMock.mockReturnValue({ isAdmin: true, can: () => true });
});
it('shows Routing and Secrets when experimental discovery is on for paid admin', () => {
it('shows Routing when experimental is on; Secrets always visible for Admin', () => {
render(<FleetView onNavigateToNode={vi.fn()} onOpenNodeNetworking={vi.fn()} />);
expect(screen.getByRole('tab', { name: /routing/i })).toBeTruthy();
expect(screen.getByRole('tab', { name: /secrets/i })).toBeTruthy();
});
it('hides Routing and Secrets when experimental discovery is off', () => {
it('hides Routing when experimental off; Secrets stays visible for Admin', () => {
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true });
render(<FleetView onNavigateToNode={vi.fn()} onOpenNodeNetworking={vi.fn()} />);
expect(screen.queryByRole('tab', { name: /routing/i })).toBeNull();
expect(screen.queryByRole('tab', { name: /secrets/i })).toBeNull();
expect(screen.getByRole('tab', { name: /secrets/i })).toBeTruthy();
expect(screen.getByRole('tab', { name: /deployments/i })).toBeTruthy();
expect(screen.getByRole('tab', { name: /federation/i })).toBeTruthy();
expect(screen.getByRole('tab', { name: /actions/i })).toBeTruthy();
@@ -173,4 +175,29 @@ describe('FleetView experimental discovery', () => {
await waitFor(() => expect(onTab).toHaveBeenCalledWith('overview'));
expect(onTab).toHaveBeenCalledTimes(1);
});
it('does not rewrite a Secrets deep link for Admin when experimental is off', () => {
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true });
const onTab = vi.fn();
render(
<FleetView
onNavigateToNode={vi.fn()}
onOpenNodeNetworking={vi.fn()}
fleetActiveTab="secrets"
onFleetActiveTabChange={onTab}
/>,
);
// No fallback rewrite; Secrets is always available to Admin
expect(onTab).not.toHaveBeenCalled();
});
it('hides Secrets tab for non-admin', () => {
useAuthMock.mockReturnValue({ isAdmin: false, can: () => false });
render(<FleetView onNavigateToNode={vi.fn()} onOpenNodeNetworking={vi.fn()} />);
expect(screen.queryByRole('tab', { name: /secrets/i })).toBeNull();
// Routing still visible (gated on experimental + paid, not admin)
expect(screen.getByRole('tab', { name: /routing/i })).toBeTruthy();
// Unrelated tabs still visible
expect(screen.getByRole('tab', { name: /deployments/i })).toBeTruthy();
});
});