Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 889504962d | |||
| 7b51df5ce5 | |||
| a3b860c1f5 | |||
| 4efe88cf6e | |||
| 0ded85fe23 | |||
| b08df4fb76 | |||
| 905283f134 | |||
| 9c62405a0c | |||
| 175310c4a6 | |||
| 329d030c1a |
+4
-4
@@ -1,13 +1,13 @@
|
|||||||
# Artifact Hub package metadata file
|
# Artifact Hub package metadata file
|
||||||
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml
|
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml
|
||||||
version: 0.2.8
|
version: 0.2.13
|
||||||
name: headlamp-sealed-secrets
|
name: headlamp-sealed-secrets
|
||||||
displayName: Sealed Secrets Plugin for Headlamp
|
displayName: Sealed Secrets Plugin for Headlamp
|
||||||
createdAt: "2026-02-12T00:00:00Z"
|
createdAt: "2026-02-12T00:00:00Z"
|
||||||
description: A comprehensive Headlamp plugin for managing Bitnami Sealed Secrets with client-side encryption and RBAC-aware UI
|
description: A comprehensive Headlamp plugin for managing Bitnami Sealed Secrets with client-side encryption and RBAC-aware UI
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
homeURL: https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin
|
homeURL: https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin
|
||||||
appVersion: 0.2.8
|
appVersion: 0.2.13
|
||||||
containersImages:
|
containersImages:
|
||||||
- name: sealed-secrets-controller
|
- name: sealed-secrets-controller
|
||||||
image: docker.io/bitnami/sealed-secrets-controller:v0.24.0
|
image: docker.io/bitnami/sealed-secrets-controller:v0.24.0
|
||||||
@@ -19,8 +19,8 @@ keywords:
|
|||||||
- encryption
|
- encryption
|
||||||
- security
|
- security
|
||||||
annotations:
|
annotations:
|
||||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin/releases/download/v0.2.8/headlamp-sealed-secrets-0.2.8.tar.gz"
|
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin/releases/download/v0.2.13/headlamp-sealed-secrets-0.2.13.tar.gz"
|
||||||
headlamp/plugin/archive-checksum: sha256:83ba4a09a1b7cfc6ce422f0f62232959d1d4b087cf9f34f4d27241e6c9c838e3
|
headlamp/plugin/archive-checksum: sha256:f9d8038471ae323b8bd351b46886cc9276cf7fba8ebc76858f1842c31fd4ba7c
|
||||||
headlamp/plugin/version-compat: ">=0.13.0"
|
headlamp/plugin/version-compat: ">=0.13.0"
|
||||||
headlamp/plugin/distro-compat: "desktop,in-cluster,web,docker-desktop"
|
headlamp/plugin/distro-compat: "desktop,in-cluster,web,docker-desktop"
|
||||||
links:
|
links:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "headlamp-sealed-secrets",
|
"name": "headlamp-sealed-secrets",
|
||||||
"version": "0.2.8",
|
"version": "0.2.13",
|
||||||
"description": "Headlamp plugin for Bitnami Sealed Secrets - manage encrypted Kubernetes secrets",
|
"description": "Headlamp plugin for Bitnami Sealed Secrets - manage encrypted Kubernetes secrets",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ abstract class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoun
|
|||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
console.error('Error caught by boundary:', error, errorInfo);
|
console.error('Error caught by boundary:', error, errorInfo);
|
||||||
|
console.error('Error type:', typeof error);
|
||||||
|
console.error('Error keys:', Object.keys(error));
|
||||||
|
console.error('Error message:', error.message);
|
||||||
|
console.error('Error toString:', String(error));
|
||||||
this.setState({ errorInfo });
|
this.setState({ errorInfo });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +99,14 @@ export class CryptoErrorBoundary extends BaseErrorBoundary {
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
|
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||||||
>
|
>
|
||||||
Error: {this.state.error.message}
|
{(() => {
|
||||||
|
try {
|
||||||
|
const msg = this.state.error.message || this.state.error.toString();
|
||||||
|
return `Error: ${String(msg)}`;
|
||||||
|
} catch (e) {
|
||||||
|
return 'Error: [Unable to display error message]';
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -143,7 +154,14 @@ export class ApiErrorBoundary extends BaseErrorBoundary {
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
|
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||||||
>
|
>
|
||||||
Error: {this.state.error.message}
|
{(() => {
|
||||||
|
try {
|
||||||
|
const msg = this.state.error.message || this.state.error.toString();
|
||||||
|
return `Error: ${String(msg)}`;
|
||||||
|
} catch (e) {
|
||||||
|
return 'Error: [Unable to display error message]';
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -182,7 +200,14 @@ export class GenericErrorBoundary extends BaseErrorBoundary {
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
|
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||||||
>
|
>
|
||||||
Error: {this.state.error.message}
|
{(() => {
|
||||||
|
try {
|
||||||
|
const msg = this.state.error.message || this.state.error.toString();
|
||||||
|
return `Error: ${String(msg)}`;
|
||||||
|
} catch (e) {
|
||||||
|
return 'Error: [Unable to display error message]';
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* encrypted data, template, resulting Secret, and actions
|
* encrypted data, template, resulting Secret, and actions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||||
import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||||
import {
|
import {
|
||||||
@@ -13,7 +14,18 @@ import {
|
|||||||
SimpleTable,
|
SimpleTable,
|
||||||
StatusLabel,
|
StatusLabel,
|
||||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||||
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Drawer,
|
||||||
|
IconButton,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@@ -46,7 +58,7 @@ function formatScope(scope: SealedSecretScope): string {
|
|||||||
*/
|
*/
|
||||||
export function SealedSecretDetail() {
|
export function SealedSecretDetail() {
|
||||||
const { namespace, name } = useParams<{ namespace: string; name: string }>();
|
const { namespace, name } = useParams<{ namespace: string; name: string }>();
|
||||||
const [sealedSecret] = SealedSecret.useGet(name, namespace);
|
const [sealedSecret, error] = SealedSecret.useGet(name, namespace);
|
||||||
const [secret] = K8s.ResourceClasses.Secret.useGet(name, namespace);
|
const [secret] = K8s.ResourceClasses.Secret.useGet(name, namespace);
|
||||||
const [decryptKey, setDecryptKey] = React.useState<string | null>(null);
|
const [decryptKey, setDecryptKey] = React.useState<string | null>(null);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
|
||||||
@@ -62,6 +74,22 @@ export function SealedSecretDetail() {
|
|||||||
}
|
}
|
||||||
}, [namespace]);
|
}, [namespace]);
|
||||||
|
|
||||||
|
// Show error if fetch failed
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Box p={3}>
|
||||||
|
<Alert severity="error">
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Failed to load SealedSecret
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{String(error)}
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Show loading skeleton while data is being fetched
|
// Show loading skeleton while data is being fetched
|
||||||
if (!sealedSecret) {
|
if (!sealedSecret) {
|
||||||
return <SealedSecretDetailSkeleton />;
|
return <SealedSecretDetailSkeleton />;
|
||||||
@@ -94,16 +122,41 @@ export function SealedSecretDetail() {
|
|||||||
}
|
}
|
||||||
}, [sealedSecret, enqueueSnackbar]);
|
}, [sealedSecret, enqueueSnackbar]);
|
||||||
|
|
||||||
const encryptedKeys = Object.keys(sealedSecret.spec.encryptedData || {});
|
// Safety check - should never happen due to early returns above, but be defensive
|
||||||
|
if (!sealedSecret?.spec?.encryptedData) {
|
||||||
|
return <SealedSecretDetailSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedKeys = Object.keys(sealedSecret.spec.encryptedData);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
window.history.back();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Drawer
|
||||||
<SectionBox
|
anchor="right"
|
||||||
title={
|
open
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
onClose={handleClose}
|
||||||
<span>{sealedSecret.metadata.name}</span>
|
PaperProps={{
|
||||||
<Box>
|
sx: {
|
||||||
|
width: { xs: '100%', sm: '600px', md: '800px' },
|
||||||
|
maxWidth: '100%',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<SectionBox
|
||||||
|
title={
|
||||||
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||||
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
<IconButton onClick={handleClose} edge="start" size="small">
|
||||||
|
<Icon icon="mdi:close" />
|
||||||
|
</IconButton>
|
||||||
|
<span>{sealedSecret.metadata.name}</span>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
{permissions?.canUpdate && (
|
{permissions?.canUpdate && (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -131,15 +184,15 @@ export function SealedSecretDetail() {
|
|||||||
rows={[
|
rows={[
|
||||||
{
|
{
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
value: sealedSecret.metadata.name,
|
value: String(sealedSecret.metadata.name || ''),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Namespace',
|
name: 'Namespace',
|
||||||
value: sealedSecret.metadata.namespace,
|
value: String(sealedSecret.metadata.namespace || ''),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Scope',
|
name: 'Scope',
|
||||||
value: formatScope(sealedSecret.scope),
|
value: String(formatScope(sealedSecret.scope)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sync Status',
|
name: 'Sync Status',
|
||||||
@@ -151,16 +204,18 @@ export function SealedSecretDetail() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Status Message',
|
name: 'Status Message',
|
||||||
value: sealedSecret.syncMessage,
|
value: String(sealedSecret.syncMessage || 'Unknown'),
|
||||||
hide: !sealedSecret.syncCondition,
|
hide: !sealedSecret.syncCondition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Age',
|
name: 'Age',
|
||||||
value: sealedSecret.getAge(),
|
value: String(sealedSecret.getAge() || ''),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Created',
|
name: 'Created',
|
||||||
value: new Date(sealedSecret.metadata.creationTimestamp!).toLocaleString(),
|
value: sealedSecret.metadata.creationTimestamp
|
||||||
|
? new Date(sealedSecret.metadata.creationTimestamp).toLocaleString()
|
||||||
|
: 'Unknown',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -207,16 +262,18 @@ export function SealedSecretDetail() {
|
|||||||
rows={[
|
rows={[
|
||||||
{
|
{
|
||||||
name: 'Secret Type',
|
name: 'Secret Type',
|
||||||
value: sealedSecret.spec.template.type || 'Opaque',
|
value: String(sealedSecret.spec.template.type || 'Opaque'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Labels',
|
name: 'Labels',
|
||||||
value: JSON.stringify(sealedSecret.spec.template.metadata?.labels || {}),
|
value: String(JSON.stringify(sealedSecret.spec.template.metadata?.labels || {})),
|
||||||
hide: !sealedSecret.spec.template.metadata?.labels,
|
hide: !sealedSecret.spec.template.metadata?.labels,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Annotations',
|
name: 'Annotations',
|
||||||
value: JSON.stringify(sealedSecret.spec.template.metadata?.annotations || {}),
|
value: String(
|
||||||
|
JSON.stringify(sealedSecret.spec.template.metadata?.annotations || {})
|
||||||
|
),
|
||||||
hide: !sealedSecret.spec.template.metadata?.annotations,
|
hide: !sealedSecret.spec.template.metadata?.annotations,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -234,7 +291,7 @@ export function SealedSecretDetail() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Keys',
|
name: 'Keys',
|
||||||
value: Object.keys(secret.data || {}).join(', '),
|
value: String(Object.keys(secret.data || {}).join(', ') || 'None'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Link',
|
name: 'Link',
|
||||||
@@ -242,8 +299,8 @@ export function SealedSecretDetail() {
|
|||||||
<Link
|
<Link
|
||||||
routeName="secret"
|
routeName="secret"
|
||||||
params={{
|
params={{
|
||||||
namespace: secret.metadata.namespace,
|
namespace: String(secret.metadata.namespace || ''),
|
||||||
name: secret.metadata.name,
|
name: String(secret.metadata.name || ''),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View Secret
|
View Secret
|
||||||
@@ -259,29 +316,30 @@ export function SealedSecretDetail() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</SectionBox>
|
</SectionBox>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{decryptKey && (
|
{decryptKey && (
|
||||||
<DecryptDialog
|
<DecryptDialog
|
||||||
sealedSecret={sealedSecret}
|
sealedSecret={sealedSecret}
|
||||||
secretKey={decryptKey}
|
secretKey={decryptKey}
|
||||||
onClose={() => setDecryptKey(null)}
|
onClose={() => setDecryptKey(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
|
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
|
||||||
<DialogTitle>Delete SealedSecret?</DialogTitle>
|
<DialogTitle>Delete SealedSecret?</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
Are you sure you want to delete the SealedSecret <strong>{name}</strong>? This will also
|
Are you sure you want to delete the SealedSecret <strong>{name}</strong>? This will also
|
||||||
delete the resulting Kubernetes Secret.
|
delete the resulting Kubernetes Secret.
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
|
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
|
||||||
<Button onClick={handleDelete} color="error">
|
<Button onClick={handleDelete} color="error">
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import {
|
|||||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||||
import { Box, Button } from '@mui/material';
|
import { Box, Button } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import { usePermission } from '../hooks/usePermissions';
|
import { usePermission } from '../hooks/usePermissions';
|
||||||
import { SealedSecret } from '../lib/SealedSecretCRD';
|
import { SealedSecret } from '../lib/SealedSecretCRD';
|
||||||
import { SealedSecretScope } from '../types';
|
import { SealedSecretScope } from '../types';
|
||||||
import { EncryptDialog } from './EncryptDialog';
|
import { EncryptDialog } from './EncryptDialog';
|
||||||
import { SealedSecretListSkeleton } from './LoadingSkeletons';
|
import { SealedSecretListSkeleton } from './LoadingSkeletons';
|
||||||
|
import { SealedSecretDetail } from './SealedSecretDetail';
|
||||||
import { VersionWarning } from './VersionWarning';
|
import { VersionWarning } from './VersionWarning';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +42,7 @@ function formatScope(scope: SealedSecretScope): string {
|
|||||||
* SealedSecrets list view component
|
* SealedSecrets list view component
|
||||||
*/
|
*/
|
||||||
export function SealedSecretList() {
|
export function SealedSecretList() {
|
||||||
|
const { namespace, name } = useParams<{ namespace?: string; name?: string }>();
|
||||||
const [sealedSecrets, error, loading] = SealedSecret.useList();
|
const [sealedSecrets, error, loading] = SealedSecret.useList();
|
||||||
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
||||||
const { allowed: canCreate } = usePermission(undefined, 'canCreate');
|
const { allowed: canCreate } = usePermission(undefined, 'canCreate');
|
||||||
@@ -159,6 +162,8 @@ export function SealedSecretList() {
|
|||||||
</SectionBox>
|
</SectionBox>
|
||||||
|
|
||||||
<EncryptDialog open={createDialogOpen} onClose={handleCloseDialog} />
|
<EncryptDialog open={createDialogOpen} onClose={handleCloseDialog} />
|
||||||
|
|
||||||
|
{namespace && name && <SealedSecretDetail />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,14 +32,24 @@ export function VersionWarning({ autoDetect = true, showDetails = false }: Versi
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const result = await SealedSecret.detectApiVersion();
|
try {
|
||||||
|
const result = await SealedSecret.detectApiVersion();
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
setDetectedVersion(result.value);
|
setDetectedVersion(result.value);
|
||||||
setError(null);
|
setError(null);
|
||||||
} else if (result.ok === false) {
|
} else if (result.ok === false) {
|
||||||
|
setDetectedVersion(null);
|
||||||
|
// Ensure error is always a string
|
||||||
|
const errorMessage = typeof result.error === 'string'
|
||||||
|
? result.error
|
||||||
|
: String(result.error);
|
||||||
|
setError(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Catch any unexpected errors
|
||||||
setDetectedVersion(null);
|
setDetectedVersion(null);
|
||||||
setError(result.error);
|
setError(e instanceof Error ? e.message : String(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -67,8 +77,8 @@ export function VersionWarning({ autoDetect = true, showDetails = false }: Versi
|
|||||||
}>
|
}>
|
||||||
<strong>API Version Detection Failed</strong>
|
<strong>API Version Detection Failed</strong>
|
||||||
<br />
|
<br />
|
||||||
{error}
|
{String(error)}
|
||||||
{error.includes('not found') && (
|
{String(error).includes('not found') && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
registerDetailsViewSection,
|
registerDetailsViewSection,
|
||||||
|
registerPluginSettings,
|
||||||
registerRoute,
|
registerRoute,
|
||||||
registerSidebarEntry,
|
registerSidebarEntry,
|
||||||
} from '@kinvolk/headlamp-plugin/lib';
|
} from '@kinvolk/headlamp-plugin/lib';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ApiErrorBoundary, GenericErrorBoundary } from './components/ErrorBoundary';
|
import { ApiErrorBoundary, GenericErrorBoundary } from './components/ErrorBoundary';
|
||||||
import { SealedSecretDetail } from './components/SealedSecretDetail';
|
|
||||||
import { SealedSecretList } from './components/SealedSecretList';
|
import { SealedSecretList } from './components/SealedSecretList';
|
||||||
import { SealingKeysView } from './components/SealingKeysView';
|
import { SealingKeysView } from './components/SealingKeysView';
|
||||||
import { SecretDetailsSection } from './components/SecretDetailsSection';
|
import { SecretDetailsSection } from './components/SecretDetailsSection';
|
||||||
@@ -56,21 +56,13 @@ registerSidebarEntry({
|
|||||||
url: '/sealedsecrets/keys',
|
url: '/sealedsecrets/keys',
|
||||||
});
|
});
|
||||||
|
|
||||||
// "Settings" child entry
|
|
||||||
registerSidebarEntry({
|
|
||||||
parent: 'sealed-secrets',
|
|
||||||
name: 'sealed-secrets-settings',
|
|
||||||
label: 'Settings',
|
|
||||||
url: '/sealedsecrets/settings',
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register routes
|
* Register routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// List view
|
// List view with optional detail drawer
|
||||||
registerRoute({
|
registerRoute({
|
||||||
path: '/sealedsecrets',
|
path: '/sealedsecrets/:namespace?/:name?',
|
||||||
sidebar: 'sealed-secrets-list',
|
sidebar: 'sealed-secrets-list',
|
||||||
component: () => (
|
component: () => (
|
||||||
<ApiErrorBoundary>
|
<ApiErrorBoundary>
|
||||||
@@ -78,18 +70,6 @@ registerRoute({
|
|||||||
</ApiErrorBoundary>
|
</ApiErrorBoundary>
|
||||||
),
|
),
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
|
||||||
|
|
||||||
// Detail view
|
|
||||||
registerRoute({
|
|
||||||
path: '/sealedsecrets/:namespace/:name',
|
|
||||||
sidebar: 'sealed-secrets-list',
|
|
||||||
component: () => (
|
|
||||||
<ApiErrorBoundary>
|
|
||||||
<SealedSecretDetail />
|
|
||||||
</ApiErrorBoundary>
|
|
||||||
),
|
|
||||||
exact: true,
|
|
||||||
name: 'sealedsecret',
|
name: 'sealedsecret',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,18 +85,6 @@ registerRoute({
|
|||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Settings page
|
|
||||||
registerRoute({
|
|
||||||
path: '/sealedsecrets/settings',
|
|
||||||
sidebar: 'sealed-secrets-settings',
|
|
||||||
component: () => (
|
|
||||||
<GenericErrorBoundary>
|
|
||||||
<SettingsPage />
|
|
||||||
</GenericErrorBoundary>
|
|
||||||
),
|
|
||||||
exact: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register integration with Secret detail view
|
* Register integration with Secret detail view
|
||||||
*
|
*
|
||||||
@@ -132,3 +100,18 @@ registerDetailsViewSection(({ resource }) => {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register plugin settings
|
||||||
|
*
|
||||||
|
* Settings will appear in Settings → Plugins → Sealed Secrets
|
||||||
|
*/
|
||||||
|
registerPluginSettings(
|
||||||
|
'Sealed Secrets',
|
||||||
|
() => (
|
||||||
|
<GenericErrorBoundary>
|
||||||
|
<SettingsPage />
|
||||||
|
</GenericErrorBoundary>
|
||||||
|
),
|
||||||
|
true // Display save button
|
||||||
|
);
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export class SealedSecret extends KubeObject<SealedSecretInterface> {
|
|||||||
if (!condition) {
|
if (!condition) {
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
return condition.message || condition.reason || condition.status;
|
// Ensure we always return a string, not an object
|
||||||
|
const message = condition.message || condition.reason || condition.status;
|
||||||
|
return String(message || 'Unknown');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,20 +122,11 @@ export class SealedSecret extends KubeObject<SealedSecretInterface> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await tryCatchAsync(async () => {
|
const result = await tryCatchAsync(async () => {
|
||||||
// Query the CRD to get available versions
|
// Query the CRD to get available versions using Headlamp's API proxy
|
||||||
const response = await fetch(
|
const crd = await ApiProxy.request(
|
||||||
'/apis/apiextensions.k8s.io/v1/customresourcedefinitions/sealedsecrets.bitnami.com'
|
'/apis/apiextensions.k8s.io/v1/customresourcedefinitions/sealedsecrets.bitnami.com'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 404) {
|
|
||||||
throw new Error('SealedSecrets CRD not found. Please install Sealed Secrets on the cluster.');
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to fetch CRD: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const crd = await response.json();
|
|
||||||
|
|
||||||
// Find the storage version (the version used for persistence)
|
// Find the storage version (the version used for persistence)
|
||||||
const storageVersion = crd.spec?.versions?.find((v: any) => v.storage === true);
|
const storageVersion = crd.spec?.versions?.find((v: any) => v.storage === true);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user