@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,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getSecretStoreProviderType, getSecretStoreProviderKey } from './resource-utils-eso'
|
|
3
|
+
|
|
4
|
+
// The provider label answers "which backend holds these secrets?", so it has to
|
|
5
|
+
// track the upstream `spec.provider` field names rather than plausible ones.
|
|
6
|
+
|
|
7
|
+
describe('getSecretStoreProviderType', () => {
|
|
8
|
+
it('names the AWS product rather than assuming Secrets Manager', () => {
|
|
9
|
+
// One `aws` key addresses three services; `service` is required and is the
|
|
10
|
+
// only thing that distinguishes them.
|
|
11
|
+
const forService = (service: string) =>
|
|
12
|
+
getSecretStoreProviderType({ spec: { provider: { aws: { service, region: 'us-east-1' } } } })
|
|
13
|
+
|
|
14
|
+
expect(forService('SecretsManager')).toBe('AWS Secrets Manager')
|
|
15
|
+
expect(forService('ParameterStore')).toBe('AWS Parameter Store')
|
|
16
|
+
expect(forService('CertificateManager')).toBe('AWS Certificate Manager')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('falls back to the vendor when an AWS store declares no service', () => {
|
|
20
|
+
// Invalid per the CRD, but naming a product we cannot confirm is worse than
|
|
21
|
+
// naming only the vendor.
|
|
22
|
+
expect(getSecretStoreProviderType({ spec: { provider: { aws: {} } } })).toBe('AWS')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('recognises Bitwarden under its real field name', () => {
|
|
26
|
+
expect(getSecretStoreProviderType({ spec: { provider: { bitwardensecretsmanager: {} } } }))
|
|
27
|
+
.toBe('Bitwarden Secrets Manager')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('labels providers added after the map was first written', () => {
|
|
31
|
+
expect(getSecretStoreProviderType({ spec: { provider: { openBao: {} } } })).toBe('OpenBao')
|
|
32
|
+
expect(getSecretStoreProviderType({ spec: { provider: { conjur: {} } } })).toBe('CyberArk Conjur')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keeps v1beta1-only providers, which v1 clusters never send', () => {
|
|
36
|
+
expect(getSecretStoreProviderType({ spec: { provider: { device42: {} } } })).toBe('Device42')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('shows the raw key rather than Unknown for an unrecognised provider', () => {
|
|
40
|
+
expect(getSecretStoreProviderType({ spec: { provider: { somefutureprovider: {} } } }))
|
|
41
|
+
.toBe('somefutureprovider')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('reports nothing when there is no provider at all', () => {
|
|
45
|
+
expect(getSecretStoreProviderType({ spec: {} })).toBe('Unknown')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('getSecretStoreProviderKey', () => {
|
|
50
|
+
it('stays at the vendor level, which is what the badge colours by', () => {
|
|
51
|
+
const key = (service: string) =>
|
|
52
|
+
getSecretStoreProviderKey({ spec: { provider: { aws: { service } } } })
|
|
53
|
+
expect(key('SecretsManager')).toBe('aws')
|
|
54
|
+
expect(key('ParameterStore')).toBe('aws')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -7,33 +7,63 @@ import { healthColors, formatAge } from './resource-utils'
|
|
|
7
7
|
// PROVIDER DETECTION
|
|
8
8
|
// ============================================================================
|
|
9
9
|
|
|
10
|
+
// Keys are the SecretStore `spec.provider` field names. v1 and v1beta1 do not
|
|
11
|
+
// carry the same set, so entries dropped upstream (device42, alibaba) stay for
|
|
12
|
+
// clusters still serving v1beta1.
|
|
10
13
|
const PROVIDER_MAP: Record<string, string> = {
|
|
11
|
-
aws: 'AWS
|
|
14
|
+
aws: 'AWS',
|
|
12
15
|
azurekv: 'Azure Key Vault',
|
|
13
16
|
gcpsm: 'GCP Secret Manager',
|
|
14
17
|
vault: 'HashiCorp Vault',
|
|
18
|
+
openBao: 'OpenBao',
|
|
15
19
|
kubernetes: 'Kubernetes',
|
|
20
|
+
crd: 'Kubernetes Resource',
|
|
16
21
|
oracle: 'Oracle Vault',
|
|
17
22
|
ibm: 'IBM Secrets Manager',
|
|
23
|
+
alibaba: 'Alibaba Cloud KMS',
|
|
24
|
+
ovh: 'OVHcloud Secret Manager',
|
|
25
|
+
volcengine: 'Volcengine',
|
|
26
|
+
cloudrusm: 'Cloud.ru Secret Manager',
|
|
27
|
+
nebiusmysterybox: 'Nebius Mysterybox',
|
|
28
|
+
barbican: 'OpenStack Barbican',
|
|
29
|
+
yandexlockbox: 'Yandex Lockbox',
|
|
30
|
+
yandexcertificatemanager: 'Yandex Certificate Manager',
|
|
18
31
|
doppler: 'Doppler',
|
|
19
32
|
onepassword: '1Password',
|
|
33
|
+
onepasswordSDK: '1Password',
|
|
34
|
+
bitwardensecretsmanager: 'Bitwarden Secrets Manager',
|
|
20
35
|
senhasegura: 'senhasegura',
|
|
36
|
+
github: 'GitHub',
|
|
21
37
|
gitlab: 'GitLab',
|
|
22
38
|
webhook: 'Webhook',
|
|
23
39
|
fake: 'Fake',
|
|
24
40
|
keepersecurity: 'Keeper Security',
|
|
25
41
|
scaleway: 'Scaleway',
|
|
26
|
-
|
|
42
|
+
conjur: 'CyberArk Conjur',
|
|
43
|
+
delinea: 'Delinea DevOps Secrets Vault',
|
|
44
|
+
secretserver: 'Delinea Secret Server',
|
|
45
|
+
beyondtrust: 'BeyondTrust',
|
|
46
|
+
beyondtrustworkloadcredentials: 'BeyondTrust Workload Credentials',
|
|
27
47
|
chef: 'Chef',
|
|
28
48
|
pulumi: 'Pulumi ESC',
|
|
29
49
|
fortanix: 'Fortanix',
|
|
30
50
|
passworddepot: 'Password Depot',
|
|
51
|
+
passbolt: 'Passbolt',
|
|
52
|
+
previder: 'Previder',
|
|
53
|
+
onboardbase: 'Onboardbase',
|
|
54
|
+
dvls: 'Devolutions Server',
|
|
31
55
|
device42: 'Device42',
|
|
32
56
|
akeyless: 'Akeyless',
|
|
33
|
-
beyondtrust: 'BeyondTrust',
|
|
34
57
|
infisical: 'Infisical',
|
|
35
|
-
|
|
36
|
-
|
|
58
|
+
ngrok: 'ngrok',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A single `aws` key addresses three unrelated AWS products; `service` is a
|
|
62
|
+
// required enum and is the only thing that says which one holds the secrets.
|
|
63
|
+
const AWS_SERVICE_LABELS: Record<string, string> = {
|
|
64
|
+
SecretsManager: 'AWS Secrets Manager',
|
|
65
|
+
ParameterStore: 'AWS Parameter Store',
|
|
66
|
+
CertificateManager: 'AWS Certificate Manager',
|
|
37
67
|
}
|
|
38
68
|
|
|
39
69
|
/**
|
|
@@ -44,6 +74,8 @@ export function getSecretStoreProviderType(resource: any): string {
|
|
|
44
74
|
const provider = resource.spec?.provider
|
|
45
75
|
if (!provider) return 'Unknown'
|
|
46
76
|
|
|
77
|
+
if (provider.aws) return AWS_SERVICE_LABELS[provider.aws.service] ?? 'AWS'
|
|
78
|
+
|
|
47
79
|
for (const [key, label] of Object.entries(PROVIDER_MAP)) {
|
|
48
80
|
if (provider[key]) return label
|
|
49
81
|
}
|
|
@@ -28,15 +28,19 @@ export function getVirtualServiceStatus(resource: any): StatusBadge {
|
|
|
28
28
|
const tlsRoutes = spec.tls || []
|
|
29
29
|
const hosts = spec.hosts || []
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
return { text: 'No Hosts', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
33
|
-
}
|
|
34
|
-
|
|
31
|
+
// Routes are what a VirtualService exists to provide, delegate or not.
|
|
35
32
|
const totalRoutes = httpRoutes.length + tcpRoutes.length + tlsRoutes.length
|
|
36
33
|
if (totalRoutes === 0) {
|
|
37
34
|
return { text: 'No Routes', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
38
35
|
}
|
|
39
36
|
|
|
37
|
+
// A delegate VirtualService is required to declare no hosts, and this object
|
|
38
|
+
// cannot tell us whether a parent delegates to it, so an empty list is not
|
|
39
|
+
// evidence of a fault.
|
|
40
|
+
if (hosts.length === 0) {
|
|
41
|
+
return { text: 'No Hosts', color: healthColors.unknown, level: 'unknown' }
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
// Check for fault injection on any route
|
|
41
45
|
const hasFaultInjection = httpRoutes.some((r: any) => r.fault)
|
|
42
46
|
if (hasFaultInjection) {
|
|
@@ -49,7 +53,7 @@ export function getVirtualServiceStatus(resource: any): StatusBadge {
|
|
|
49
53
|
return { text: 'Mirroring', color: healthColors.degraded, level: 'degraded' }
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
return { text: '
|
|
56
|
+
return { text: 'Defined', color: healthColors.neutral, level: 'neutral' }
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
export function getVirtualServiceHosts(resource: any): string {
|
|
@@ -123,26 +127,18 @@ export function getVirtualServiceDestinations(resource: any): Array<{ host: stri
|
|
|
123
127
|
// DESTINATIONRULE UTILITIES
|
|
124
128
|
// ============================================================================
|
|
125
129
|
|
|
130
|
+
/**
|
|
131
|
+
* The only failure this object can show on its own.
|
|
132
|
+
*
|
|
133
|
+
* A rule without a host applies to nothing. Everything else it declares —
|
|
134
|
+
* subsets, a traffic policy — is configuration, and says nothing about whether
|
|
135
|
+
* the host is reachable or the policy is in effect.
|
|
136
|
+
*/
|
|
126
137
|
export function getDestinationRuleStatus(resource: any): StatusBadge {
|
|
127
|
-
|
|
128
|
-
const host = spec.host
|
|
129
|
-
|
|
130
|
-
if (!host) {
|
|
138
|
+
if (!resource.spec?.host) {
|
|
131
139
|
return { text: 'No Host', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
132
140
|
}
|
|
133
|
-
|
|
134
|
-
const subsets = spec.subsets || []
|
|
135
|
-
const trafficPolicy = spec.trafficPolicy
|
|
136
|
-
|
|
137
|
-
if (subsets.length > 0) {
|
|
138
|
-
return { text: pluralize(subsets.length, 'Subset'), color: healthColors.healthy, level: 'healthy' }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (trafficPolicy) {
|
|
142
|
-
return { text: 'Configured', color: healthColors.healthy, level: 'healthy' }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return { text: 'Active', color: healthColors.healthy, level: 'healthy' }
|
|
141
|
+
return { text: 'Not assessed', color: healthColors.unknown, level: 'unknown' }
|
|
146
142
|
}
|
|
147
143
|
|
|
148
144
|
export function getDestinationRuleHost(resource: any): string {
|
|
@@ -161,6 +157,22 @@ export function getDestinationRuleSubsets(resource: any): Array<{ name: string;
|
|
|
161
157
|
}))
|
|
162
158
|
}
|
|
163
159
|
|
|
160
|
+
/**
|
|
161
|
+
* The client-side TLS mode this rule declares for its host.
|
|
162
|
+
*
|
|
163
|
+
* What it means depends on a different object. DISABLE is defined as "do not
|
|
164
|
+
* setup a TLS connection to the upstream endpoint", so the client sends
|
|
165
|
+
* plaintext; a PeerAuthentication STRICT is "an mTLS tunnel (TLS with client
|
|
166
|
+
* cert must be presented)" and PERMISSIVE is "either plaintext or mTLS tunnel".
|
|
167
|
+
* The pairing therefore lands as unencrypted traffic or as rejected requests —
|
|
168
|
+
* deduced from those two definitions, since neither API documents the mismatch
|
|
169
|
+
* directly. Nothing in this row shows which, and subset and port policies can
|
|
170
|
+
* override the mode besides, so it reports the declaration and not the posture.
|
|
171
|
+
*/
|
|
172
|
+
export function getDestinationRuleTlsMode(resource: any): string {
|
|
173
|
+
return getDestinationRuleTrafficPolicy(resource)?.tls?.mode || '-'
|
|
174
|
+
}
|
|
175
|
+
|
|
164
176
|
export function getDestinationRuleTrafficPolicy(resource: any): {
|
|
165
177
|
connectionPool?: { tcp?: any; http?: any }
|
|
166
178
|
loadBalancer?: { simple?: string; consistentHash?: any }
|
|
@@ -182,6 +194,15 @@ export function getDestinationRuleLoadBalancer(resource: any): string {
|
|
|
182
194
|
// ISTIO GATEWAY UTILITIES
|
|
183
195
|
// ============================================================================
|
|
184
196
|
|
|
197
|
+
/**
|
|
198
|
+
* What spec.servers alone can honestly claim.
|
|
199
|
+
*
|
|
200
|
+
* Not liveness: an Istio Gateway whose spec.selector matches no running
|
|
201
|
+
* ingressgateway pod is how these actually fail, and it has servers like any
|
|
202
|
+
* other, so a green badge there would say the opposite of the truth. TLS is a
|
|
203
|
+
* property of a server rather than a verdict, and this slot is the one every
|
|
204
|
+
* other kind here uses for a verdict.
|
|
205
|
+
*/
|
|
185
206
|
export function getIstioGatewayStatus(resource: any): StatusBadge {
|
|
186
207
|
const servers = resource.spec?.servers || []
|
|
187
208
|
|
|
@@ -189,12 +210,7 @@ export function getIstioGatewayStatus(resource: any): StatusBadge {
|
|
|
189
210
|
return { text: 'No Servers', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
190
211
|
}
|
|
191
212
|
|
|
192
|
-
|
|
193
|
-
if (hasTLS) {
|
|
194
|
-
return { text: 'TLS', color: healthColors.healthy, level: 'healthy' }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return { text: 'Active', color: healthColors.healthy, level: 'healthy' }
|
|
213
|
+
return { text: 'Defined', color: healthColors.neutral, level: 'neutral' }
|
|
198
214
|
}
|
|
199
215
|
|
|
200
216
|
export function getIstioGatewayServers(resource: any): Array<{
|
|
@@ -236,12 +252,14 @@ export function getServiceEntryStatus(resource: any): StatusBadge {
|
|
|
236
252
|
return { text: 'No Hosts', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
237
253
|
}
|
|
238
254
|
|
|
255
|
+
// Location is a mesh-membership classification, not reachability: nothing here
|
|
256
|
+
// establishes the hosts resolve or answer.
|
|
239
257
|
const location = spec.location || 'MESH_EXTERNAL'
|
|
240
258
|
if (location === 'MESH_EXTERNAL') {
|
|
241
|
-
return { text: 'External', color: healthColors.
|
|
259
|
+
return { text: 'External', color: healthColors.neutral, level: 'neutral' }
|
|
242
260
|
}
|
|
243
261
|
|
|
244
|
-
return { text: 'Internal', color: healthColors.
|
|
262
|
+
return { text: 'Internal', color: healthColors.neutral, level: 'neutral' }
|
|
245
263
|
}
|
|
246
264
|
|
|
247
265
|
export function getServiceEntryHosts(resource: any): string {
|
|
@@ -327,21 +345,33 @@ export function getPeerAuthenticationPortLevelMtls(resource: any): Record<string
|
|
|
327
345
|
// AUTHORIZATIONPOLICY UTILITIES
|
|
328
346
|
// ============================================================================
|
|
329
347
|
|
|
348
|
+
/**
|
|
349
|
+
* What one AuthorizationPolicy declares — never a health verdict.
|
|
350
|
+
*
|
|
351
|
+
* An action is not a state of wellness: a DENY doing its job is not unhealthy,
|
|
352
|
+
* and an ALLOW is not proof anything is permitted. The request decision is made
|
|
353
|
+
* across every policy selecting the workload, so no single object can establish
|
|
354
|
+
* it, and a green or red tone would assert an answer this object does not have.
|
|
355
|
+
*
|
|
356
|
+
* The one shape worth marking is an ALLOW — the default action — carrying no
|
|
357
|
+
* rules. Rules are alternatives, so zero of them match nothing, and Istio
|
|
358
|
+
* documents `spec: {}` as the deny-all idiom. Amber says look closer; it stops
|
|
359
|
+
* short of claiming the outage, because other policies may permit the traffic.
|
|
360
|
+
*/
|
|
330
361
|
export function getAuthorizationPolicyStatus(resource: any): StatusBadge {
|
|
331
362
|
const action = resource.spec?.action || 'ALLOW'
|
|
332
363
|
const rules = resource.spec?.rules || []
|
|
333
364
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return { text: action, color: healthColors.unknown, level: 'unknown' }
|
|
365
|
+
if (action === 'ALLOW' && rules.length === 0) {
|
|
366
|
+
return { text: 'No allow rules', color: healthColors.degraded, level: 'degraded' }
|
|
367
|
+
}
|
|
368
|
+
const known: Record<string, string> = { ALLOW: 'Allow', DENY: 'Deny', CUSTOM: 'Custom', AUDIT: 'Audit' }
|
|
369
|
+
const label = known[action]
|
|
370
|
+
if (!label) return { text: action, color: healthColors.unknown, level: 'unknown' }
|
|
371
|
+
return {
|
|
372
|
+
text: `${label} (${pluralize(rules.length, 'rule')})`,
|
|
373
|
+
color: healthColors.neutral,
|
|
374
|
+
level: 'neutral',
|
|
345
375
|
}
|
|
346
376
|
}
|
|
347
377
|
|
|
@@ -365,9 +395,87 @@ export function getAuthorizationPolicySelector(resource: any): Record<string, st
|
|
|
365
395
|
return resource.spec?.selector?.matchLabels || {}
|
|
366
396
|
}
|
|
367
397
|
|
|
398
|
+
/**
|
|
399
|
+
* What the policy declares it applies to.
|
|
400
|
+
*
|
|
401
|
+
* A policy can attach by targetRef instead of by labels — waypoints and
|
|
402
|
+
* Gateways in ambient mode do — so the selector alone does not describe scope.
|
|
403
|
+
* With neither, scope is inherited: the namespace, or the whole mesh when the policy
|
|
404
|
+
* sits in the mesh root namespace. Which of those applies depends on
|
|
405
|
+
* MeshConfig.rootNamespace, which is not readable from this object, so the cell
|
|
406
|
+
* names both rather than guessing from the conventional namespace name.
|
|
407
|
+
*/
|
|
368
408
|
export function getAuthorizationPolicySelectorString(resource: any): string {
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
409
|
+
const spec = resource.spec || {}
|
|
410
|
+
const targets = [
|
|
411
|
+
...(Array.isArray(spec.targetRefs) ? spec.targetRefs : []),
|
|
412
|
+
...(spec.targetRef ? [spec.targetRef] : []),
|
|
413
|
+
].filter(Boolean)
|
|
414
|
+
if (targets.length > 0) {
|
|
415
|
+
return targets
|
|
416
|
+
.map((t: any) => {
|
|
417
|
+
const name = [t?.kind, t?.name].filter(Boolean).join('/') || 'target'
|
|
418
|
+
return t?.namespace && t.namespace !== resource.metadata?.namespace
|
|
419
|
+
? `${t.namespace}/${name}`
|
|
420
|
+
: name
|
|
421
|
+
})
|
|
422
|
+
.join(', ')
|
|
423
|
+
}
|
|
424
|
+
const entries = Object.entries(spec.selector?.matchLabels || {})
|
|
425
|
+
if (entries.length === 0) return 'Namespace / mesh scope'
|
|
372
426
|
return entries.map(([k, v]) => `${k}=${v}`).join(', ')
|
|
373
427
|
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* What an AuthorizationPolicy's rule list means for the traffic it governs.
|
|
431
|
+
*
|
|
432
|
+
* Rules are alternatives and an unset list never matches, so BOTH actions with
|
|
433
|
+
* no rules are inert in the same way — a DENY with none denies nothing, an
|
|
434
|
+
* ALLOW with none permits nothing. Only the ALLOW case has a consequence worth
|
|
435
|
+
* raising, because a workload with any ALLOW policy admits only what one of
|
|
436
|
+
* them matches. Neither can be stated as an outcome for the workload: every
|
|
437
|
+
* policy selecting it takes part in the decision.
|
|
438
|
+
*/
|
|
439
|
+
export function getAuthorizationPolicyRuleNotice(
|
|
440
|
+
resource: any,
|
|
441
|
+
): { level: 'warning' | 'info'; title: string; message: string } | null {
|
|
442
|
+
const action = resource?.spec?.action || 'ALLOW'
|
|
443
|
+
const rules = resource?.spec?.rules
|
|
444
|
+
if (Array.isArray(rules) && rules.length > 0) {
|
|
445
|
+
// A rule matches every request when it carries no conditions — which
|
|
446
|
+
// includes serialized empty lists, not just an empty object. On a DENY that
|
|
447
|
+
// is not a baseline other policies carve exceptions out of: DENY is
|
|
448
|
+
// evaluated first, so no ALLOW can re-permit what it matches. Unknown keys
|
|
449
|
+
// are treated as conditions, so a future field cannot be read as blanket.
|
|
450
|
+
const CONDITION_KEYS = ['from', 'to', 'when']
|
|
451
|
+
const matchesEverything = rules.some((r: any) => {
|
|
452
|
+
if (!r || typeof r !== 'object') return false
|
|
453
|
+
const keys = Object.keys(r)
|
|
454
|
+
if (keys.some(k => !CONDITION_KEYS.includes(k))) return false
|
|
455
|
+
return CONDITION_KEYS.every(k => !Array.isArray(r[k]) || r[k].length === 0)
|
|
456
|
+
})
|
|
457
|
+
if (action === 'DENY' && matchesEverything) {
|
|
458
|
+
return {
|
|
459
|
+
level: 'info',
|
|
460
|
+
title: 'Matches all requests',
|
|
461
|
+
message: 'A rule with no conditions matches every request, so this policy denies all traffic to the workloads it selects. DENY is evaluated before ALLOW, so no ALLOW policy can permit an exception.',
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return null
|
|
465
|
+
}
|
|
466
|
+
if (action === 'ALLOW') {
|
|
467
|
+
return {
|
|
468
|
+
level: 'warning',
|
|
469
|
+
title: 'No allow rules',
|
|
470
|
+
message: 'Rules are alternatives, so an ALLOW policy with none of them matches nothing and contributes no permitted traffic. Other ALLOW policies selecting the same workload may still permit requests — a default-deny-plus-exceptions setup looks exactly like this.',
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (action === 'DENY') {
|
|
474
|
+
return {
|
|
475
|
+
level: 'info',
|
|
476
|
+
title: 'No deny rules',
|
|
477
|
+
message: 'This DENY policy has no rules, so it matches no requests and denies no traffic. Other policies may still deny requests.',
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return null
|
|
481
|
+
}
|
|
@@ -107,12 +107,9 @@ export function getKyvernoPolicyStatus(resource: any): StatusBadge {
|
|
|
107
107
|
return { text: readyCond.reason || 'Not Ready', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
110
|
+
// Reached when the controller has not written Ready, or wrote it as Unknown.
|
|
111
|
+
// Declaring rules is not the controller accepting them.
|
|
112
|
+
return { text: 'Not assessed', color: healthColors.unknown, level: 'unknown' }
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
/**
|
|
@@ -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
|
}
|