style: format all source files with Prettier
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,9 @@ export function DecryptDialog({ sealedSecret, secretKey, onClose }: DecryptDialo
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} aria-label="Close dialog">Close</Button>
|
||||
<Button onClick={onClose} aria-label="Close dialog">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -103,7 +105,9 @@ export function DecryptDialog({ sealedSecret, secretKey, onClose }: DecryptDialo
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} aria-label="Close dialog">Close</Button>
|
||||
<Button onClick={onClose} aria-label="Close dialog">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -182,7 +186,9 @@ export function DecryptDialog({ sealedSecret, secretKey, onClose }: DecryptDialo
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} aria-label="Close dialog">Close</Button>
|
||||
<Button onClick={onClose} aria-label="Close dialog">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -83,10 +83,12 @@ export function EncryptDialog({ open, onClose }: EncryptDialogProps) {
|
||||
|
||||
const handleCreate = async () => {
|
||||
// Filter out empty rows
|
||||
const validKeyValues = keyValues.filter(kv => kv.key || kv.value).map(kv => ({
|
||||
key: kv.key,
|
||||
value: kv.value,
|
||||
}));
|
||||
const validKeyValues = keyValues
|
||||
.filter(kv => kv.key || kv.value)
|
||||
.map(kv => ({
|
||||
key: kv.key,
|
||||
value: kv.value,
|
||||
}));
|
||||
|
||||
// Use the encryption hook
|
||||
const result = await encrypt({
|
||||
@@ -232,7 +234,11 @@ export function EncryptDialog({ open, onClose }: EncryptDialogProps) {
|
||||
disabled={keyValues.length === 1}
|
||||
color="error"
|
||||
aria-label={`Remove key-value pair ${index + 1}`}
|
||||
title={keyValues.length === 1 ? 'At least one key-value pair is required' : `Remove key-value pair ${index + 1}`}
|
||||
title={
|
||||
keyValues.length === 1
|
||||
? 'At least one key-value pair is required'
|
||||
: `Remove key-value pair ${index + 1}`
|
||||
}
|
||||
>
|
||||
<Icon icon="mdi:delete" />
|
||||
</IconButton>
|
||||
|
||||
@@ -192,8 +192,8 @@ export class GenericErrorBoundary extends BaseErrorBoundary {
|
||||
Something Went Wrong
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
An unexpected error occurred. Please try reloading the page or contact your administrator
|
||||
if the problem persists.
|
||||
An unexpected error occurred. Please try reloading the page or contact your
|
||||
administrator if the problem persists.
|
||||
</Typography>
|
||||
{this.state.error && (
|
||||
<Typography
|
||||
|
||||
@@ -41,15 +41,37 @@ export function SealedSecretDetailSkeleton() {
|
||||
<Skeleton variant="text" width="40%" height={40} sx={{ mb: 3 }} animation="wave" />
|
||||
|
||||
{/* Metadata section */}
|
||||
<Skeleton variant="rectangular" height={200} sx={{ mb: 2, borderRadius: 1 }} animation="wave" />
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={200}
|
||||
sx={{ mb: 2, borderRadius: 1 }}
|
||||
animation="wave"
|
||||
/>
|
||||
|
||||
{/* Encrypted data section */}
|
||||
<Skeleton variant="rectangular" height={150} sx={{ mb: 2, borderRadius: 1 }} animation="wave" />
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={150}
|
||||
sx={{ mb: 2, borderRadius: 1 }}
|
||||
animation="wave"
|
||||
/>
|
||||
|
||||
{/* Actions section */}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Skeleton variant="rectangular" width={120} height={36} sx={{ borderRadius: 1 }} animation="wave" />
|
||||
<Skeleton variant="rectangular" width={120} height={36} sx={{ borderRadius: 1 }} animation="wave" />
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width={120}
|
||||
height={36}
|
||||
sx={{ borderRadius: 1 }}
|
||||
animation="wave"
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width={120}
|
||||
height={36}
|
||||
sx={{ borderRadius: 1 }}
|
||||
animation="wave"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -66,10 +88,27 @@ export function SealingKeysListSkeleton() {
|
||||
{[1, 2].map(i => (
|
||||
<Box key={i} sx={{ mb: 3 }}>
|
||||
<Skeleton variant="text" width="30%" height={32} sx={{ mb: 1 }} animation="wave" />
|
||||
<Skeleton variant="rectangular" height={100} sx={{ borderRadius: 1, mb: 1 }} animation="wave" />
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={100}
|
||||
sx={{ borderRadius: 1, mb: 1 }}
|
||||
animation="wave"
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Skeleton variant="rectangular" width={100} height={28} sx={{ borderRadius: 1 }} animation="wave" />
|
||||
<Skeleton variant="rectangular" width={100} height={28} sx={{ borderRadius: 1 }} animation="wave" />
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width={100}
|
||||
height={28}
|
||||
sx={{ borderRadius: 1 }}
|
||||
animation="wave"
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width={100}
|
||||
height={28}
|
||||
sx={{ borderRadius: 1 }}
|
||||
animation="wave"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -87,9 +87,7 @@ export function SealedSecretDetail() {
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Failed to load SealedSecret
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{String(error)}
|
||||
</Typography>
|
||||
<Typography variant="body2">{String(error)}</Typography>
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
@@ -162,165 +160,167 @@ export function SealedSecretDetail() {
|
||||
<span>{sealedSecret.metadata.name}</span>
|
||||
</Box>
|
||||
<Box>
|
||||
{permissions?.canUpdate && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleRotate}
|
||||
disabled={rotating}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
{rotating ? 'Re-encrypting...' : 'Re-encrypt'}
|
||||
</Button>
|
||||
)}
|
||||
{permissions?.canDelete && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
{permissions?.canUpdate && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleRotate}
|
||||
disabled={rotating}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
{rotating ? 'Re-encrypting...' : 'Re-encrypt'}
|
||||
</Button>
|
||||
)}
|
||||
{permissions?.canDelete && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Name',
|
||||
value: String(sealedSecret.metadata.name || ''),
|
||||
},
|
||||
{
|
||||
name: 'Namespace',
|
||||
value: String(sealedSecret.metadata.namespace || ''),
|
||||
},
|
||||
{
|
||||
name: 'Scope',
|
||||
value: String(formatScope(sealedSecret.scope)),
|
||||
},
|
||||
{
|
||||
name: 'Sync Status',
|
||||
value: (
|
||||
<StatusLabel status={sealedSecret.isSynced ? 'success' : 'error'}>
|
||||
{sealedSecret.isSynced ? 'Synced' : 'Not Synced'}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Status Message',
|
||||
value: String(sealedSecret.syncMessage || 'Unknown'),
|
||||
hide: !sealedSecret.syncCondition,
|
||||
},
|
||||
{
|
||||
name: 'Age',
|
||||
value: String(sealedSecret.getAge() || ''),
|
||||
},
|
||||
{
|
||||
name: 'Created',
|
||||
value: sealedSecret.metadata.creationTimestamp
|
||||
? new Date(sealedSecret.metadata.creationTimestamp).toLocaleString()
|
||||
: 'Unknown',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Encrypted Data">
|
||||
<SimpleTable
|
||||
data={encryptedKeys.map(key => ({
|
||||
key,
|
||||
value: sealedSecret.spec.encryptedData[key],
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
label: 'Key',
|
||||
getter: (row: any) => row.key,
|
||||
},
|
||||
{
|
||||
label: 'Encrypted Value',
|
||||
getter: (row: any) => {
|
||||
const val = row.value;
|
||||
return val.length > 40 ? val.substring(0, 40) + '...' : val;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Actions',
|
||||
getter: (row: any) =>
|
||||
canDecrypt ? (
|
||||
<Button size="small" onClick={() => setDecryptKey(row.key)}>
|
||||
Decrypt
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="small" disabled title="No permission to access Secrets">
|
||||
Decrypt
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
{sealedSecret.spec.template && (
|
||||
<SectionBox title="Template">
|
||||
}
|
||||
>
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Secret Type',
|
||||
value: String(sealedSecret.spec.template.type || 'Opaque'),
|
||||
name: 'Name',
|
||||
value: String(sealedSecret.metadata.name || ''),
|
||||
},
|
||||
{
|
||||
name: 'Labels',
|
||||
value: String(JSON.stringify(sealedSecret.spec.template.metadata?.labels || {})),
|
||||
hide: !sealedSecret.spec.template.metadata?.labels,
|
||||
name: 'Namespace',
|
||||
value: String(sealedSecret.metadata.namespace || ''),
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
value: String(
|
||||
JSON.stringify(sealedSecret.spec.template.metadata?.annotations || {})
|
||||
name: 'Scope',
|
||||
value: String(formatScope(sealedSecret.scope)),
|
||||
},
|
||||
{
|
||||
name: 'Sync Status',
|
||||
value: (
|
||||
<StatusLabel status={sealedSecret.isSynced ? 'success' : 'error'}>
|
||||
{sealedSecret.isSynced ? 'Synced' : 'Not Synced'}
|
||||
</StatusLabel>
|
||||
),
|
||||
hide: !sealedSecret.spec.template.metadata?.annotations,
|
||||
},
|
||||
{
|
||||
name: 'Status Message',
|
||||
value: String(sealedSecret.syncMessage || 'Unknown'),
|
||||
hide: !sealedSecret.syncCondition,
|
||||
},
|
||||
{
|
||||
name: 'Age',
|
||||
value: String(sealedSecret.getAge() || ''),
|
||||
},
|
||||
{
|
||||
name: 'Created',
|
||||
value: sealedSecret.metadata.creationTimestamp
|
||||
? new Date(sealedSecret.metadata.creationTimestamp).toLocaleString()
|
||||
: 'Unknown',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
<SectionBox title="Resulting Secret">
|
||||
{secret ? (
|
||||
<NameValueTable
|
||||
rows={[
|
||||
<SectionBox title="Encrypted Data">
|
||||
<SimpleTable
|
||||
data={encryptedKeys.map(key => ({
|
||||
key,
|
||||
value: sealedSecret.spec.encryptedData[key],
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: <StatusLabel status="success">Secret exists</StatusLabel>,
|
||||
label: 'Key',
|
||||
getter: (row: any) => row.key,
|
||||
},
|
||||
{
|
||||
name: 'Keys',
|
||||
value: String(Object.keys(secret.data || {}).join(', ') || 'None'),
|
||||
label: 'Encrypted Value',
|
||||
getter: (row: any) => {
|
||||
const val = row.value;
|
||||
return val.length > 40 ? val.substring(0, 40) + '...' : val;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Link',
|
||||
value: (
|
||||
<Link
|
||||
routeName="secret"
|
||||
params={{
|
||||
namespace: String(secret.metadata.namespace || ''),
|
||||
name: String(secret.metadata.name || ''),
|
||||
}}
|
||||
>
|
||||
View Secret
|
||||
</Link>
|
||||
),
|
||||
label: 'Actions',
|
||||
getter: (row: any) =>
|
||||
canDecrypt ? (
|
||||
<Button size="small" onClick={() => setDecryptKey(row.key)}>
|
||||
Decrypt
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="small" disabled title="No permission to access Secrets">
|
||||
Decrypt
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Box p={2}>
|
||||
<StatusLabel status="warning">Secret not yet created</StatusLabel>
|
||||
<p>The controller will create the Secret once it processes this SealedSecret.</p>
|
||||
</Box>
|
||||
</SectionBox>
|
||||
|
||||
{sealedSecret.spec.template && (
|
||||
<SectionBox title="Template">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Secret Type',
|
||||
value: String(sealedSecret.spec.template.type || 'Opaque'),
|
||||
},
|
||||
{
|
||||
name: 'Labels',
|
||||
value: String(
|
||||
JSON.stringify(sealedSecret.spec.template.metadata?.labels || {})
|
||||
),
|
||||
hide: !sealedSecret.spec.template.metadata?.labels,
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
value: String(
|
||||
JSON.stringify(sealedSecret.spec.template.metadata?.annotations || {})
|
||||
),
|
||||
hide: !sealedSecret.spec.template.metadata?.annotations,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Resulting Secret">
|
||||
{secret ? (
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: <StatusLabel status="success">Secret exists</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Keys',
|
||||
value: String(Object.keys(secret.data || {}).join(', ') || 'None'),
|
||||
},
|
||||
{
|
||||
name: 'Link',
|
||||
value: (
|
||||
<Link
|
||||
routeName="secret"
|
||||
params={{
|
||||
namespace: String(secret.metadata.namespace || ''),
|
||||
name: String(secret.metadata.name || ''),
|
||||
}}
|
||||
>
|
||||
View Secret
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Box p={2}>
|
||||
<StatusLabel status="warning">Secret not yet created</StatusLabel>
|
||||
<p>The controller will create the Secret once it processes this SealedSecret.</p>
|
||||
</Box>
|
||||
)}
|
||||
</SectionBox>
|
||||
</Box>
|
||||
|
||||
{decryptKey && (
|
||||
|
||||
@@ -126,9 +126,7 @@ export function SealedSecretList() {
|
||||
// Show error if CRD is not installed
|
||||
if (error) {
|
||||
return (
|
||||
<SectionBox
|
||||
title="Sealed Secrets"
|
||||
>
|
||||
<SectionBox title="Sealed Secrets">
|
||||
<Box p={2}>
|
||||
<StatusLabel status="error">Error</StatusLabel>
|
||||
<Box mt={2}>
|
||||
@@ -139,7 +137,11 @@ export function SealedSecretList() {
|
||||
cluster.
|
||||
</p>
|
||||
<p>
|
||||
Install with: <code>kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml</code>
|
||||
Install with:{' '}
|
||||
<code>
|
||||
kubectl apply -f
|
||||
https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
|
||||
</code>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
@@ -153,9 +155,7 @@ export function SealedSecretList() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionBox
|
||||
title="Sealed Secrets"
|
||||
>
|
||||
<SectionBox title="Sealed Secrets">
|
||||
<VersionWarning autoDetect showDetails={false} />
|
||||
<SectionFilterHeader title="" noNamespaceFilter={false} actions={actions} />
|
||||
<SimpleTable data={sealedSecrets} columns={columns} />
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
*/
|
||||
|
||||
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
import { SectionBox, SimpleTable, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
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';
|
||||
@@ -27,7 +31,9 @@ interface SealingKey {
|
||||
*/
|
||||
export function SealingKeysView() {
|
||||
const config = getPluginConfig();
|
||||
const [secrets, , loading] = K8s.ResourceClasses.Secret.useList({ namespace: config.controllerNamespace });
|
||||
const [secrets, , loading] = K8s.ResourceClasses.Secret.useList({
|
||||
namespace: config.controllerNamespace,
|
||||
});
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
// Filter for sealing key secrets
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
*/
|
||||
|
||||
import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { NameValueTable, SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import {
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { SealedSecret } from '../lib/SealedSecretCRD';
|
||||
|
||||
|
||||
@@ -41,9 +41,7 @@ export function VersionWarning({ autoDetect = true, showDetails = false }: Versi
|
||||
} else if (result.ok === false) {
|
||||
setDetectedVersion(null);
|
||||
// Ensure error is always a string
|
||||
const errorMessage = typeof result.error === 'string'
|
||||
? result.error
|
||||
: String(result.error);
|
||||
const errorMessage = typeof result.error === 'string' ? result.error : String(result.error);
|
||||
setError(errorMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -70,11 +68,14 @@ export function VersionWarning({ autoDetect = true, showDetails = false }: Versi
|
||||
if (error) {
|
||||
return (
|
||||
<Box mb={2}>
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={detectVersion}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
<Alert
|
||||
severity="error"
|
||||
action={
|
||||
<Button color="inherit" size="small" onClick={detectVersion}>
|
||||
Retry
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<strong>API Version Detection Failed</strong>
|
||||
<br />
|
||||
{String(error)}
|
||||
@@ -84,7 +85,8 @@ export function VersionWarning({ autoDetect = true, showDetails = false }: Versi
|
||||
<br />
|
||||
Install Sealed Secrets with:{' '}
|
||||
<code style={{ fontSize: '0.875em' }}>
|
||||
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
|
||||
kubectl apply -f
|
||||
https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
|
||||
</code>
|
||||
<br />
|
||||
Or visit:{' '}
|
||||
|
||||
@@ -15,14 +15,7 @@ import {
|
||||
parsePublicKeyFromCert,
|
||||
} from '../lib/crypto';
|
||||
import { validateSecretKey, validateSecretName, validateSecretValue } from '../lib/validators';
|
||||
import {
|
||||
AsyncResult,
|
||||
CertificateInfo,
|
||||
Err,
|
||||
Ok,
|
||||
PlaintextValue,
|
||||
SealedSecretScope,
|
||||
} from '../types';
|
||||
import { AsyncResult, CertificateInfo, Err, Ok, PlaintextValue, SealedSecretScope } from '../types';
|
||||
|
||||
/**
|
||||
* Request parameters for encryption
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* SealedSecret Custom Resource Definition
|
||||
*/
|
||||
|
||||
import { ApiProxy,K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
|
||||
const { apiFactoryWithNamespace } = ApiProxy;
|
||||
const { KubeObject } = K8s.cluster;
|
||||
|
||||
@@ -39,7 +39,10 @@ describe('retry logic', () => {
|
||||
.mockResolvedValueOnce({ ok: false, error: 'error2' })
|
||||
.mockResolvedValueOnce({ ok: true, value: 'success' });
|
||||
|
||||
const promise = retryWithBackoff(failTwiceThenSucceed, { maxAttempts: 3, initialDelayMs: 100 });
|
||||
const promise = retryWithBackoff(failTwiceThenSucceed, {
|
||||
maxAttempts: 3,
|
||||
initialDelayMs: 100,
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
const result = await promise;
|
||||
|
||||
@@ -72,7 +75,10 @@ describe('retry logic', () => {
|
||||
.mockResolvedValueOnce({ ok: false, error: 'error2' })
|
||||
.mockResolvedValueOnce({ ok: true, value: 'success' });
|
||||
|
||||
const promise = retryWithBackoff(failTwiceThenSucceed, { maxAttempts: 3, initialDelayMs: 1000 });
|
||||
const promise = retryWithBackoff(failTwiceThenSucceed, {
|
||||
maxAttempts: 3,
|
||||
initialDelayMs: 1000,
|
||||
});
|
||||
|
||||
// Fast-forward through retries
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
+6
-8
@@ -109,9 +109,7 @@ export async function retryWithBackoff<T, E>(
|
||||
const isLastAttempt = attempt === opts.maxAttempts - 1;
|
||||
if (isLastAttempt) {
|
||||
// No more retries, return final error
|
||||
return Err(
|
||||
`Operation failed after ${opts.maxAttempts} attempts:\n${errors.join('\n')}`
|
||||
);
|
||||
return Err(`Operation failed after ${opts.maxAttempts} attempts:\n${errors.join('\n')}`);
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
@@ -124,9 +122,7 @@ export async function retryWithBackoff<T, E>(
|
||||
|
||||
const isLastAttempt = attempt === opts.maxAttempts - 1;
|
||||
if (isLastAttempt) {
|
||||
return Err(
|
||||
`Operation failed after ${opts.maxAttempts} attempts:\n${errors.join('\n')}`
|
||||
);
|
||||
return Err(`Operation failed after ${opts.maxAttempts} attempts:\n${errors.join('\n')}`);
|
||||
}
|
||||
|
||||
const delay = calculateDelay(attempt, opts);
|
||||
@@ -171,10 +167,12 @@ export function isRetryableHttpError(error: Error): boolean {
|
||||
}
|
||||
|
||||
// Check for specific retryable status codes
|
||||
return message.includes('429') || // Too Many Requests
|
||||
return (
|
||||
message.includes('429') || // Too Many Requests
|
||||
message.includes('408') || // Request Timeout
|
||||
message.includes('503') || // Service Unavailable
|
||||
message.includes('504'); // Gateway Timeout
|
||||
message.includes('504')
|
||||
); // Gateway Timeout
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-3
@@ -16,9 +16,7 @@ type KubeObjectInterface = K8s.cluster.KubeObjectInterface;
|
||||
* return Ok(a / b);
|
||||
* }
|
||||
*/
|
||||
export type Result<T, E = Error> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: E };
|
||||
export type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
||||
|
||||
/**
|
||||
* Async result type for promises that can fail
|
||||
|
||||
Reference in New Issue
Block a user