@skyhook-io/k8s-ui 1.14.1 → 1.14.2
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 +129 -67
- 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/eso-cells.tsx +0 -5
- package/src/components/resources/renderers/istio-cells.tsx +36 -26
- package/src/components/resources/renderers/kueue-cells.tsx +0 -9
- package/src/components/resources/renderers/kyverno-cells.tsx +1 -1
- 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/volcano-cells.tsx +0 -9
- 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.ts +166 -18
- 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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mergeSavedVisibleColumns, type Column, type ExtraColumn } from './ResourcesView'
|
|
3
|
+
|
|
4
|
+
// A saved blob records only what is VISIBLE, so a curated key's absence is
|
|
5
|
+
// ambiguous: the user hid it, or it did not exist when they saved. `known` — the
|
|
6
|
+
// key set they were offered at save time — is what separates the two. Without
|
|
7
|
+
// it, every column added to a curated kind is invisible to anyone who has ever
|
|
8
|
+
// touched that kind's column picker.
|
|
9
|
+
|
|
10
|
+
const col = (key: string, defaultVisible?: boolean): Column =>
|
|
11
|
+
defaultVisible === undefined ? { key, label: key } : { key, label: key, defaultVisible }
|
|
12
|
+
|
|
13
|
+
const printer = (key: string, defaultVisible?: boolean): ExtraColumn =>
|
|
14
|
+
({ ...col(key, defaultVisible), render: () => null })
|
|
15
|
+
|
|
16
|
+
describe('mergeSavedVisibleColumns', () => {
|
|
17
|
+
it('shows a curated column added since the blob was written', () => {
|
|
18
|
+
const effective = [col('name'), col('status'), col('lastSuccess')]
|
|
19
|
+
const got = mergeSavedVisibleColumns(['name', 'status'], [], [], effective, ['name', 'status'])
|
|
20
|
+
expect(got.has('lastSuccess')).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('keeps a column the user actually hid hidden', () => {
|
|
24
|
+
// 'status' was offered and is absent from visible — a deliberate hide.
|
|
25
|
+
const effective = [col('name'), col('status')]
|
|
26
|
+
const got = mergeSavedVisibleColumns(['name'], [], [], effective, ['name', 'status'])
|
|
27
|
+
expect(got.has('status')).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('respects defaultVisible:false on a newly added column', () => {
|
|
31
|
+
const effective = [col('name'), col('podIP', false)]
|
|
32
|
+
const got = mergeSavedVisibleColumns(['name'], [], [], effective, ['name'])
|
|
33
|
+
expect(got.has('podIP')).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('leaves blobs written before `known` on the old behaviour', () => {
|
|
37
|
+
// No record of what was offered, so absence stays ambiguous and we don't
|
|
38
|
+
// guess — the next save adopts the current set as the baseline.
|
|
39
|
+
const effective = [col('name'), col('status'), col('lastSuccess')]
|
|
40
|
+
const got = mergeSavedVisibleColumns(['name', 'status'], [], [], effective, undefined)
|
|
41
|
+
expect(got.has('lastSuccess')).toBe(false)
|
|
42
|
+
expect(Array.from(got).sort()).toEqual(['name', 'status'])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('does nothing without the effective column set', () => {
|
|
46
|
+
const got = mergeSavedVisibleColumns(['name'], [], [], undefined, ['name'])
|
|
47
|
+
expect(Array.from(got)).toEqual(['name'])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('still seeds host extras unconditionally', () => {
|
|
51
|
+
const got = mergeSavedVisibleColumns(['name'], ['cluster'], [], [col('name')], ['name'])
|
|
52
|
+
expect(got.has('cluster')).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('still seeds printer columns only when the blob names none', () => {
|
|
56
|
+
const cols = [printer('printer:Phase'), printer('printer:Ready')]
|
|
57
|
+
expect(mergeSavedVisibleColumns(['name'], [], cols).has('printer:Phase')).toBe(true)
|
|
58
|
+
// Once the blob names one, the user has seen the set — absence is a hide.
|
|
59
|
+
expect(mergeSavedVisibleColumns(['name', 'printer:Ready'], [], cols).has('printer:Phase')).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('does not re-show a hidden printer column when known predates the printer table', () => {
|
|
63
|
+
// Printer columns are fetched, so `known` can be written during the window
|
|
64
|
+
// before the table arrives — listing curated keys only. Treating their
|
|
65
|
+
// absence as "not offered yet" would un-hide one the user had hidden.
|
|
66
|
+
const cols = [printer('printer:Phase'), printer('printer:Ready')]
|
|
67
|
+
const effective = [col('name'), ...cols]
|
|
68
|
+
const got = mergeSavedVisibleColumns(
|
|
69
|
+
['name', 'printer:Phase'], [], cols, effective,
|
|
70
|
+
['name'], // written before the printer table loaded
|
|
71
|
+
)
|
|
72
|
+
expect(got.has('printer:Ready')).toBe(false)
|
|
73
|
+
expect(got.has('printer:Phase')).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('treats a persisted known that is not an array as no record at all', () => {
|
|
77
|
+
// The blob is untyped JSON and can be corrupted or hand-edited; a throw
|
|
78
|
+
// here would abort the load effect and strand the table's column state.
|
|
79
|
+
const effective = [col('name'), col('status')]
|
|
80
|
+
const call = () => mergeSavedVisibleColumns(['name'], [], [], effective, {} as unknown as string[])
|
|
81
|
+
expect(call).not.toThrow()
|
|
82
|
+
expect(call().has('status')).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('keeps a column hidden after the offered set shrinks and grows again', () => {
|
|
86
|
+
// Uncurated kinds offer a generic Status until their printer columns arrive,
|
|
87
|
+
// and stop offering it afterwards. If the record were replaced on each save
|
|
88
|
+
// rather than accumulated, that second save would forget Status was ever
|
|
89
|
+
// offered — and a later load without printer data would read the user's
|
|
90
|
+
// hide as "never shown" and undo it.
|
|
91
|
+
const known = ['name', 'namespace', 'status', 'age', 'printer:Phase']
|
|
92
|
+
const effective = [col('name'), col('namespace'), col('status'), col('age')]
|
|
93
|
+
const got = mergeSavedVisibleColumns(['name', 'namespace', 'age'], [], [], effective, known)
|
|
94
|
+
expect(got.has('status')).toBe(false)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('seeds a new curated column alongside printer columns without disturbing them', () => {
|
|
98
|
+
const cols = [printer('printer:Phase')]
|
|
99
|
+
const effective = [col('name'), col('lastSuccess'), printer('printer:Phase')]
|
|
100
|
+
const got = mergeSavedVisibleColumns(['name', 'printer:Phase'], [], cols, effective, ['name', 'printer:Phase'])
|
|
101
|
+
expect(got.has('lastSuccess')).toBe(true)
|
|
102
|
+
expect(got.has('printer:Phase')).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -13,13 +13,15 @@ describe('Istio Gateway filter values', () => {
|
|
|
13
13
|
const tls = gateway({ servers: [{ port: { number: 443 }, tls: { mode: 'SIMPLE' } }] })
|
|
14
14
|
const plain = gateway({ servers: [{ port: { number: 80 } }] })
|
|
15
15
|
const none = gateway({ servers: [] })
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// TLS and plain share a verdict: neither says whether the gateway's
|
|
17
|
+
// selector resolves to a running pod, which is what a status column claims.
|
|
18
|
+
expect(getCellFilterValue(tls, 'status', 'istiogateways')).toBe('Defined')
|
|
19
|
+
expect(getCellFilterValue(plain, 'status', 'istiogateways')).toBe('Defined')
|
|
18
20
|
expect(getCellFilterValue(none, 'status', 'istiogateways')).toBe('No Servers')
|
|
19
21
|
})
|
|
20
22
|
|
|
21
23
|
// Read as a Gateway API Gateway instead, the same object filters as
|
|
22
|
-
// "Unknown" — a value that appears on no row, since the cell shows
|
|
24
|
+
// "Unknown" — a value that appears on no row, since the cell shows Defined.
|
|
23
25
|
// That divergence is what routing the normalized key fixes.
|
|
24
26
|
it('diverges from the row when read under the Gateway API key', () => {
|
|
25
27
|
const g = gateway({ servers: [{ port: { number: 80 } }] })
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getAuthorizationPolicySelectorString,
|
|
4
|
+
getAuthorizationPolicyStatus,
|
|
5
|
+
getDestinationRuleStatus,
|
|
6
|
+
getAuthorizationPolicyRuleNotice,
|
|
7
|
+
getDestinationRuleTlsMode,
|
|
8
|
+
} from './resource-utils-istio'
|
|
9
|
+
|
|
10
|
+
// A request decision is made across every policy selecting a workload, so no
|
|
11
|
+
// single object can establish it. These pin the line between what one policy
|
|
12
|
+
// declares and what the mesh actually does.
|
|
13
|
+
|
|
14
|
+
describe('getAuthorizationPolicyStatus', () => {
|
|
15
|
+
it('marks the deny-all idiom rather than presenting it as permitted', () => {
|
|
16
|
+
// `spec: {}` is Istio's documented deny-all: action defaults to ALLOW, and
|
|
17
|
+
// rules are alternatives, so zero of them match nothing.
|
|
18
|
+
expect(getAuthorizationPolicyStatus({ spec: {} })).toMatchObject({
|
|
19
|
+
text: 'No allow rules', level: 'degraded',
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('treats an explicit rule-less ALLOW the same as the defaulted one', () => {
|
|
24
|
+
expect(getAuthorizationPolicyStatus({ spec: { action: 'ALLOW', rules: [] } })).toMatchObject({
|
|
25
|
+
text: 'No allow rules', level: 'degraded',
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not call a DENY doing its job unhealthy', () => {
|
|
30
|
+
expect(getAuthorizationPolicyStatus({ spec: { action: 'DENY', rules: [{}] } })).toMatchObject({
|
|
31
|
+
text: 'Deny (1 rule)', level: 'neutral',
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('does not present an ALLOW with rules as proof anything is permitted', () => {
|
|
36
|
+
expect(getAuthorizationPolicyStatus({ spec: { action: 'ALLOW', rules: [{}, {}] } })).toMatchObject({
|
|
37
|
+
text: 'Allow (2 rules)', level: 'neutral',
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('keeps AUDIT and CUSTOM neutral — neither blocks nor permits on its own', () => {
|
|
42
|
+
expect(getAuthorizationPolicyStatus({ spec: { action: 'AUDIT', rules: [{}] } }).level).toBe('neutral')
|
|
43
|
+
expect(getAuthorizationPolicyStatus({ spec: { action: 'CUSTOM', rules: [{}] } }).level).toBe('neutral')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('does not claim to recognise an unknown action', () => {
|
|
47
|
+
expect(getAuthorizationPolicyStatus({ spec: { action: 'FUTURE' } })).toMatchObject({
|
|
48
|
+
text: 'FUTURE', level: 'unknown',
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('getAuthorizationPolicySelectorString', () => {
|
|
54
|
+
it('reports a targetRef attachment instead of claiming namespace scope', () => {
|
|
55
|
+
// Waypoint and Gateway attachment in ambient mode uses targetRefs, not
|
|
56
|
+
// labels; reading only the selector called these namespace-wide.
|
|
57
|
+
const p = {
|
|
58
|
+
metadata: { namespace: 'prod' },
|
|
59
|
+
spec: { targetRefs: [{ kind: 'Gateway', name: 'waypoint' }] },
|
|
60
|
+
}
|
|
61
|
+
expect(getAuthorizationPolicySelectorString(p)).toBe('Gateway/waypoint')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('qualifies a target naming another namespace', () => {
|
|
65
|
+
// Rendering only — supported attachments are same-namespace, so this
|
|
66
|
+
// records what the cell shows, not that the attachment would be enforced.
|
|
67
|
+
const p = {
|
|
68
|
+
metadata: { namespace: 'prod' },
|
|
69
|
+
spec: { targetRef: { kind: 'Gateway', name: 'shared', namespace: 'infra' } },
|
|
70
|
+
}
|
|
71
|
+
expect(getAuthorizationPolicySelectorString(p)).toBe('infra/Gateway/shared')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('lists every target', () => {
|
|
75
|
+
const p = {
|
|
76
|
+
metadata: { namespace: 'prod' },
|
|
77
|
+
spec: { targetRefs: [{ kind: 'Gateway', name: 'a' }, { kind: 'Service', name: 'b' }] },
|
|
78
|
+
}
|
|
79
|
+
expect(getAuthorizationPolicySelectorString(p)).toBe('Gateway/a, Service/b')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('falls back to selector labels', () => {
|
|
83
|
+
const p = { metadata: { namespace: 'prod' }, spec: { selector: { matchLabels: { app: 'api' } } } }
|
|
84
|
+
expect(getAuthorizationPolicySelectorString(p)).toBe('app=api')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('names both possible scopes when neither is declared', () => {
|
|
88
|
+
// Which one applies depends on MeshConfig.rootNamespace, which this object
|
|
89
|
+
// cannot see — and the root namespace is configurable, so the conventional
|
|
90
|
+
// name is not evidence.
|
|
91
|
+
expect(getAuthorizationPolicySelectorString({ metadata: { namespace: 'istio-system' }, spec: {} }))
|
|
92
|
+
.toBe('Namespace / mesh scope')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('getDestinationRuleStatus', () => {
|
|
97
|
+
it('flags a rule that applies to nothing', () => {
|
|
98
|
+
expect(getDestinationRuleStatus({ spec: {} })).toMatchObject({ text: 'No Host', level: 'unhealthy' })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('does not restate the subset count as a health verdict', () => {
|
|
102
|
+
expect(getDestinationRuleStatus({ spec: { host: 'api', subsets: [{ name: 'v1' }, { name: 'v2' }] } }))
|
|
103
|
+
.toMatchObject({ text: 'Not assessed', level: 'unknown' })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does not read a declared traffic policy as working', () => {
|
|
107
|
+
expect(getDestinationRuleStatus({ spec: { host: 'api', trafficPolicy: { tls: { mode: 'ISTIO_MUTUAL' } } } }))
|
|
108
|
+
.toMatchObject({ text: 'Not assessed', level: 'unknown' })
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('getDestinationRuleTlsMode', () => {
|
|
113
|
+
it('surfaces a declared DISABLE, whose outcome depends on the server policy', () => {
|
|
114
|
+
// Against a PERMISSIVE PeerAuthentication this sends unencrypted traffic;
|
|
115
|
+
// against a STRICT one the requests fail. The row shows neither.
|
|
116
|
+
expect(getDestinationRuleTlsMode({ spec: { host: 'api', trafficPolicy: { tls: { mode: 'DISABLE' } } } }))
|
|
117
|
+
.toBe('DISABLE')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('shows the declared mode when one is set', () => {
|
|
121
|
+
expect(getDestinationRuleTlsMode({ spec: { trafficPolicy: { tls: { mode: 'ISTIO_MUTUAL' } } } }))
|
|
122
|
+
.toBe('ISTIO_MUTUAL')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('reports nothing declared rather than inventing a default', () => {
|
|
126
|
+
expect(getDestinationRuleTlsMode({ spec: { host: 'api' } })).toBe('-')
|
|
127
|
+
expect(getDestinationRuleTlsMode({ spec: { host: 'api', trafficPolicy: {} } })).toBe('-')
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('getAuthorizationPolicyRuleNotice', () => {
|
|
132
|
+
it('does not claim an empty DENY blocks anything', () => {
|
|
133
|
+
// "If not set, the match will never occur" — a DENY with no rules matches
|
|
134
|
+
// nothing and denies nothing. Calling it Deny All is the dangerous
|
|
135
|
+
// direction: it tells an operator an ineffective policy protects them.
|
|
136
|
+
for (const spec of [{ action: 'DENY' }, { action: 'DENY', rules: [] }]) {
|
|
137
|
+
const notice = getAuthorizationPolicyRuleNotice({ spec })
|
|
138
|
+
expect(notice).toMatchObject({ level: 'info', title: 'No deny rules' })
|
|
139
|
+
expect(notice!.message).not.toMatch(/denies all/i)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('explains an unconditional DENY, which no ALLOW can carve an exception from', () => {
|
|
144
|
+
// rules: [{}] matches every request. DENY is evaluated before ALLOW, so
|
|
145
|
+
// this is not a baseline with exceptions — it blocks everything it selects.
|
|
146
|
+
const notice = getAuthorizationPolicyRuleNotice({ spec: { action: 'DENY', rules: [{}] } })
|
|
147
|
+
expect(notice).toMatchObject({ level: 'info', title: 'Matches all requests' })
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('recognises a blanket DENY written with empty condition lists', () => {
|
|
151
|
+
// from: [] / to: [] / when: [] serialize as present but impose nothing, so
|
|
152
|
+
// the rule still matches every request the policy selects.
|
|
153
|
+
for (const rule of [{ from: [] }, { to: [] }, { when: [] }, { from: [], to: [], when: [] }]) {
|
|
154
|
+
expect(getAuthorizationPolicyRuleNotice({ spec: { action: 'DENY', rules: [rule] } }))
|
|
155
|
+
.toMatchObject({ title: 'Matches all requests' })
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('does not read an unrecognised rule field as unconditional', () => {
|
|
160
|
+
// A future condition type must not be mistaken for the absence of one.
|
|
161
|
+
expect(getAuthorizationPolicyRuleNotice({ spec: { action: 'DENY', rules: [{ someFutureMatcher: [{}] }] } }))
|
|
162
|
+
.toBeNull()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('stays quiet on a DENY whose rules have conditions', () => {
|
|
166
|
+
expect(getAuthorizationPolicyRuleNotice({
|
|
167
|
+
spec: { action: 'DENY', rules: [{ from: [{ source: { namespaces: ['dev'] } }] }] },
|
|
168
|
+
})).toBeNull()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('does not flag an unconditional ALLOW the same way', () => {
|
|
172
|
+
// An ALLOW matching everything is permissive, not a blanket block.
|
|
173
|
+
expect(getAuthorizationPolicyRuleNotice({ spec: { action: 'ALLOW', rules: [{}] } })).toBeNull()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('warns on a rule-less ALLOW without asserting an outage', () => {
|
|
177
|
+
for (const spec of [{}, { action: 'ALLOW', rules: [] }]) {
|
|
178
|
+
const notice = getAuthorizationPolicyRuleNotice({ spec })
|
|
179
|
+
expect(notice).toMatchObject({ level: 'warning', title: 'No allow rules' })
|
|
180
|
+
expect(notice!.message).toMatch(/may still permit/i)
|
|
181
|
+
expect(notice!.message).not.toMatch(/no traffic is allowed/i)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('stays quiet when the policy has rules', () => {
|
|
186
|
+
expect(getAuthorizationPolicyRuleNotice({ spec: { action: 'ALLOW', rules: [{}] } })).toBeNull()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('does not editorialise about AUDIT or CUSTOM', () => {
|
|
190
|
+
expect(getAuthorizationPolicyRuleNotice({ spec: { action: 'AUDIT' } })).toBeNull()
|
|
191
|
+
expect(getAuthorizationPolicyRuleNotice({ spec: { action: 'CUSTOM' } })).toBeNull()
|
|
192
|
+
})
|
|
193
|
+
})
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import { Shield } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, KeyValueBadgeList } from '../../ui/drawer-components'
|
|
3
|
-
import { Badge
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
4
4
|
import {
|
|
5
5
|
getAuthorizationPolicyAction,
|
|
6
6
|
getAuthorizationPolicyRules,
|
|
7
|
+
getAuthorizationPolicyRuleNotice,
|
|
7
8
|
getAuthorizationPolicySelector,
|
|
8
9
|
} from '../resource-utils-istio'
|
|
9
10
|
|
|
10
|
-
const actionSeverity: Record<string, BadgeSeverity> = {
|
|
11
|
-
ALLOW: 'success',
|
|
12
|
-
DENY: 'error',
|
|
13
|
-
CUSTOM: 'info',
|
|
14
|
-
AUDIT: 'warning',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
interface IstioAuthorizationPolicyRendererProps {
|
|
18
12
|
data: any
|
|
19
13
|
}
|
|
@@ -24,25 +18,18 @@ export function IstioAuthorizationPolicyRenderer({ data }: IstioAuthorizationPol
|
|
|
24
18
|
const selector = getAuthorizationPolicySelector(data)
|
|
25
19
|
const hasSelector = Object.keys(selector).length > 0
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
21
|
+
const ruleNotice = getAuthorizationPolicyRuleNotice(data)
|
|
22
|
+
// The action badge is uniformly neutral: an action is a declaration, not a
|
|
23
|
+
// verdict. A green ALLOW would make a deny-all policy look healthiest, and a
|
|
24
|
+
// red DENY would make a control doing its job look failed.
|
|
31
25
|
|
|
32
26
|
return (
|
|
33
27
|
<>
|
|
34
|
-
{
|
|
35
|
-
<AlertBanner
|
|
36
|
-
variant="error"
|
|
37
|
-
title="Deny All"
|
|
38
|
-
message="This policy denies all traffic to the target workload (DENY action with no rules)."
|
|
39
|
-
/>
|
|
40
|
-
)}
|
|
41
|
-
{isAllowNothing && (
|
|
28
|
+
{ruleNotice && (
|
|
42
29
|
<AlertBanner
|
|
43
|
-
variant=
|
|
44
|
-
title=
|
|
45
|
-
message=
|
|
30
|
+
variant={ruleNotice.level}
|
|
31
|
+
title={ruleNotice.title}
|
|
32
|
+
message={ruleNotice.message}
|
|
46
33
|
/>
|
|
47
34
|
)}
|
|
48
35
|
|
|
@@ -50,7 +37,7 @@ export function IstioAuthorizationPolicyRenderer({ data }: IstioAuthorizationPol
|
|
|
50
37
|
<Section title="Authorization Policy" icon={Shield} defaultExpanded>
|
|
51
38
|
<PropertyList>
|
|
52
39
|
<Property label="Action" value={
|
|
53
|
-
<Badge severity=
|
|
40
|
+
<Badge severity="neutral">
|
|
54
41
|
{action}
|
|
55
42
|
</Badge>
|
|
56
43
|
} />
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
getExternalSecretStore,
|
|
7
7
|
getExternalSecretRefreshInterval,
|
|
8
8
|
getExternalSecretLastSync,
|
|
9
|
-
getExternalSecretProvider,
|
|
10
9
|
getClusterExternalSecretStatus,
|
|
11
10
|
getClusterExternalSecretNamespaceCount,
|
|
12
11
|
getClusterExternalSecretFailedCount,
|
|
@@ -33,10 +32,6 @@ export function ExternalSecretCell({ resource, column }: { resource: any; column
|
|
|
33
32
|
</span>
|
|
34
33
|
)
|
|
35
34
|
}
|
|
36
|
-
case 'provider': {
|
|
37
|
-
const provider = getExternalSecretProvider(resource)
|
|
38
|
-
return <span className="text-sm text-theme-text-secondary truncate block">{provider}</span>
|
|
39
|
-
}
|
|
40
35
|
case 'refreshInterval': {
|
|
41
36
|
const interval = getExternalSecretRefreshInterval(resource)
|
|
42
37
|
return <span className="text-sm text-theme-text-secondary">{interval}</span>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Badge, type BadgeSeverity } from '../../ui/Badge'
|
|
5
|
+
import { Tooltip } from '../../ui/Tooltip'
|
|
5
6
|
import {
|
|
6
7
|
getVirtualServiceStatus,
|
|
7
8
|
getVirtualServiceHosts,
|
|
@@ -11,19 +12,19 @@ import {
|
|
|
11
12
|
getDestinationRuleHost,
|
|
12
13
|
getDestinationRuleSubsetCount,
|
|
13
14
|
getDestinationRuleLoadBalancer,
|
|
15
|
+
getDestinationRuleTlsMode,
|
|
14
16
|
getIstioGatewayStatus,
|
|
15
17
|
getIstioGatewayServerCount,
|
|
16
18
|
getIstioGatewaySelectorString,
|
|
17
|
-
getServiceEntryStatus,
|
|
18
19
|
getServiceEntryHosts,
|
|
19
20
|
getServiceEntryLocation,
|
|
20
21
|
getServiceEntryPortsString,
|
|
21
|
-
getPeerAuthenticationStatus,
|
|
22
22
|
getPeerAuthenticationMode,
|
|
23
23
|
getPeerAuthenticationSelectorString,
|
|
24
24
|
getAuthorizationPolicyStatus,
|
|
25
25
|
getAuthorizationPolicyAction,
|
|
26
26
|
getAuthorizationPolicyRuleCount,
|
|
27
|
+
getAuthorizationPolicySelectorString,
|
|
27
28
|
} from '../resource-utils-istio'
|
|
28
29
|
|
|
29
30
|
const modeSeverity: Record<string, BadgeSeverity> = {
|
|
@@ -32,13 +33,6 @@ const modeSeverity: Record<string, BadgeSeverity> = {
|
|
|
32
33
|
DISABLE: 'error',
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const actionSeverity: Record<string, BadgeSeverity> = {
|
|
36
|
-
ALLOW: 'success',
|
|
37
|
-
DENY: 'error',
|
|
38
|
-
CUSTOM: 'info',
|
|
39
|
-
AUDIT: 'warning',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
36
|
export function VirtualServiceCell({ resource, column }: { resource: any; column: string }) {
|
|
43
37
|
switch (column) {
|
|
44
38
|
case 'status': {
|
|
@@ -88,6 +82,13 @@ export function DestinationRuleCell({ resource, column }: { resource: any; colum
|
|
|
88
82
|
const lb = getDestinationRuleLoadBalancer(resource)
|
|
89
83
|
return <span className="text-sm text-theme-text-secondary">{lb}</span>
|
|
90
84
|
}
|
|
85
|
+
case 'tlsMode': {
|
|
86
|
+
// Every mode reads neutral: this is the rule's declaration, not the
|
|
87
|
+
// posture in force. What a DISABLE produces depends on the server's
|
|
88
|
+
// PeerAuthentication — unencrypted traffic, or failed requests.
|
|
89
|
+
const mode = getDestinationRuleTlsMode(resource)
|
|
90
|
+
return <span className="text-sm text-theme-text-secondary">{mode}</span>
|
|
91
|
+
}
|
|
91
92
|
default:
|
|
92
93
|
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
93
94
|
}
|
|
@@ -118,14 +119,6 @@ export function IstioGatewayCell({ resource, column }: { resource: any; column:
|
|
|
118
119
|
|
|
119
120
|
export function ServiceEntryCell({ resource, column }: { resource: any; column: string }) {
|
|
120
121
|
switch (column) {
|
|
121
|
-
case 'status': {
|
|
122
|
-
const status = getServiceEntryStatus(resource)
|
|
123
|
-
return (
|
|
124
|
-
<span className={clsx('badge', status.color)}>
|
|
125
|
-
{status.text}
|
|
126
|
-
</span>
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
122
|
case 'hosts': {
|
|
130
123
|
const hosts = getServiceEntryHosts(resource)
|
|
131
124
|
return <span className="text-sm text-theme-text-secondary truncate block">{hosts}</span>
|
|
@@ -149,14 +142,6 @@ export function ServiceEntryCell({ resource, column }: { resource: any; column:
|
|
|
149
142
|
|
|
150
143
|
export function PeerAuthenticationCell({ resource, column }: { resource: any; column: string }) {
|
|
151
144
|
switch (column) {
|
|
152
|
-
case 'status': {
|
|
153
|
-
const status = getPeerAuthenticationStatus(resource)
|
|
154
|
-
return (
|
|
155
|
-
<span className={clsx('badge', status.color)}>
|
|
156
|
-
{status.text}
|
|
157
|
-
</span>
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
145
|
case 'mode': {
|
|
161
146
|
const mode = getPeerAuthenticationMode(resource)
|
|
162
147
|
return (
|
|
@@ -186,16 +171,41 @@ export function AuthorizationPolicyCell({ resource, column }: { resource: any; c
|
|
|
186
171
|
}
|
|
187
172
|
case 'action': {
|
|
188
173
|
const action = getAuthorizationPolicyAction(resource)
|
|
174
|
+
// Uniformly neutral: an action is a declaration, not a verdict. A green
|
|
175
|
+
// ALLOW would make a deny-all policy look healthiest, and a red DENY
|
|
176
|
+
// would make a control doing its job look failed.
|
|
189
177
|
return (
|
|
190
|
-
<Badge severity=
|
|
178
|
+
<Badge severity="neutral">
|
|
191
179
|
{action}
|
|
192
180
|
</Badge>
|
|
193
181
|
)
|
|
194
182
|
}
|
|
195
183
|
case 'rules': {
|
|
196
184
|
const count = getAuthorizationPolicyRuleCount(resource)
|
|
185
|
+
// Rules are alternatives, so an ALLOW with none of them matches nothing —
|
|
186
|
+
// Istio's deny-all idiom. This is the default-visible cell that carries
|
|
187
|
+
// the warning; the Status column is off by default.
|
|
188
|
+
const allowsNothing = count === 0 && (resource.spec?.action || 'ALLOW') === 'ALLOW'
|
|
189
|
+
// Amber on the count itself: it fits the column and reads at a glance
|
|
190
|
+
// against grey neighbours. The explanation rides on the badge so it does
|
|
191
|
+
// not depend on finding the column header's tooltip.
|
|
192
|
+
if (allowsNothing) {
|
|
193
|
+
return (
|
|
194
|
+
<Tooltip content="Rules are alternatives, so this ALLOW policy matches nothing and permits no traffic on its own. Other ALLOW policies selecting the same workload may still permit requests.">
|
|
195
|
+
<Badge severity="warning">0</Badge>
|
|
196
|
+
</Tooltip>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
197
199
|
return <span className="text-sm text-theme-text-secondary">{count}</span>
|
|
198
200
|
}
|
|
201
|
+
case 'selector': {
|
|
202
|
+
const scope = getAuthorizationPolicySelectorString(resource)
|
|
203
|
+
return (
|
|
204
|
+
<Tooltip content={scope}>
|
|
205
|
+
<span className="text-sm text-theme-text-secondary truncate block">{scope}</span>
|
|
206
|
+
</Tooltip>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
199
209
|
default:
|
|
200
210
|
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
201
211
|
}
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
getKueueWorkloadQueueName,
|
|
16
16
|
getKueueWorkloadAdmittedBy,
|
|
17
17
|
getKueueWorkloadPriority,
|
|
18
|
-
getResourceFlavorStatus,
|
|
19
18
|
getResourceFlavorNodeLabelCount,
|
|
20
19
|
getResourceFlavorTaintCount,
|
|
21
20
|
getAdmissionCheckStatus,
|
|
@@ -112,14 +111,6 @@ export function KueueWorkloadCell({ resource, column }: { resource: any; column:
|
|
|
112
111
|
|
|
113
112
|
export function ResourceFlavorCell({ resource, column }: { resource: any; column: string }) {
|
|
114
113
|
switch (column) {
|
|
115
|
-
case 'status': {
|
|
116
|
-
const status = getResourceFlavorStatus(resource)
|
|
117
|
-
return (
|
|
118
|
-
<span className={clsx('badge', status.color)}>
|
|
119
|
-
{status.text}
|
|
120
|
-
</span>
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
114
|
case 'nodeLabels': {
|
|
124
115
|
const count = getResourceFlavorNodeLabelCount(resource)
|
|
125
116
|
return <span className="text-sm text-theme-text-secondary">{count > 0 ? count : '-'}</span>
|
|
@@ -73,7 +73,7 @@ export function KyvernoPolicyCell({ resource, column }: { resource: any; column:
|
|
|
73
73
|
case 'status': {
|
|
74
74
|
const status = getKyvernoPolicyStatus(resource)
|
|
75
75
|
return (
|
|
76
|
-
<span className={clsx('badge', status.color)}>
|
|
76
|
+
<span className={clsx('badge truncate max-w-full', status.color)} title={status.text}>
|
|
77
77
|
{status.text}
|
|
78
78
|
</span>
|
|
79
79
|
)
|
|
@@ -19,7 +19,7 @@ export function ServiceMonitorCell({ resource, column }: { resource: any; column
|
|
|
19
19
|
case 'status': {
|
|
20
20
|
const status = getServiceMonitorStatus(resource)
|
|
21
21
|
return (
|
|
22
|
-
<span className={clsx('badge', status.color)}>
|
|
22
|
+
<span className={clsx('badge truncate max-w-full', status.color)} title={status.text}>
|
|
23
23
|
{status.text}
|
|
24
24
|
</span>
|
|
25
25
|
)
|
|
@@ -40,7 +40,7 @@ export function PrometheusRuleCell({ resource, column }: { resource: any; column
|
|
|
40
40
|
case 'status': {
|
|
41
41
|
const status = getPrometheusRuleStatus(resource)
|
|
42
42
|
return (
|
|
43
|
-
<span className={clsx('badge', status.color)}>
|
|
43
|
+
<span className={clsx('badge truncate max-w-full', status.color)} title={status.text}>
|
|
44
44
|
{status.text}
|
|
45
45
|
</span>
|
|
46
46
|
)
|
|
@@ -59,7 +59,7 @@ export function PodMonitorCell({ resource, column }: { resource: any; column: st
|
|
|
59
59
|
case 'status': {
|
|
60
60
|
const status = getPodMonitorStatus(resource)
|
|
61
61
|
return (
|
|
62
|
-
<span className={clsx('badge', status.color)}>
|
|
62
|
+
<span className={clsx('badge truncate max-w-full', status.color)} title={status.text}>
|
|
63
63
|
{status.text}
|
|
64
64
|
</span>
|
|
65
65
|
)
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
getRayServiceClusters,
|
|
16
16
|
getRayCronJobStatus,
|
|
17
17
|
getRayCronJobSchedule,
|
|
18
|
-
|
|
18
|
+
getRayCronJobTimeZone,
|
|
19
19
|
getRayCronJobLastSchedule,
|
|
20
20
|
} from '../resource-utils-ray'
|
|
21
21
|
|
|
@@ -110,9 +110,17 @@ export function RayCronJobCell({ resource, column }: { resource: any; column: st
|
|
|
110
110
|
const schedule = getRayCronJobSchedule(resource)
|
|
111
111
|
return <span className="text-sm font-mono text-theme-text-secondary">{schedule}</span>
|
|
112
112
|
}
|
|
113
|
-
case '
|
|
114
|
-
const
|
|
115
|
-
|
|
113
|
+
case 'timeZone': {
|
|
114
|
+
const zone = getRayCronJobTimeZone(resource)
|
|
115
|
+
const declared = Boolean(resource.spec?.timeZone)
|
|
116
|
+
return (
|
|
117
|
+
<span
|
|
118
|
+
className={clsx('text-sm truncate block', declared ? 'text-theme-text-secondary' : 'text-theme-text-tertiary')}
|
|
119
|
+
title={zone}
|
|
120
|
+
>
|
|
121
|
+
{zone}
|
|
122
|
+
</span>
|
|
123
|
+
)
|
|
116
124
|
}
|
|
117
125
|
case 'lastSchedule': {
|
|
118
126
|
const lastSchedule = getRayCronJobLastSchedule(resource)
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
getVolcanoPodGroupMinMember,
|
|
17
17
|
getJobFlowStatus,
|
|
18
18
|
getJobFlowFlowCount,
|
|
19
|
-
getJobTemplateStatus,
|
|
20
19
|
getJobTemplateTaskCount,
|
|
21
20
|
} from '../resource-utils-volcano'
|
|
22
21
|
|
|
@@ -122,14 +121,6 @@ export function JobFlowCell({ resource, column }: { resource: any; column: strin
|
|
|
122
121
|
|
|
123
122
|
export function JobTemplateCell({ resource, column }: { resource: any; column: string }) {
|
|
124
123
|
switch (column) {
|
|
125
|
-
case 'status': {
|
|
126
|
-
const status = getJobTemplateStatus(resource)
|
|
127
|
-
return (
|
|
128
|
-
<span className={clsx('badge', status.color)}>
|
|
129
|
-
{status.text}
|
|
130
|
-
</span>
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
124
|
case 'tasks': {
|
|
134
125
|
const count = getJobTemplateTaskCount(resource)
|
|
135
126
|
return <span className="text-sm text-theme-text-secondary">{count > 0 ? count : '-'}</span>
|
|
@@ -646,6 +646,11 @@ export function getCNPGObjectStoreStatus(resource: any): StatusBadge {
|
|
|
646
646
|
if (windows.some((w) => w.failingSinceLastSuccess)) {
|
|
647
647
|
return { text: 'Backups Failing', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
648
648
|
}
|
|
649
|
+
// Every timestamp on a recovery window is optional, so a server can be listed
|
|
650
|
+
// with no recovery point at all. The entry existing is not a backup existing.
|
|
651
|
+
if (!windows.some((w) => w.firstRecoverabilityPoint || w.lastSuccessfulBackupTime)) {
|
|
652
|
+
return { text: 'No recovery point', color: healthColors.unknown, level: 'unknown' }
|
|
653
|
+
}
|
|
649
654
|
return { text: 'Recoverable', color: healthColors.healthy, level: 'healthy' }
|
|
650
655
|
}
|
|
651
656
|
|