Files
headlamp-sealed-secrets-plugin/src/components/SealingKeysView.tsx
T
DevContainer User 9d9bc5f22f fix: remove any types, dead code, unused exports; add comprehensive tests
- Fix handleRotate bug ignoring Result from rotateSealedSecret()
- Fix dead code branch in useControllerHealth
- Replace all `any` types with `unknown` + type guards
- Delete unused functions/exports (452 lines removed)
- Add 18 new test files covering all hooks, libs, and components
- 233 tests passing, zero tsc errors, zero lint issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:13:00 +00:00

214 lines
6.8 KiB
TypeScript

/**
* Sealing Keys Management View
*
* Lists all sealing key pairs (TLS Secrets) used by the controller
*/
import { K8s } from '@kinvolk/headlamp-plugin/lib';
import {
SectionBox,
SimpleTable,
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { Box, Button, Chip } from '@mui/material';
import { useSnackbar } from 'notistack';
import React from 'react';
import { fetchPublicCertificate, getPluginConfig } from '../lib/controller';
import { isCertificateExpiringSoon, parseCertificateInfo } from '../lib/crypto';
import { CertificateInfo, PEMCertificate } from '../types';
import { ControllerStatus } from './ControllerStatus';
import { SealingKeysListSkeleton } from './LoadingSkeletons';
interface SealingKey {
name: string;
status: 'active' | 'compromised';
created: string;
certInfo?: CertificateInfo;
}
/**
* Sealing keys management view
*/
export function SealingKeysView() {
const config = getPluginConfig();
const [secrets, , loading] = K8s.ResourceClasses.Secret.useList({
namespace: config.controllerNamespace,
});
const { enqueueSnackbar } = useSnackbar();
// Filter for sealing key secrets
const sealingKeys: SealingKey[] = React.useMemo(() => {
if (!secrets) return [];
return secrets
.filter(secret => {
const labelValue = secret.metadata.labels?.['sealedsecrets.bitnami.com/sealed-secrets-key'];
return labelValue === 'active' || labelValue === 'compromised';
})
.map(secret => {
const status = secret.metadata.labels?.['sealedsecrets.bitnami.com/sealed-secrets-key'] as
| 'active'
| 'compromised';
const certPem = secret.data?.['tls.crt'] ? atob(secret.data['tls.crt']) : '';
let certInfo: CertificateInfo | undefined;
if (certPem) {
const infoResult = parseCertificateInfo(PEMCertificate(certPem));
if (infoResult.ok) {
certInfo = infoResult.value;
}
}
return {
name: secret.metadata.name!,
status,
created: secret.metadata.creationTimestamp!,
certInfo,
};
})
.sort((a, b) => {
// Sort active keys first, then by creation date (newest first)
if (a.status !== b.status) {
return a.status === 'active' ? -1 : 1;
}
return new Date(b.created).getTime() - new Date(a.created).getTime();
});
}, [secrets]);
const handleDownloadCert = async () => {
const result = await fetchPublicCertificate(config);
if (result.ok === false) {
enqueueSnackbar(`Failed to download certificate: ${result.error}`, { variant: 'error' });
return;
}
try {
const blob = new Blob([result.value], { type: 'application/x-pem-file' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'sealed-secrets-cert.pem';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
enqueueSnackbar('Certificate downloaded', { variant: 'success' });
} catch (error: unknown) {
enqueueSnackbar(
`Failed to create download: ${error instanceof Error ? error.message : String(error)}`,
{ variant: 'error' }
);
}
};
// Show loading skeleton while data is being fetched
if (loading) {
return (
<SectionBox title="Sealing Keys">
<SealingKeysListSkeleton />
</SectionBox>
);
}
return (
<SectionBox
title="Sealing Keys"
headerProps={{
actions: [
<ControllerStatus key="status" autoRefresh refreshIntervalMs={60000} showDetails />,
<Button key="download" variant="contained" onClick={handleDownloadCert}>
Download Public Certificate
</Button>,
],
}}
>
{sealingKeys.length === 0 ? (
<Box p={2}>
<p>No sealing keys found in namespace {config.controllerNamespace}.</p>
<p>
Ensure the Sealed Secrets controller is installed and running in the{' '}
<strong>{config.controllerNamespace}</strong> namespace.
</p>
</Box>
) : (
<>
<Box p={2}>
<p>
Sealing keys are TLS key pairs used by the controller to decrypt SealedSecrets. The
active key is used for new encryptions. Old keys are kept to decrypt existing
SealedSecrets.
</p>
</Box>
<SimpleTable
data={sealingKeys}
columns={[
{
label: 'Key Name',
getter: (key: SealingKey) => key.name,
},
{
label: 'Status',
getter: (key: SealingKey) => (
<StatusLabel status={key.status === 'active' ? 'success' : 'warning'}>
{key.status === 'active' ? 'Active' : 'Compromised'}
</StatusLabel>
),
},
{
label: 'Created',
getter: (key: SealingKey) => new Date(key.created).toLocaleString(),
},
{
label: 'Certificate Expiry',
getter: (key: SealingKey) => {
if (!key.certInfo) return 'N/A';
const { certInfo } = key;
const expiryDate = certInfo.validTo.toLocaleDateString();
if (certInfo.isExpired) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip label="Expired" color="error" size="small" />
<span>{expiryDate}</span>
</Box>
);
}
if (isCertificateExpiringSoon(certInfo, 30)) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
label={`${certInfo.daysUntilExpiry} days left`}
color="warning"
size="small"
/>
<span>{expiryDate}</span>
</Box>
);
}
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span>{expiryDate}</span>
<span
style={{
color: 'var(--mui-palette-text-secondary, #666)',
fontSize: '0.9em',
}}
>
({certInfo.daysUntilExpiry} days)
</span>
</Box>
);
},
},
]}
/>
</>
)}
</SectionBox>
);
}