@skyhook-io/k8s-ui 1.14.1 → 1.14.3
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 +1 -1
- package/src/components/issues/IssuesView.tsx +1 -1
- package/src/components/issues/types.ts +2 -0
- package/src/components/resources/ResourcesView.tsx +261 -188
- package/src/components/resources/column-settings-merge.test.ts +104 -0
- package/src/components/resources/istio-gateway-filter.test.ts +5 -3
- package/src/components/resources/istio-policy-accessors.test.ts +193 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +11 -24
- 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/eso-cells.tsx +0 -5
- package/src/components/resources/renderers/istio-cells.tsx +36 -26
- package/src/components/resources/renderers/knative-cells.tsx +13 -0
- package/src/components/resources/renderers/kueue-cells.tsx +0 -9
- package/src/components/resources/renderers/kyverno-cells.tsx +8 -1
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +6 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +3 -3
- package/src/components/resources/renderers/ray-cells.tsx +12 -4
- package/src/components/resources/renderers/trivy-cells.tsx +7 -0
- package/src/components/resources/renderers/velero-cells.tsx +3 -0
- package/src/components/resources/renderers/volcano-cells.tsx +0 -9
- 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-cnpg.ts +5 -0
- package/src/components/resources/resource-utils-eso.test.ts +56 -0
- package/src/components/resources/resource-utils-eso.ts +37 -5
- package/src/components/resources/resource-utils-istio.ts +152 -44
- package/src/components/resources/resource-utils-kyverno.ts +3 -6
- package/src/components/resources/resource-utils-prometheus.ts +51 -23
- package/src/components/resources/resource-utils-ray.test.ts +36 -0
- package/src/components/resources/resource-utils-ray.ts +6 -0
- package/src/components/resources/resource-utils-trivy.ts +3 -0
- package/src/components/resources/resource-utils.ts +183 -19
- package/src/components/resources/resources-column-filter.test.ts +20 -0
- package/src/components/resources/status-accessors.test.ts +363 -0
- package/src/components/resources/status-overclaim.test.ts +158 -0
- package/src/components/timeline/DiffViewer.test.tsx +26 -0
- package/src/components/timeline/DiffViewer.tsx +16 -12
- package/src/components/topology/TopologyGraph.tsx +7 -2
- package/src/components/ui/Collapse.tsx +9 -2
- package/src/components/ui/ConfirmDialog.test.tsx +37 -0
- package/src/components/ui/ConfirmDialog.tsx +3 -1
- package/src/components/ui/drawer-components.tsx +1 -1
- package/src/components/workload/WorkloadView.tsx +2 -1
|
@@ -7,31 +7,59 @@ import { healthColors } from './resource-utils'
|
|
|
7
7
|
// SHARED HELPERS
|
|
8
8
|
// ============================================================================
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
// ServiceMonitor, PodMonitor and PrometheusRule are configuration resources.
|
|
11
|
+
// They have no `status.conditions`: the operator reports per selecting workload
|
|
12
|
+
// under `status.bindings[].conditions`, the only condition type is `Accepted`,
|
|
13
|
+
// and it writes any of it only when the StatusForConfigurationResources feature
|
|
14
|
+
// gate is on. Absence is therefore the ordinary case and is not evidence the
|
|
15
|
+
// configuration works — and `Accepted` means the config was accepted, not that
|
|
16
|
+
// any target is being scraped.
|
|
17
|
+
function getConfigResourceStatus(resource: any): StatusBadge {
|
|
18
|
+
const generation = resource?.metadata?.generation
|
|
19
|
+
let reporting = 0
|
|
20
|
+
let accepted = 0
|
|
21
|
+
let rejected = 0
|
|
22
|
+
let rejection = ''
|
|
23
|
+
|
|
24
|
+
for (const binding of resource?.status?.bindings || []) {
|
|
25
|
+
for (const cond of binding?.conditions || []) {
|
|
26
|
+
if (cond?.type !== 'Accepted') continue
|
|
27
|
+
// A condition observed against an older spec does not describe the object
|
|
28
|
+
// as it stands now.
|
|
29
|
+
if (generation !== undefined && cond.observedGeneration !== undefined
|
|
30
|
+
&& cond.observedGeneration !== generation) continue
|
|
31
|
+
reporting++
|
|
32
|
+
if (cond.status === 'True') accepted++
|
|
33
|
+
else if (cond.status === 'False') {
|
|
34
|
+
rejected++
|
|
35
|
+
if (!rejection) rejection = cond.reason || ''
|
|
36
|
+
}
|
|
37
|
+
}
|
|
19
38
|
}
|
|
20
39
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return { text: 'Available', color: healthColors.healthy, level: 'healthy' }
|
|
40
|
+
if (reporting === 0) {
|
|
41
|
+
return { text: 'Not assessed', color: healthColors.unknown, level: 'unknown' }
|
|
24
42
|
}
|
|
25
|
-
if (
|
|
26
|
-
return { text:
|
|
43
|
+
if (rejected === reporting) {
|
|
44
|
+
return { text: rejection || 'Not Accepted', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
27
45
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
if (rejected > 0) {
|
|
47
|
+
return {
|
|
48
|
+
text: `Accepted by ${accepted}/${reporting}`,
|
|
49
|
+
color: healthColors.degraded,
|
|
50
|
+
level: 'degraded',
|
|
51
|
+
}
|
|
32
52
|
}
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
// No rejection, but a workload reported Accepted=Unknown: it has not decided,
|
|
54
|
+
// so the resource is not accepted everywhere that selects it.
|
|
55
|
+
if (accepted < reporting) {
|
|
56
|
+
return {
|
|
57
|
+
text: `Accepted by ${accepted}/${reporting}`,
|
|
58
|
+
color: healthColors.unknown,
|
|
59
|
+
level: 'unknown',
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { text: 'Accepted', color: healthColors.healthy, level: 'healthy' }
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
function formatMatchLabels(selector: any): string {
|
|
@@ -45,7 +73,7 @@ function formatMatchLabels(selector: any): string {
|
|
|
45
73
|
// ============================================================================
|
|
46
74
|
|
|
47
75
|
export function getServiceMonitorStatus(resource: any): StatusBadge {
|
|
48
|
-
return
|
|
76
|
+
return getConfigResourceStatus(resource)
|
|
49
77
|
}
|
|
50
78
|
|
|
51
79
|
export function getServiceMonitorEndpointCount(resource: any): number {
|
|
@@ -87,7 +115,7 @@ export function getServiceMonitorNamespaceSelector(resource: any): string {
|
|
|
87
115
|
// ============================================================================
|
|
88
116
|
|
|
89
117
|
export function getPrometheusRuleStatus(resource: any): StatusBadge {
|
|
90
|
-
return
|
|
118
|
+
return getConfigResourceStatus(resource)
|
|
91
119
|
}
|
|
92
120
|
|
|
93
121
|
export function getPrometheusRuleGroupCount(resource: any): number {
|
|
@@ -169,7 +197,7 @@ export function getPrometheusRuleGroups(resource: any): PrometheusRuleGroup[] {
|
|
|
169
197
|
// ============================================================================
|
|
170
198
|
|
|
171
199
|
export function getPodMonitorStatus(resource: any): StatusBadge {
|
|
172
|
-
return
|
|
200
|
+
return getConfigResourceStatus(resource)
|
|
173
201
|
}
|
|
174
202
|
|
|
175
203
|
export function getPodMonitorEndpointCount(resource: any): number {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getRayCronJobTimeZone, getRayCronJobStatus } from './resource-utils-ray'
|
|
3
|
+
import { getCellFilterValue } from './resource-utils'
|
|
4
|
+
|
|
5
|
+
describe('getRayCronJobTimeZone', () => {
|
|
6
|
+
it('shows the declared zone', () => {
|
|
7
|
+
expect(getRayCronJobTimeZone({ spec: { schedule: '0 2 * * *', timeZone: 'America/New_York' } }))
|
|
8
|
+
.toBe('America/New_York')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('does not present an undeclared zone as UTC', () => {
|
|
12
|
+
// KubeRay reads the schedule in the operator pod's local zone when timeZone
|
|
13
|
+
// is unset, so the object cannot say what the cron expression means.
|
|
14
|
+
expect(getRayCronJobTimeZone({ spec: { schedule: '0 2 * * *' } })).toBe('Operator local')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('getRayCronJobStatus', () => {
|
|
19
|
+
it('reports only what spec.suspend says, which is all the CRD carries', () => {
|
|
20
|
+
// RayCronJobStatus holds lastScheduleTime and nothing else — there is no
|
|
21
|
+
// controller-reported phase for this badge to reflect.
|
|
22
|
+
expect(getRayCronJobStatus({ spec: { suspend: true } }).text).toBe('Suspended')
|
|
23
|
+
expect(getRayCronJobStatus({ spec: {} }).text).toBe('Active')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('getCellFilterValue for raycronjobs timeZone', () => {
|
|
28
|
+
it('offers the same value the cell renders, so undeclared rows stay filterable', () => {
|
|
29
|
+
// The generic fallback reads spec.timeZone and yields '' when unset, which
|
|
30
|
+
// would hide those rows from a filter whose column shows them a value.
|
|
31
|
+
expect(getCellFilterValue({ spec: { schedule: '0 2 * * *' } }, 'timeZone', 'raycronjobs'))
|
|
32
|
+
.toBe('Operator local')
|
|
33
|
+
expect(getCellFilterValue({ spec: { timeZone: 'Europe/Berlin' } }, 'timeZone', 'raycronjobs'))
|
|
34
|
+
.toBe('Europe/Berlin')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -203,6 +203,12 @@ export function getRayCronJobSchedule(resource: any): string {
|
|
|
203
203
|
return resource.spec?.schedule || '-'
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// An unset timeZone does not mean UTC: KubeRay reads the schedule in the
|
|
207
|
+
// operator pod's own local zone, which this object cannot report.
|
|
208
|
+
export function getRayCronJobTimeZone(resource: any): string {
|
|
209
|
+
return resource.spec?.timeZone || 'Operator local'
|
|
210
|
+
}
|
|
211
|
+
|
|
206
212
|
export function getRayCronJobSuspend(resource: any): boolean {
|
|
207
213
|
return resource.spec?.suspend === true
|
|
208
214
|
}
|
|
@@ -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,11 +11,12 @@ 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'
|
|
18
|
-
import { getRayClusterStatus as _getRayClusterStatus, getRayJobStatus as _getRayJobStatus, getRayServiceStatus as _getRayServiceStatus, getRayCronJobStatus as _getRayCronJobStatus } from './resource-utils-ray'
|
|
19
|
+
import { getRayClusterStatus as _getRayClusterStatus, getRayJobStatus as _getRayJobStatus, getRayServiceStatus as _getRayServiceStatus, getRayCronJobStatus as _getRayCronJobStatus, getRayCronJobTimeZone as _getRayCronJobTimeZone } from './resource-utils-ray'
|
|
19
20
|
import { getLeaderWorkerSetStatus as _getLeaderWorkerSetStatus, getJobSetStatus as _getJobSetStatus } from './resource-utils-jobset-lws'
|
|
20
21
|
import { getInferenceServiceStatus as _getInferenceServiceStatus, getServingRuntimeStatus as _getServingRuntimeStatus, getInferenceGraphStatus as _getInferenceGraphStatus, getTrainedModelStatus as _getTrainedModelStatus, getLLMInferenceServiceStatus as _getLLMInferenceServiceStatus } from './resource-utils-kserve'
|
|
21
22
|
import { getInferencePoolStatus as _getInferencePoolStatus, getInferenceObjectiveStatus as _getInferenceObjectiveStatus } from './resource-utils-inference-gateway'
|
|
@@ -30,8 +31,8 @@ import { getExternalSecretStatus as _getExternalSecretStatus, getClusterExternal
|
|
|
30
31
|
import { getHPATableState, hpaStatusFromState } from './resource-utils-hpa'
|
|
31
32
|
import { getCNPGClusterStatus as _getCNPGClusterStatus, getCNPGBackupStatus as _getCNPGBackupStatus, getCNPGScheduledBackupStatus as _getCNPGScheduledBackupStatus, getCNPGPoolerStatus as _getCNPGPoolerStatus, isApiGroup as _isApiGroup, CNPG_GROUP as _CNPG_GROUP } from './resource-utils-cnpg'
|
|
32
33
|
import { getGenericResourceStatus } from './generic-status'
|
|
33
|
-
import { getIstioGatewayStatus as _getIstioGatewayStatus, getIstioGatewayServerCount as _getIstioGatewayServerCount, getIstioGatewaySelectorString as _getIstioGatewaySelectorString } from './resource-utils-istio'
|
|
34
|
-
import { getCalicoIPPoolAllowedUses, getCalicoIPPoolBlockSize, getCalicoIPPoolEncapsulation, getCalicoPolicyNamespaceSelector, getCalicoPolicyServiceAccountSelector, getCalicoPolicyTypes, isCalicoApiVersion, isCalicoPolicyResource } from './resource-utils-calico'
|
|
34
|
+
import { getIstioGatewayStatus as _getIstioGatewayStatus, getIstioGatewayServerCount as _getIstioGatewayServerCount, getIstioGatewaySelectorString as _getIstioGatewaySelectorString, getAuthorizationPolicySelectorString as _getAuthorizationPolicySelectorString, getDestinationRuleTlsMode as _getDestinationRuleTlsMode } from './resource-utils-istio'
|
|
35
|
+
import { formatKubernetesLabelSelector, getCalicoIPPoolAllowedUses, getCalicoIPPoolBlockSize, getCalicoIPPoolEncapsulation, getCalicoPolicyNamespaceSelector, getCalicoPolicyServiceAccountSelector, getCalicoPolicyTypes, isCalicoApiVersion, isCalicoPolicyResource } from './resource-utils-calico'
|
|
35
36
|
|
|
36
37
|
// ============================================================================
|
|
37
38
|
// STATUS & HEALTH UTILITIES
|
|
@@ -1861,21 +1862,145 @@ export function getGatewayClassDescription(gc: any): string {
|
|
|
1861
1862
|
// GATEWAY API ROUTE UTILITIES (shared by HTTPRoute, GRPCRoute, TCPRoute, TLSRoute)
|
|
1862
1863
|
// ============================================================================
|
|
1863
1864
|
|
|
1865
|
+
const GATEWAY_API_GROUP = 'gateway.networking.k8s.io'
|
|
1866
|
+
|
|
1867
|
+
/**
|
|
1868
|
+
* Whether a status report describes a parent the spec still asks for.
|
|
1869
|
+
*
|
|
1870
|
+
* A controller can leave a report behind after its parentRef is removed, and
|
|
1871
|
+
* the defaults matter: an unset group/kind/namespace on either side means
|
|
1872
|
+
* gateway.networking.k8s.io, Gateway, and the route's own namespace.
|
|
1873
|
+
*/
|
|
1874
|
+
function isReportForParentRef(ref: any, reported: any, routeNamespace: string): boolean {
|
|
1875
|
+
const norm = (r: any) => ({
|
|
1876
|
+
group: r?.group ?? GATEWAY_API_GROUP,
|
|
1877
|
+
kind: r?.kind ?? 'Gateway',
|
|
1878
|
+
namespace: r?.namespace ?? routeNamespace,
|
|
1879
|
+
name: r?.name,
|
|
1880
|
+
sectionName: r?.sectionName,
|
|
1881
|
+
port: r?.port,
|
|
1882
|
+
})
|
|
1883
|
+
const a = norm(ref)
|
|
1884
|
+
const b = norm(reported)
|
|
1885
|
+
return a.group === b.group && a.kind === b.kind && a.namespace === b.namespace &&
|
|
1886
|
+
a.name === b.name && a.sectionName === b.sectionName && a.port === b.port
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1864
1889
|
// All Gateway API route types share the same status/parents/rules/hostnames structure
|
|
1890
|
+
/**
|
|
1891
|
+
* A route's verdict, from BOTH conditions the API guarantees it carries.
|
|
1892
|
+
*
|
|
1893
|
+
* Accepted alone is not health: a backendRef naming a Service that does not
|
|
1894
|
+
* exist is Accepted=True by design, because the gateway still has to answer the
|
|
1895
|
+
* request — with a 5xx. Reading only Accepted therefore reported the "why is my
|
|
1896
|
+
* route 503ing" case as healthy, beside a Backends column naming the Service
|
|
1897
|
+
* that is missing. An ABSENT ResolvedRefs is not healthy either: GEP-1364 has
|
|
1898
|
+
* routes always carry both, so a missing one means nothing has confirmed the
|
|
1899
|
+
* refs yet.
|
|
1900
|
+
*/
|
|
1901
|
+
/**
|
|
1902
|
+
* The reports that describe parents this route is actually attached to, plus
|
|
1903
|
+
* the readers the verdict uses. Shared with getRouteStatusReason so the badge
|
|
1904
|
+
* and its explanation can never disagree about which parents counted.
|
|
1905
|
+
*/
|
|
1906
|
+
function routeParentEvidence(route: any) {
|
|
1907
|
+
const refs = route.spec?.parentRefs || []
|
|
1908
|
+
const reports = route.status?.parents || []
|
|
1909
|
+
const routeNamespace = route.metadata?.namespace || ''
|
|
1910
|
+
const generation = route.metadata?.generation
|
|
1911
|
+
|
|
1912
|
+
// Status entries are identified by parentRef AND controllerName, so one
|
|
1913
|
+
// parent can carry reports from two controllers while one replaces the other.
|
|
1914
|
+
// All of them count: reading a single report would make the verdict depend on
|
|
1915
|
+
// their order in the array.
|
|
1916
|
+
const reportsFor = (ref: any) =>
|
|
1917
|
+
reports.filter((p: any) => isReportForParentRef(ref, p?.parentRef, routeNamespace))
|
|
1918
|
+
const perRef = refs.map(reportsFor)
|
|
1919
|
+
|
|
1920
|
+
// A route can also attach to default Gateways without naming them, in which
|
|
1921
|
+
// case the attachment exists only in status. Those reports are real and their
|
|
1922
|
+
// failures must count. Without that opt-in an unmatched report is a leftover
|
|
1923
|
+
// from a parentRef the spec no longer names, and counting it would resurrect
|
|
1924
|
+
// a verdict for a parent the route has left.
|
|
1925
|
+
const useDefaultGateways = route.spec?.useDefaultGateways
|
|
1926
|
+
const attachedByDefault = useDefaultGateways !== undefined && useDefaultGateways !== 'None'
|
|
1927
|
+
const claimed = new Set(perRef.flat())
|
|
1928
|
+
// Only a Gateway can be a default parent. An unmatched report for anything
|
|
1929
|
+
// else — a mesh Service parent the spec has dropped — is obsolete by
|
|
1930
|
+
// definition, and would otherwise keep voting forever if its controller is
|
|
1931
|
+
// gone.
|
|
1932
|
+
const isDefaultGatewayReport = (p: any) =>
|
|
1933
|
+
(p?.parentRef?.group ?? GATEWAY_API_GROUP) === GATEWAY_API_GROUP &&
|
|
1934
|
+
(p?.parentRef?.kind ?? 'Gateway') === 'Gateway'
|
|
1935
|
+
const considered = attachedByDefault
|
|
1936
|
+
? [...perRef.flat(), ...reports.filter((p: any) => !claimed.has(p) && isDefaultGatewayReport(p))]
|
|
1937
|
+
: perRef.flat()
|
|
1938
|
+
|
|
1939
|
+
// A condition observed against a superseded generation describes a spec that
|
|
1940
|
+
// has since changed, so it cannot confirm the current one. A condition with no
|
|
1941
|
+
// observedGeneration at all is taken at face value.
|
|
1942
|
+
const isCurrent = (c: any) =>
|
|
1943
|
+
c?.observedGeneration === undefined || generation === undefined || c.observedGeneration === generation
|
|
1944
|
+
const conditionOf = (report: any, type: string) =>
|
|
1945
|
+
(report?.conditions || []).filter((c: any) => c?.type === type && isCurrent(c)).pop()
|
|
1946
|
+
const statusOf = (report: any, type: string) => conditionOf(report, type)?.status
|
|
1947
|
+
|
|
1948
|
+
// A report whose conditions all describe a superseded spec says nothing about
|
|
1949
|
+
// the current one, either way — so it neither votes nor confirms the parent it
|
|
1950
|
+
// belongs to. Requiring it to agree would let one leftover from a replaced
|
|
1951
|
+
// controller hold a live healthy parent at Pending; counting it as
|
|
1952
|
+
// confirmation would let the other parents speak for one nothing has
|
|
1953
|
+
// confirmed.
|
|
1954
|
+
const speaksToCurrentSpec = (r: any) =>
|
|
1955
|
+
conditionOf(r, 'Accepted') !== undefined || conditionOf(r, 'ResolvedRefs') !== undefined
|
|
1956
|
+
const current = considered.filter(speaksToCurrentSpec)
|
|
1957
|
+
const everyRefConfirmed = perRef.every((list: any[]) => list.some(speaksToCurrentSpec))
|
|
1958
|
+
|
|
1959
|
+
return { considered, current, everyRefConfirmed, conditionOf, statusOf }
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1865
1962
|
export function getRouteStatus(route: any): StatusBadge {
|
|
1866
|
-
const
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
)
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
)
|
|
1874
|
-
|
|
1875
|
-
|
|
1963
|
+
const { considered, current, everyRefConfirmed, statusOf } = routeParentEvidence(route)
|
|
1964
|
+
|
|
1965
|
+
if (considered.length === 0) return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
1966
|
+
if (current.length === 0) return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
|
|
1967
|
+
const anyFailure = current.some((r: any) =>
|
|
1968
|
+
statusOf(r, 'Accepted') === 'False' || statusOf(r, 'ResolvedRefs') === 'False')
|
|
1969
|
+
|
|
1970
|
+
if (everyRefConfirmed && current.every((r: any) => statusOf(r, 'Accepted') === 'False')) {
|
|
1971
|
+
return { text: 'Not Accepted', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
1972
|
+
}
|
|
1973
|
+
if (anyFailure) {
|
|
1974
|
+
return { text: 'Degraded', color: healthColors.degraded, level: 'degraded' }
|
|
1975
|
+
}
|
|
1976
|
+
if (everyRefConfirmed && current.every((r: any) =>
|
|
1977
|
+
statusOf(r, 'Accepted') === 'True' && statusOf(r, 'ResolvedRefs') === 'True')) {
|
|
1978
|
+
return { text: 'Accepted', color: healthColors.healthy, level: 'healthy' }
|
|
1979
|
+
}
|
|
1876
1980
|
return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
|
|
1877
1981
|
}
|
|
1878
1982
|
|
|
1983
|
+
/**
|
|
1984
|
+
* Why the badge says what it says: which parent is unhappy, and the controller's
|
|
1985
|
+
* own reason. Without it "Degraded" tells an operator something is wrong and
|
|
1986
|
+
* nothing about what — and the whole point of reading ResolvedRefs is that the
|
|
1987
|
+
* answer is usually "the backend you named does not exist".
|
|
1988
|
+
*/
|
|
1989
|
+
export function getRouteStatusReason(route: any): string {
|
|
1990
|
+
const { current, conditionOf } = routeParentEvidence(route)
|
|
1991
|
+
const parts: string[] = []
|
|
1992
|
+
for (const report of current) {
|
|
1993
|
+
const name = report?.parentRef?.name || 'parent'
|
|
1994
|
+
for (const type of ['Accepted', 'ResolvedRefs']) {
|
|
1995
|
+
const c = conditionOf(report, type)
|
|
1996
|
+
if (c?.status !== 'False') continue
|
|
1997
|
+
const detail = [c.reason, c.message].filter(Boolean).join(': ')
|
|
1998
|
+
parts.push(detail ? `${name}: ${detail}` : `${name}: ${type} is false`)
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return parts.join(' · ')
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1879
2004
|
export function getRouteParents(route: any): string {
|
|
1880
2005
|
const refs = route.spec?.parentRefs || []
|
|
1881
2006
|
return refs.map((r: any) => r.name).join(', ') || '-'
|
|
@@ -1954,10 +2079,24 @@ export function getNetworkPolicyRuleCount(np: any): { ingress: number; egress: n
|
|
|
1954
2079
|
}
|
|
1955
2080
|
}
|
|
1956
2081
|
|
|
2082
|
+
/**
|
|
2083
|
+
* Who the policy applies to.
|
|
2084
|
+
*
|
|
2085
|
+
* podSelector is a LabelSelector, so it can target a subset through
|
|
2086
|
+
* matchExpressions alone. Reading only matchLabels reported those policies as
|
|
2087
|
+
* covering the whole namespace — a gap rendered as coverage, on the surface
|
|
2088
|
+
* where that reads as "this namespace is protected, look elsewhere".
|
|
2089
|
+
*/
|
|
1957
2090
|
export function getNetworkPolicySelector(np: any): string {
|
|
1958
|
-
const
|
|
1959
|
-
|
|
1960
|
-
|
|
2091
|
+
const selector = np.spec?.podSelector
|
|
2092
|
+
const hasLabels = Object.keys(selector?.matchLabels ?? {}).length > 0
|
|
2093
|
+
// Count only what the formatter will actually render: it skips malformed
|
|
2094
|
+
// entries, and an expression list of nothing but those would otherwise reach
|
|
2095
|
+
// its empty-case string and describe the same state in different words.
|
|
2096
|
+
const hasExpressions = (Array.isArray(selector?.matchExpressions) ? selector.matchExpressions : [])
|
|
2097
|
+
.some((e: any) => e && typeof e === 'object')
|
|
2098
|
+
if (!hasLabels && !hasExpressions) return 'All pods'
|
|
2099
|
+
return formatKubernetesLabelSelector(selector)
|
|
1961
2100
|
}
|
|
1962
2101
|
|
|
1963
2102
|
// ============================================================================
|
|
@@ -2232,7 +2371,6 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
2232
2371
|
if (kindLower === 'clusterqueues') return _getClusterQueueStatus(resource).text
|
|
2233
2372
|
if (kindLower === 'localqueues') return _getLocalQueueStatus(resource).text
|
|
2234
2373
|
if (kindLower === 'workloads') return _getKueueWorkloadStatus(resource).text
|
|
2235
|
-
if (kindLower === 'resourceflavors') return _getResourceFlavorStatus(resource).text
|
|
2236
2374
|
if (kindLower === 'admissionchecks') return _getAdmissionCheckStatus(resource).text
|
|
2237
2375
|
if (kindLower === 'provisioningrequests') return _getProvisioningRequestStatus(resource).text
|
|
2238
2376
|
if (kindLower === 'rayclusters') return _getRayClusterStatus(resource).text
|
|
@@ -2252,7 +2390,6 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
2252
2390
|
if (kindLower === 'volcanoqueues') return _getVolcanoQueueStatus(resource).text
|
|
2253
2391
|
if (kindLower === 'volcanopodgroups') return _getVolcanoPodGroupStatus(resource).text
|
|
2254
2392
|
if (kindLower === 'jobflows') return _getJobFlowStatus(resource).text
|
|
2255
|
-
if (kindLower === 'jobtemplates') return _getJobTemplateStatus(resource).text
|
|
2256
2393
|
if (kindLower === 'kaiqueues') return _getKaiQueueStatus(resource).text
|
|
2257
2394
|
if (kindLower === 'kaipodgroups') return _getKaiPodGroupStatus(resource).text
|
|
2258
2395
|
if (kindLower === 'kaitoworkspaces') return _getKaitoWorkspaceStatus(resource).text
|
|
@@ -2320,6 +2457,13 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
2320
2457
|
return ''
|
|
2321
2458
|
case 'selector':
|
|
2322
2459
|
if (kindLower === 'istiogateways') return _getIstioGatewaySelectorString(resource)
|
|
2460
|
+
if (kindLower === 'authorizationpolicies') return _getAuthorizationPolicySelectorString(resource)
|
|
2461
|
+
return ''
|
|
2462
|
+
case 'tlsMode':
|
|
2463
|
+
// Distinct from Traefik's boolean 'tls' key, which SKIP_FILTER_COLUMNS
|
|
2464
|
+
// excludes — sharing it would have made this reader unreachable. The
|
|
2465
|
+
// generic fallback cannot reach spec.trafficPolicy.tls.mode.
|
|
2466
|
+
if (kindLower === 'destinationrules') return _getDestinationRuleTlsMode(resource)
|
|
2323
2467
|
return ''
|
|
2324
2468
|
case 'state':
|
|
2325
2469
|
if (kindLower === 'orders') return getOrderState(resource).text
|
|
@@ -2451,6 +2595,25 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
2451
2595
|
case 'mig':
|
|
2452
2596
|
if (kindLower === 'nvidiaclusterpolicies') return resource.spec?.mig?.strategy || ''
|
|
2453
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
|
|
2612
|
+
// The fallback would read spec.timeZone and yield '' for an undeclared
|
|
2613
|
+
// zone, so those rows would drop out of a filter the cell shows a value for.
|
|
2614
|
+
case 'timeZone':
|
|
2615
|
+
if (kindLower === 'raycronjobs') return _getRayCronJobTimeZone(resource)
|
|
2616
|
+
break
|
|
2454
2617
|
}
|
|
2455
2618
|
|
|
2456
2619
|
// Fallback: try common paths
|
|
@@ -2474,6 +2637,7 @@ export {
|
|
|
2474
2637
|
getVulnerabilityReportSummary,
|
|
2475
2638
|
getVulnerabilityReportStatus,
|
|
2476
2639
|
getVulnerabilityReportImage,
|
|
2640
|
+
getSbomReportImage,
|
|
2477
2641
|
getVulnerabilityReportContainer,
|
|
2478
2642
|
getConfigAuditReportSummary,
|
|
2479
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
|
+
})
|