Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd5a8c40ee | |||
| 1ec8340a0f | |||
| 2f746486db | |||
| 55b10c5ab2 | |||
| a7761e992b | |||
| 679922e711 | |||
| 248ffa4962 | |||
| 0b082984a7 | |||
| 4da3513015 | |||
| 74af59ef50 | |||
| 67287158fd | |||
| dbc1fb199b | |||
| c63afb1461 | |||
| 3429b32625 | |||
| 5cf360b591 | |||
| 889504962d | |||
| 7b51df5ce5 | |||
| a3b860c1f5 | |||
| 4efe88cf6e | |||
| 0ded85fe23 | |||
| b08df4fb76 | |||
| 905283f134 | |||
| 9c62405a0c |
@@ -24,6 +24,14 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get package name
|
||||||
|
id: package_name
|
||||||
|
working-directory: ./headlamp-sealed-secrets
|
||||||
|
run: |
|
||||||
|
PKG_NAME=$(jq -r '.name' package.json)
|
||||||
|
echo "name=${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Package name: ${PKG_NAME}"
|
||||||
|
|
||||||
- name: Configure git
|
- name: Configure git
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
@@ -38,7 +46,7 @@ jobs:
|
|||||||
- name: Update artifacthub-pkg.yml version
|
- name: Update artifacthub-pkg.yml version
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ inputs.version }}"
|
VERSION="${{ inputs.version }}"
|
||||||
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/headlamp-sealed-secrets-${VERSION}.tar.gz"
|
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${{ steps.package_name.outputs.name }}-${VERSION}.tar.gz"
|
||||||
|
|
||||||
sed -i "s|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml
|
sed -i "s|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml
|
||||||
sed -i "s|^appVersion:.*|appVersion: ${VERSION}|" artifacthub-pkg.yml
|
sed -i "s|^appVersion:.*|appVersion: ${VERSION}|" artifacthub-pkg.yml
|
||||||
@@ -74,7 +82,7 @@ jobs:
|
|||||||
- name: Move tarball to root
|
- name: Move tarball to root
|
||||||
working-directory: ./headlamp-sealed-secrets
|
working-directory: ./headlamp-sealed-secrets
|
||||||
run: |
|
run: |
|
||||||
TARBALL="headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
|
TARBALL="${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
|
||||||
if [ ! -f "${TARBALL}" ]; then
|
if [ ! -f "${TARBALL}" ]; then
|
||||||
echo "::error::Expected tarball ${TARBALL} not found"
|
echo "::error::Expected tarball ${TARBALL} not found"
|
||||||
ls -la *.tar.gz
|
ls -la *.tar.gz
|
||||||
@@ -85,7 +93,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate tarball name
|
- name: Validate tarball name
|
||||||
run: |
|
run: |
|
||||||
EXPECTED="headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
|
EXPECTED="${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
|
||||||
ACTUAL=$(ls *.tar.gz)
|
ACTUAL=$(ls *.tar.gz)
|
||||||
if [ "$EXPECTED" != "$ACTUAL" ]; then
|
if [ "$EXPECTED" != "$ACTUAL" ]; then
|
||||||
echo "::error::Tarball name mismatch! Expected: $EXPECTED, Got: $ACTUAL"
|
echo "::error::Tarball name mismatch! Expected: $EXPECTED, Got: $ACTUAL"
|
||||||
@@ -96,19 +104,19 @@ jobs:
|
|||||||
- name: Compute checksum
|
- name: Compute checksum
|
||||||
id: compute_checksum
|
id: compute_checksum
|
||||||
run: |
|
run: |
|
||||||
TARBALL="headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
|
TARBALL="${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
|
||||||
CHECKSUM=$(sha256sum "$TARBALL" | awk '{print $1}')
|
CHECKSUM=$(sha256sum "$TARBALL" | awk '{print $1}')
|
||||||
echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT
|
echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT
|
||||||
echo "Checksum: sha256:${CHECKSUM}"
|
echo "Checksum: sha256:${CHECKSUM}"
|
||||||
|
|
||||||
- name: Verify tarball contents
|
- name: Verify tarball contents
|
||||||
run: |
|
run: |
|
||||||
TARBALL="headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
|
TARBALL="${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
|
||||||
echo "Tarball contents:"
|
echo "Tarball contents:"
|
||||||
tar -tzf "${TARBALL}" | head -20
|
tar -tzf "${TARBALL}" | head -20
|
||||||
|
|
||||||
# Verify main.js exists (structure is headlamp-sealed-secrets/main.js)
|
# Verify main.js exists (structure is <package-name>/main.js)
|
||||||
if ! tar -tzf "${TARBALL}" | grep -q "headlamp-sealed-secrets/main.js"; then
|
if ! tar -tzf "${TARBALL}" | grep -q "${{ steps.package_name.outputs.name }}/main.js"; then
|
||||||
echo "::error::main.js not found in tarball"
|
echo "::error::main.js not found in tarball"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -134,7 +142,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: "v${{ inputs.version }}"
|
tag_name: "v${{ inputs.version }}"
|
||||||
files: headlamp-sealed-secrets-${{ inputs.version }}.tar.gz
|
files: ${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
@@ -147,9 +155,9 @@ jobs:
|
|||||||
echo "Release Summary:"
|
echo "Release Summary:"
|
||||||
echo "=================="
|
echo "=================="
|
||||||
echo "Version: v${{ inputs.version }}"
|
echo "Version: v${{ inputs.version }}"
|
||||||
echo "Tarball: headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
|
echo "Tarball: ${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
|
||||||
echo "Checksum: sha256:${{ steps.compute_checksum.outputs.checksum }}"
|
echo "Checksum: sha256:${{ steps.compute_checksum.outputs.checksum }}"
|
||||||
echo "Archive URL: https://github.com/${{ github.repository }}/releases/download/v${{ inputs.version }}/headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
|
echo "Archive URL: https://github.com/${{ github.repository }}/releases/download/v${{ inputs.version }}/${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
|
||||||
echo ""
|
echo ""
|
||||||
echo "✓ Version bumped to ${{ inputs.version }}"
|
echo "✓ Version bumped to ${{ inputs.version }}"
|
||||||
echo "✓ Metadata updated with checksum"
|
echo "✓ Metadata updated with checksum"
|
||||||
|
|||||||
+6
-6
@@ -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.9
|
version: 0.2.18
|
||||||
name: headlamp-sealed-secrets
|
name: sealed-secrets
|
||||||
displayName: Sealed Secrets Plugin for Headlamp
|
displayName: Sealed Secrets
|
||||||
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.9
|
appVersion: 0.2.18
|
||||||
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.9/headlamp-sealed-secrets-0.2.9.tar.gz"
|
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin/releases/download/v0.2.18/sealed-secrets-0.2.18.tar.gz"
|
||||||
headlamp/plugin/archive-checksum: sha256:196f58dcd823a1e1b20b061eb22583121e6cd19bf7491c7dcc72658dc7fd15bc
|
headlamp/plugin/archive-checksum: sha256:b110bd4a2ac333ab17881c9f097f0b796edffa48ad9d95be6fda15236a503475
|
||||||
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:
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "headlamp-sealed-secrets",
|
"name": "sealed-secrets",
|
||||||
"version": "0.2.9",
|
"version": "0.2.18",
|
||||||
"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';
|
||||||
@@ -45,15 +57,15 @@ function formatScope(scope: SealedSecretScope): string {
|
|||||||
* SealedSecret detail view component
|
* SealedSecret detail view component
|
||||||
*/
|
*/
|
||||||
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 || undefined, namespace || undefined);
|
||||||
const [secret] = K8s.ResourceClasses.Secret.useGet(name, namespace);
|
const [secret] = K8s.ResourceClasses.Secret.useGet(name || undefined, namespace || undefined);
|
||||||
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);
|
||||||
const [rotating, setRotating] = React.useState(false);
|
const [rotating, setRotating] = React.useState(false);
|
||||||
const [canDecrypt, setCanDecrypt] = React.useState(false);
|
const [canDecrypt, setCanDecrypt] = React.useState(false);
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const { permissions } = usePermissions(namespace);
|
const { permissions } = usePermissions(namespace || undefined);
|
||||||
|
|
||||||
// Check if user can decrypt secrets (requires get permission on Secrets)
|
// Check if user can decrypt secrets (requires get permission on Secrets)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -62,6 +74,27 @@ export function SealedSecretDetail() {
|
|||||||
}
|
}
|
||||||
}, [namespace]);
|
}, [namespace]);
|
||||||
|
|
||||||
|
// Wait for required params before rendering
|
||||||
|
if (!namespace || !name) {
|
||||||
|
return <SealedSecretDetailSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 +127,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 sx={{ height: '100%', overflow: 'auto' }}>
|
||||||
|
<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 +189,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 +209,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 +267,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 +296,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 +304,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 +321,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 />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ export function SettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionBox
|
<SectionBox title="Sealed Secrets Plugin Settings">
|
||||||
title="Sealed Secrets Plugin Settings"
|
|
||||||
>
|
|
||||||
<Box p={3}>
|
<Box p={3}>
|
||||||
<Typography variant="body1" paragraph id="settings-description">
|
<Typography variant="body1" paragraph id="settings-description">
|
||||||
Configure the connection to your Sealed Secrets controller. These settings are stored in
|
Configure the connection to your Sealed Secrets controller. These settings are stored in
|
||||||
|
|||||||
@@ -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