@skyhook-io/k8s-ui 1.14.2 → 1.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/components/logs/LogCore.tsx +182 -16
- package/src/components/logs/LogsViewer.tsx +4 -21
- package/src/components/logs/WorkloadLogsViewer.tsx +5 -23
- package/src/components/resources/ResourcesView.tsx +150 -125
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/AlertRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +5 -5
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +3 -3
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +5 -5
- package/src/components/resources/renderers/ChallengeRenderer.tsx +8 -8
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +3 -3
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +2 -2
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +3 -3
- package/src/components/resources/renderers/GatewayRenderer.tsx +2 -2
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
- package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -1
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +3 -3
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +2 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -3
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +2 -2
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +23 -20
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +2 -2
- package/src/components/resources/renderers/NodeRenderer.tsx +3 -3
- package/src/components/resources/renderers/OrderRenderer.tsx +6 -6
- package/src/components/resources/renderers/RoleRenderer.tsx +3 -3
- package/src/components/resources/renderers/RolloutRenderer.tsx +2 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -3
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +8 -8
- package/src/components/resources/renderers/VPARenderer.tsx +1 -1
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -1
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +9 -9
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +6 -12
- package/src/components/resources/renderers/capi-cells.tsx +3 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +2 -2
- package/src/components/resources/renderers/cnpg-cells.tsx +1 -1
- package/src/components/resources/renderers/eso-cells.tsx +17 -1
- package/src/components/resources/renderers/knative-cells.tsx +14 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +14 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +6 -0
- package/src/components/resources/renderers/trivy-cells.tsx +7 -0
- package/src/components/resources/renderers/velero-cells.tsx +3 -0
- package/src/components/resources/resource-utils-calico.test.ts +6 -0
- package/src/components/resources/resource-utils-calico.ts +6 -2
- package/src/components/resources/resource-utils-certmanager.ts +2 -2
- package/src/components/resources/resource-utils-eso.test.ts +26 -1
- package/src/components/resources/resource-utils-eso.ts +13 -0
- package/src/components/resources/resource-utils-knative.ts +3 -3
- package/src/components/resources/resource-utils-trivy.ts +3 -0
- package/src/components/resources/resource-utils.ts +17 -1
- package/src/components/resources/resources-column-filter.test.ts +20 -0
- package/src/components/topology/TopologyGraph.tsx +18 -1
- package/src/components/topology/pod-phase-fallback.test.ts +24 -0
- package/src/theme/components.css +29 -0
- package/src/utils/badge-colors.ts +1 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/log-export.test.ts +225 -0
- package/src/utils/log-export.ts +149 -0
- package/src/utils/navigation.test.ts +10 -0
- package/src/utils/navigation.ts +4 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getClusterClassStatus,
|
|
13
13
|
getMachineHealthCheckStatus, getMachineHealthCheckHealthy, getMachineHealthCheckClusterName,
|
|
14
14
|
getClusterProvider,
|
|
15
|
+
getMachineDeploymentUpToDate,
|
|
15
16
|
} from '../resource-utils-capi'
|
|
16
17
|
|
|
17
18
|
function StatusBadge({ resource, getStatus }: { resource: any; getStatus: (r: any) => { text: string; color: string } }) {
|
|
@@ -78,6 +79,8 @@ export function CAPIMachineDeploymentCell({ resource, column }: { resource: any;
|
|
|
78
79
|
return <TextCell value={getMachineClusterName(resource)} />
|
|
79
80
|
case 'ready':
|
|
80
81
|
return <TextCell value={getMachineDeploymentReplicas(resource)} />
|
|
82
|
+
case 'upToDate':
|
|
83
|
+
return <TextCell value={getMachineDeploymentUpToDate(resource)} />
|
|
81
84
|
case 'version':
|
|
82
85
|
return <TextCell value={getMachineDeploymentVersion(resource)} />
|
|
83
86
|
default:
|
|
@@ -48,8 +48,8 @@ export function CertificateCell({ resource, column }: { resource: any; column: s
|
|
|
48
48
|
case 'expires': {
|
|
49
49
|
const expiry = getCertificateExpiry(resource)
|
|
50
50
|
return (
|
|
51
|
-
<span className={clsx(
|
|
52
|
-
'text-sm font-medium',
|
|
51
|
+
<span title={expiry.text} className={clsx(
|
|
52
|
+
'text-sm font-medium truncate block',
|
|
53
53
|
expiry.level === 'unhealthy' ? 'text-red-400' :
|
|
54
54
|
expiry.level === 'degraded' ? 'text-yellow-400' :
|
|
55
55
|
expiry.level === 'healthy' ? 'text-green-400' :
|
|
@@ -287,7 +287,7 @@ export function CNPGPoolerCell({ resource, column }: { resource: any; column: st
|
|
|
287
287
|
return (
|
|
288
288
|
<span className={clsx(
|
|
289
289
|
'badge-sm',
|
|
290
|
-
type === 'rw' ? '
|
|
290
|
+
type === 'rw' ? 'status-blue' : 'status-purple'
|
|
291
291
|
)}>
|
|
292
292
|
{type}
|
|
293
293
|
</span>
|
|
@@ -4,6 +4,7 @@ import { clsx } from 'clsx'
|
|
|
4
4
|
import {
|
|
5
5
|
getExternalSecretStatus,
|
|
6
6
|
getExternalSecretStore,
|
|
7
|
+
getExternalSecretStoreKey,
|
|
7
8
|
getExternalSecretRefreshInterval,
|
|
8
9
|
getExternalSecretLastSync,
|
|
9
10
|
getClusterExternalSecretStatus,
|
|
@@ -14,7 +15,11 @@ import {
|
|
|
14
15
|
getClusterSecretStoreStatus,
|
|
15
16
|
} from '../resource-utils-eso'
|
|
16
17
|
|
|
17
|
-
export function ExternalSecretCell({ resource, column }: {
|
|
18
|
+
export function ExternalSecretCell({ resource, column, storeProviders }: {
|
|
19
|
+
resource: any
|
|
20
|
+
column: string
|
|
21
|
+
storeProviders?: Record<string, string>
|
|
22
|
+
}) {
|
|
18
23
|
switch (column) {
|
|
19
24
|
case 'status': {
|
|
20
25
|
const status = getExternalSecretStatus(resource)
|
|
@@ -32,6 +37,17 @@ export function ExternalSecretCell({ resource, column }: { resource: any; column
|
|
|
32
37
|
</span>
|
|
33
38
|
)
|
|
34
39
|
}
|
|
40
|
+
case 'provider': {
|
|
41
|
+
// The provider lives on the referenced store, not on this object. Three
|
|
42
|
+
// states, and the middle one matters: no map yet means the stores are
|
|
43
|
+
// still loading, which is not the same as a store we looked for and could
|
|
44
|
+
// not read.
|
|
45
|
+
if (!storeProviders) return <span className="text-sm text-theme-text-tertiary">…</span>
|
|
46
|
+
const provider = storeProviders[getExternalSecretStoreKey(resource)]
|
|
47
|
+
return provider
|
|
48
|
+
? <span className="text-sm text-theme-text-secondary truncate block" title={provider}>{provider}</span>
|
|
49
|
+
: <span className="text-sm text-theme-text-tertiary" title="The referenced store could not be read">-</span>
|
|
50
|
+
}
|
|
35
51
|
case 'refreshInterval': {
|
|
36
52
|
const interval = getExternalSecretRefreshInterval(resource)
|
|
37
53
|
return <span className="text-sm text-theme-text-secondary">{interval}</span>
|
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
getDomainMappingUrl,
|
|
29
29
|
getServerlessServiceMode,
|
|
30
30
|
} from '../resource-utils-knative'
|
|
31
|
+
// Knative's Certificate reports status.notAfter like any other TLS certificate,
|
|
32
|
+
// so it shares the expiry formatter rather than re-deriving the thresholds.
|
|
33
|
+
import { getCertificateExpiry, healthColors } from '../resource-utils'
|
|
31
34
|
|
|
32
35
|
function StatusCell({ resource, getStatus }: { resource: any; getStatus: (r: any) => { text: string; color: string } }) {
|
|
33
36
|
const status = getStatus(resource)
|
|
@@ -366,6 +369,16 @@ export function KnativeCertificateCell({ resource, column }: { resource: any; co
|
|
|
366
369
|
}
|
|
367
370
|
case 'secretName':
|
|
368
371
|
return <TextCell value={resource?.spec?.secretName || '-'} />
|
|
372
|
+
case 'expires': {
|
|
373
|
+
const expiry = getCertificateExpiry(resource)
|
|
374
|
+
// "Expired 365d ago" is far wider than the column; bound it like the
|
|
375
|
+
// other status badges that carry arbitrary-length text.
|
|
376
|
+
return (
|
|
377
|
+
<span className={clsx('badge truncate max-w-full', healthColors[expiry.level])} title={expiry.text}>
|
|
378
|
+
{expiry.text}
|
|
379
|
+
</span>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
369
382
|
default:
|
|
370
383
|
return <DefaultCell />
|
|
371
384
|
}
|
|
@@ -384,7 +397,7 @@ export function ServerlessServiceCell({ resource, column }: { resource: any; col
|
|
|
384
397
|
return (
|
|
385
398
|
<span className={clsx(
|
|
386
399
|
'badge',
|
|
387
|
-
mode === 'Proxy' ? '
|
|
400
|
+
mode === 'Proxy' ? 'status-amber' : 'status-green'
|
|
388
401
|
)}>
|
|
389
402
|
{mode}
|
|
390
403
|
</span>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getKyvernoPolicyStatus,
|
|
9
9
|
getKyvernoEnforcement,
|
|
10
10
|
getKyvernoPolicyRuleCount,
|
|
11
|
+
getKyvernoPolicyRuleTypes,
|
|
11
12
|
} from '../resource-utils-kyverno'
|
|
12
13
|
import {
|
|
13
14
|
getKyvernoRequestState,
|
|
@@ -83,16 +84,26 @@ export function KyvernoPolicyCell({ resource, column }: { resource: any; column:
|
|
|
83
84
|
return (
|
|
84
85
|
<span className={clsx(
|
|
85
86
|
'badge',
|
|
87
|
+
// Enforce and Audit are configured postures, so they take colour-named
|
|
88
|
+
// accents. A discrepancy is not a posture — the policy declares Enforce
|
|
89
|
+
// while admission evaluation is off, so it blocks nothing it claims to
|
|
90
|
+
// — and that is a fault, which is what the alert tier is for.
|
|
86
91
|
blocks
|
|
87
|
-
? '
|
|
92
|
+
? 'status-red'
|
|
88
93
|
: discrepancy
|
|
89
|
-
? '
|
|
90
|
-
: '
|
|
94
|
+
? 'status-alert'
|
|
95
|
+
: 'status-amber',
|
|
91
96
|
)}>
|
|
92
97
|
{label}
|
|
93
98
|
</span>
|
|
94
99
|
)
|
|
95
100
|
}
|
|
101
|
+
case 'ruleTypes': {
|
|
102
|
+
const types = getKyvernoPolicyRuleTypes(resource)
|
|
103
|
+
return (
|
|
104
|
+
<span className="text-sm text-theme-text-secondary truncate block" title={types}>{types}</span>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
96
107
|
case 'rules': {
|
|
97
108
|
const count = getKyvernoPolicyRuleCount(resource)
|
|
98
109
|
return <span className="text-sm text-theme-text-secondary">{count}</span>
|
|
@@ -129,6 +129,12 @@ export function KyvernoCleanupPolicyCell({ resource, column }: { resource: any;
|
|
|
129
129
|
const status = getKyvernoCleanupPolicyStatus(resource)
|
|
130
130
|
return <span className={clsx('badge', status.color)}>{status.text}</span>
|
|
131
131
|
}
|
|
132
|
+
case 'lastRun': {
|
|
133
|
+
const last = getKyvernoLastExecutionTime(resource)
|
|
134
|
+
return last
|
|
135
|
+
? <span className="text-sm text-theme-text-secondary">{formatAge(last)}</span>
|
|
136
|
+
: <span className="text-sm text-theme-text-tertiary">-</span>
|
|
137
|
+
}
|
|
132
138
|
case 'schedule': {
|
|
133
139
|
const schedule = getKyvernoCleanupSchedule(resource)
|
|
134
140
|
return schedule ? (
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getVulnerabilityReportSummary,
|
|
7
7
|
getVulnerabilityReportContainer,
|
|
8
8
|
getVulnerabilityReportImage,
|
|
9
|
+
getSbomReportImage,
|
|
9
10
|
getConfigAuditReportSummary,
|
|
10
11
|
getConfigAuditReportStatus,
|
|
11
12
|
getExposedSecretReportSummary,
|
|
@@ -133,6 +134,12 @@ export function ClusterComplianceReportCell({ resource, column }: { resource: an
|
|
|
133
134
|
|
|
134
135
|
export function SbomReportCell({ resource, column }: { resource: any; column: string }) {
|
|
135
136
|
switch (column) {
|
|
137
|
+
case 'image': {
|
|
138
|
+
const image = getSbomReportImage(resource)
|
|
139
|
+
return (
|
|
140
|
+
<span className="text-sm text-theme-text-secondary truncate block" title={image}>{image}</span>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
136
143
|
case 'container':
|
|
137
144
|
return <span className="text-sm text-theme-text-secondary">{getSbomReportContainer(resource)}</span>
|
|
138
145
|
case 'components': {
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
getVSLConfig,
|
|
31
31
|
getBackupRepositoryStatus,
|
|
32
32
|
getBackupRepositoryType,
|
|
33
|
+
getBackupRepositoryVolumeNamespace,
|
|
33
34
|
} from '../resource-utils-velero'
|
|
34
35
|
|
|
35
36
|
// The detail-page Phase value. The badge carries the same label the table
|
|
@@ -235,6 +236,8 @@ export function BackupRepositoryCell({ resource, column }: { resource: any; colu
|
|
|
235
236
|
</span>
|
|
236
237
|
)
|
|
237
238
|
}
|
|
239
|
+
case 'volumeNamespace':
|
|
240
|
+
return <span className="text-sm text-theme-text-secondary">{getBackupRepositoryVolumeNamespace(resource)}</span>
|
|
238
241
|
case 'repositoryType': {
|
|
239
242
|
const type = getBackupRepositoryType(resource)
|
|
240
243
|
// restic is on its way out (no new backups since v1.17, restore dropped in
|
|
@@ -174,6 +174,12 @@ describe('IPPool derivations', () => {
|
|
|
174
174
|
getCalicoIPPoolAllowedUses({ spec: { allowedUses: ['Tunnel'] } }),
|
|
175
175
|
).toBe('Tunnel')
|
|
176
176
|
})
|
|
177
|
+
|
|
178
|
+
it('treats an empty allowedUses list as the default, not as no uses', () => {
|
|
179
|
+
// Calico: "If not specified or empty, defaults to ["Tunnel", "Workload"]".
|
|
180
|
+
// A dash here would read as a pool that allocates for nothing.
|
|
181
|
+
expect(getCalicoIPPoolAllowedUses({ spec: { allowedUses: [] } })).toBe('Workload, Tunnel')
|
|
182
|
+
})
|
|
177
183
|
})
|
|
178
184
|
|
|
179
185
|
describe('derived policy types', () => {
|
|
@@ -256,6 +256,10 @@ export function getCalicoIPPoolEncapsulation(pool: any): string {
|
|
|
256
256
|
|
|
257
257
|
export function getCalicoIPPoolAllowedUses(pool: any): string {
|
|
258
258
|
const uses = pool?.spec?.allowedUses
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
// Calico documents an empty list as equivalent to an absent one: "If not
|
|
260
|
+
// specified or empty, defaults to ["Tunnel", "Workload"] for
|
|
261
|
+
// back-compatibility." Rendering a dash there would read as a pool that
|
|
262
|
+
// allocates for nothing.
|
|
263
|
+
if (!Array.isArray(uses) || uses.length === 0) return 'Workload, Tunnel'
|
|
264
|
+
return uses.map(String).join(', ')
|
|
261
265
|
}
|
|
@@ -126,7 +126,7 @@ export function getOrderState(order: any): StatusBadge {
|
|
|
126
126
|
case 'valid':
|
|
127
127
|
return { text: 'Valid', color: healthColors.healthy, level: 'healthy' }
|
|
128
128
|
case 'ready':
|
|
129
|
-
return { text: 'Ready', color: '
|
|
129
|
+
return { text: 'Ready', color: 'status-blue', level: 'healthy' }
|
|
130
130
|
case 'pending':
|
|
131
131
|
return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
|
|
132
132
|
case 'invalid':
|
|
@@ -162,7 +162,7 @@ export function getChallengeState(challenge: any): StatusBadge {
|
|
|
162
162
|
case 'valid':
|
|
163
163
|
return { text: 'Valid', color: healthColors.healthy, level: 'healthy' }
|
|
164
164
|
case 'ready':
|
|
165
|
-
return { text: 'Ready', color: '
|
|
165
|
+
return { text: 'Ready', color: 'status-blue', level: 'healthy' }
|
|
166
166
|
case 'pending':
|
|
167
167
|
return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
|
|
168
168
|
case 'processing':
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { getSecretStoreProviderType, getSecretStoreProviderKey } from './resource-utils-eso'
|
|
2
|
+
import { getSecretStoreProviderType, getSecretStoreProviderKey, getExternalSecretStoreKey } from './resource-utils-eso'
|
|
3
3
|
|
|
4
4
|
// The provider label answers "which backend holds these secrets?", so it has to
|
|
5
5
|
// track the upstream `spec.provider` field names rather than plausible ones.
|
|
@@ -54,3 +54,28 @@ describe('getSecretStoreProviderKey', () => {
|
|
|
54
54
|
expect(key('ParameterStore')).toBe('aws')
|
|
55
55
|
})
|
|
56
56
|
})
|
|
57
|
+
|
|
58
|
+
describe('getExternalSecretStoreKey', () => {
|
|
59
|
+
// A ClusterSecretStore is referenceable from any namespace, so keying it by
|
|
60
|
+
// namespace would miss every ExternalSecret outside the store's own.
|
|
61
|
+
it('keys a ClusterSecretStore by name alone', () => {
|
|
62
|
+
expect(getExternalSecretStoreKey({
|
|
63
|
+
metadata: { namespace: 'prod' },
|
|
64
|
+
spec: { secretStoreRef: { name: 'vault-global', kind: 'ClusterSecretStore' } },
|
|
65
|
+
})).toBe('vault-global')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('scopes a SecretStore to the ExternalSecret namespace', () => {
|
|
69
|
+
expect(getExternalSecretStoreKey({
|
|
70
|
+
metadata: { namespace: 'prod' },
|
|
71
|
+
spec: { secretStoreRef: { name: 'aws', kind: 'SecretStore' } },
|
|
72
|
+
})).toBe('prod/aws')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('treats an omitted kind as a SecretStore, as the CRD defaults it', () => {
|
|
76
|
+
expect(getExternalSecretStoreKey({
|
|
77
|
+
metadata: { namespace: 'prod' },
|
|
78
|
+
spec: { secretStoreRef: { name: 'aws' } },
|
|
79
|
+
})).toBe('prod/aws')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -235,6 +235,19 @@ export function getExternalSecretStore(resource: any): { name: string; kind: str
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Key for looking this ExternalSecret's store up in a provider map.
|
|
240
|
+
*
|
|
241
|
+
* A ClusterSecretStore is cluster-scoped and referenceable from any namespace,
|
|
242
|
+
* so it keys by name alone; a SecretStore only resolves within the
|
|
243
|
+
* ExternalSecret's own namespace.
|
|
244
|
+
*/
|
|
245
|
+
export function getExternalSecretStoreKey(resource: any): string {
|
|
246
|
+
const store = getExternalSecretStore(resource)
|
|
247
|
+
if (store.kind === 'ClusterSecretStore') return store.name
|
|
248
|
+
return `${resource?.metadata?.namespace ?? ''}/${store.name}`
|
|
249
|
+
}
|
|
250
|
+
|
|
238
251
|
export function getExternalSecretRefreshInterval(resource: any): string {
|
|
239
252
|
return resource.spec?.refreshInterval || '-'
|
|
240
253
|
}
|
|
@@ -62,14 +62,14 @@ export function getRevisionStatus(resource: any): StatusBadge {
|
|
|
62
62
|
if (ready?.status === 'True' && active?.status === 'False') {
|
|
63
63
|
const reason = active.reason || ''
|
|
64
64
|
if (reason === 'NoTraffic') {
|
|
65
|
-
return { text: 'Scaled to Zero', color: '
|
|
65
|
+
return { text: 'Scaled to Zero', color: 'status-blue', level: 'healthy' }
|
|
66
66
|
}
|
|
67
|
-
return { text: reason || 'Inactive', color: '
|
|
67
|
+
return { text: reason || 'Inactive', color: 'status-amber', level: 'degraded' }
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// Check for activating: Active condition is Unknown (scaling up)
|
|
71
71
|
if (ready?.status === 'True' && active?.status === 'Unknown') {
|
|
72
|
-
return { text: 'Activating', color: '
|
|
72
|
+
return { text: 'Activating', color: 'status-blue', level: 'healthy' }
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return getKnativeConditionStatus(resource)
|
|
@@ -102,6 +102,9 @@ export const getExposedSecretReportSummary = getVulnerabilityReportSummary
|
|
|
102
102
|
export const getExposedSecretReportContainer = getVulnerabilityReportContainer
|
|
103
103
|
export const getExposedSecretReportImage = getVulnerabilityReportImage
|
|
104
104
|
|
|
105
|
+
// SBOM reports carry the same report.artifact/registry shape as the scan reports.
|
|
106
|
+
export const getSbomReportImage = getVulnerabilityReportImage
|
|
107
|
+
|
|
105
108
|
export const getExposedSecretReportStatus = getVulnerabilityReportStatus
|
|
106
109
|
|
|
107
110
|
// ============================================================================
|
|
@@ -11,7 +11,8 @@ import { getNodePoolStatus, getNodeClaimStatus } from './resource-utils-karpente
|
|
|
11
11
|
import { getScaledObjectStatus, getScaledJobStatus } from './resource-utils-keda'
|
|
12
12
|
import { getGitRepositoryStatus, getOCIRepositoryStatus, getHelmRepositoryStatus, getHelmRepositoryType, getKustomizationStatus, getFluxHelmReleaseStatus, getFluxAlertStatus } from './resource-utils-flux'
|
|
13
13
|
import { getArgoApplicationStatus, getArgoApplicationSetStatus, getArgoApplicationSync, getArgoApplicationHealth, getArgoApplicationProject } from './resource-utils-argo'
|
|
14
|
-
import {
|
|
14
|
+
import { getVulnerabilityReportImage as _getVulnerabilityReportImage } from './resource-utils-trivy'
|
|
15
|
+
import { getKyvernoPolicyRuleTypes as _getKyvernoPolicyRuleTypes, getPolicyReportStatus as _getPolicyReportStatus, getKyvernoPolicyStatus as _getKyvernoPolicyStatus } from './resource-utils-kyverno'
|
|
15
16
|
import { getResourceClaimStatus as _getResourceClaimStatus, getResourceClaimDeviceClasses as _getResourceClaimDeviceClasses, getResourceClaimTemplateDeviceClasses as _getResourceClaimTemplateDeviceClasses, getResourceClaimAllocation as _getResourceClaimAllocation, getResourceClaimReservedFor as _getResourceClaimReservedFor } from './resource-utils-dra'
|
|
16
17
|
import { getNvidiaClusterPolicyStatus as _getNvidiaClusterPolicyStatus, getNvidiaClusterPolicyEnabledComponents as _getNvidiaClusterPolicyEnabledComponents, getNvidiaDriverStatus as _getNvidiaDriverStatus } from './resource-utils-nvidia'
|
|
17
18
|
import { getClusterQueueStatus as _getClusterQueueStatus, getLocalQueueStatus as _getLocalQueueStatus, getKueueWorkloadStatus as _getKueueWorkloadStatus, getResourceFlavorStatus as _getResourceFlavorStatus, getAdmissionCheckStatus as _getAdmissionCheckStatus, getProvisioningRequestStatus as _getProvisioningRequestStatus } from './resource-utils-kueue'
|
|
@@ -2594,6 +2595,20 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
2594
2595
|
case 'mig':
|
|
2595
2596
|
if (kindLower === 'nvidiaclusterpolicies') return resource.spec?.mig?.strategy || ''
|
|
2596
2597
|
break
|
|
2598
|
+
// Trivy keeps the scanned image under report.artifact, so the generic
|
|
2599
|
+
// spec/status probe finds nothing and the header would offer a filter
|
|
2600
|
+
// button that never populates.
|
|
2601
|
+
case 'image':
|
|
2602
|
+
if (kindLower === 'vulnerabilityreports' || kindLower === 'sbomreports'
|
|
2603
|
+
|| kindLower === 'clustersbomreports' || kindLower === 'exposedsecretreports') {
|
|
2604
|
+
return _getVulnerabilityReportImage(resource)
|
|
2605
|
+
}
|
|
2606
|
+
break
|
|
2607
|
+
case 'ruleTypes':
|
|
2608
|
+
if (kindLower === 'kyvernopolicies' || kindLower === 'clusterpolicies') {
|
|
2609
|
+
return _getKyvernoPolicyRuleTypes(resource)
|
|
2610
|
+
}
|
|
2611
|
+
break
|
|
2597
2612
|
// The fallback would read spec.timeZone and yield '' for an undeclared
|
|
2598
2613
|
// zone, so those rows would drop out of a filter the cell shows a value for.
|
|
2599
2614
|
case 'timeZone':
|
|
@@ -2622,6 +2637,7 @@ export {
|
|
|
2622
2637
|
getVulnerabilityReportSummary,
|
|
2623
2638
|
getVulnerabilityReportStatus,
|
|
2624
2639
|
getVulnerabilityReportImage,
|
|
2640
|
+
getSbomReportImage,
|
|
2625
2641
|
getVulnerabilityReportContainer,
|
|
2626
2642
|
getConfigAuditReportSummary,
|
|
2627
2643
|
getConfigAuditReportStatus,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getCellFilterValue } from './resource-utils'
|
|
2
3
|
import { getCellFilterKind, hasCuratedColumns, isColumnFilterableByDistinctCount, normalizeKindToPlural, SKIP_FILTER_COLUMNS } from './ResourcesView'
|
|
3
4
|
|
|
4
5
|
describe('isColumnFilterableByDistinctCount', () => {
|
|
@@ -98,3 +99,22 @@ describe('GPU ecosystem column routing', () => {
|
|
|
98
99
|
expect(hasCuratedColumns('inferenceobjectives', 'example.io')).toBe(false)
|
|
99
100
|
})
|
|
100
101
|
})
|
|
102
|
+
|
|
103
|
+
describe('filter values for columns whose data sits outside spec/status', () => {
|
|
104
|
+
// getCellFilterValue's fallback probes status[key]/spec[key]. A column whose
|
|
105
|
+
// value lives elsewhere needs an explicit case, or the header reserves a
|
|
106
|
+
// filter button that can never populate.
|
|
107
|
+
it('reads the scanned image out of the Trivy report', () => {
|
|
108
|
+
const report = {
|
|
109
|
+
report: { registry: { server: 'ghcr.io' }, artifact: { repository: 'acme/api', tag: 'v2' } },
|
|
110
|
+
}
|
|
111
|
+
for (const kind of ['vulnerabilityreports', 'sbomreports', 'clustersbomreports']) {
|
|
112
|
+
expect(getCellFilterValue(report, 'image', kind)).toContain('acme/api')
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('reads Kyverno rule types out of the rules array', () => {
|
|
117
|
+
const policy = { spec: { rules: [{ validate: {} }, { mutate: {} }] } }
|
|
118
|
+
expect(getCellFilterValue(policy, 'ruleTypes', 'clusterpolicies')).toBe('validate, mutate')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -245,6 +245,23 @@ interface TopologyGraphProps {
|
|
|
245
245
|
children?: ReactNode
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// A pod group's children normally carry the health the server computed. Without
|
|
249
|
+
// it, the phase is honest for every state except Running: a crash-looping pod
|
|
250
|
+
// sits at Phase=Running with its container restarting, so that one case says
|
|
251
|
+
// unknown rather than repeating the bug this fallback exists behind.
|
|
252
|
+
export function phaseOnlyHealth(phase: string | undefined): HealthStatus {
|
|
253
|
+
switch (phase) {
|
|
254
|
+
case 'Failed':
|
|
255
|
+
return 'unhealthy'
|
|
256
|
+
case 'Pending':
|
|
257
|
+
return 'degraded'
|
|
258
|
+
case 'Succeeded':
|
|
259
|
+
return 'neutral'
|
|
260
|
+
default:
|
|
261
|
+
return 'unknown'
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
248
265
|
export function TopologyGraph({
|
|
249
266
|
topology,
|
|
250
267
|
viewMode,
|
|
@@ -430,7 +447,7 @@ export function TopologyGraph({
|
|
|
430
447
|
// the kubelet's Waiting->Running oscillation, which the phase alone
|
|
431
448
|
// hides — a crash-looping pod sits at Phase=Running. Deriving it here
|
|
432
449
|
// again would rebuild that logic in a second place and get it wrong.
|
|
433
|
-
status: pod.status ??
|
|
450
|
+
status: pod.status ?? phaseOnlyHealth(pod.phase),
|
|
434
451
|
data: {
|
|
435
452
|
...podGroupNode.data,
|
|
436
453
|
namespace: pod.namespace,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { phaseOnlyHealth } from './TopologyGraph'
|
|
3
|
+
|
|
4
|
+
// Only reached when the server sent no per-pod status — an older Radar behind a
|
|
5
|
+
// newer embedding host. The point of the fallback is that it must not repeat the
|
|
6
|
+
// bug it stands in for.
|
|
7
|
+
describe('phaseOnlyHealth', () => {
|
|
8
|
+
it('does not call a Running pod healthy', () => {
|
|
9
|
+
// A crash-looping pod sits at Phase=Running with its container restarting,
|
|
10
|
+
// so Running is the one phase that hides a fault.
|
|
11
|
+
expect(phaseOnlyHealth('Running')).toBe('unknown')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('reads the phases that are honest on their own', () => {
|
|
15
|
+
expect(phaseOnlyHealth('Failed')).toBe('unhealthy')
|
|
16
|
+
expect(phaseOnlyHealth('Pending')).toBe('degraded')
|
|
17
|
+
expect(phaseOnlyHealth('Succeeded')).toBe('neutral')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('says nothing for a phase it does not recognise', () => {
|
|
21
|
+
expect(phaseOnlyHealth(undefined)).toBe('unknown')
|
|
22
|
+
expect(phaseOnlyHealth('SomeFuturePhase')).toBe('unknown')
|
|
23
|
+
})
|
|
24
|
+
})
|
package/src/theme/components.css
CHANGED
|
@@ -228,6 +228,35 @@
|
|
|
228
228
|
border-color: transparent;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/* Colour-named accents carry no health meaning — they exist so an
|
|
232
|
+
informational badge can pick a hue without borrowing a status semantic.
|
|
233
|
+
A categorical badge (an admission verb, a policy action, a configured
|
|
234
|
+
mode) belongs here; only evidence about a resource's condition belongs on
|
|
235
|
+
.status-healthy / -degraded / -unhealthy. */
|
|
236
|
+
.status-green {
|
|
237
|
+
background-color: light-dark(rgba(34, 197, 94, 0.15), rgba(34, 197, 94, 0.2));
|
|
238
|
+
color: light-dark(#15803d, #4ade80);
|
|
239
|
+
border-color: transparent;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.status-red {
|
|
243
|
+
background-color: light-dark(rgba(239, 68, 68, 0.15), rgba(239, 68, 68, 0.2));
|
|
244
|
+
color: light-dark(#b91c1c, #f87171);
|
|
245
|
+
border-color: transparent;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.status-amber {
|
|
249
|
+
background-color: light-dark(rgba(234, 179, 8, 0.15), rgba(234, 179, 8, 0.2));
|
|
250
|
+
color: light-dark(#a16207, #facc15);
|
|
251
|
+
border-color: transparent;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.status-blue {
|
|
255
|
+
background-color: light-dark(rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.2));
|
|
256
|
+
color: light-dark(#2563eb, #60a5fa);
|
|
257
|
+
border-color: transparent;
|
|
258
|
+
}
|
|
259
|
+
|
|
231
260
|
.status-cyan {
|
|
232
261
|
background-color: light-dark(rgba(6, 182, 212, 0.15), rgba(6, 182, 212, 0.2));
|
|
233
262
|
color: light-dark(#0891b2, #22d3ee);
|
|
@@ -74,7 +74,7 @@ export const NODEPOOL_MODE_BADGE: Record<string, string> = {
|
|
|
74
74
|
|
|
75
75
|
// Translucent gray badge for "inactive / unknown / pending / unset / disabled" states.
|
|
76
76
|
// The de-facto fallback in renderers when no severity or category applies.
|
|
77
|
-
export const BADGE_INACTIVE = '
|
|
77
|
+
export const BADGE_INACTIVE = 'status-unknown'
|
|
78
78
|
|
|
79
79
|
// Best practices category colors
|
|
80
80
|
export const BP_CATEGORY_BADGE: Record<string, string> = {
|
package/src/utils/index.ts
CHANGED