@skyhook-io/k8s-ui 1.13.3 → 1.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/applications/AppTooltips.tsx +13 -1
- package/src/components/applications/ApplicationDetail.test.tsx +25 -0
- package/src/components/applications/ApplicationDetail.tsx +39 -7
- package/src/components/applications/ApplicationsView.tsx +18 -3
- package/src/components/applications/ReadyBar.tsx +3 -2
- package/src/components/applications/applications-view.test.tsx +91 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -1
- package/src/components/issues/IssuesView.tsx +40 -31
- package/src/components/issues/ResourceIssuesSection.tsx +3 -2
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +123 -4
- package/src/components/issues/issues.test.ts +221 -10
- package/src/components/issues/types.ts +31 -5
- package/src/components/namespace-switcher/NamespacePicker.tsx +21 -13
- package/src/components/resources/ResourcesView.tsx +964 -96
- package/src/components/resources/column-settings-key.test.ts +98 -0
- package/src/components/resources/column-visibility-merge.test.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +236 -0
- package/src/components/resources/gateway-policy-status.test.ts +291 -0
- package/src/components/resources/gateway-policy-status.ts +237 -0
- package/src/components/resources/generic-status.test.ts +199 -0
- package/src/components/resources/generic-status.ts +171 -0
- package/src/components/resources/gpu-ecosystem-registry.test.ts +63 -0
- package/src/components/resources/index.ts +9 -1
- package/src/components/resources/istio-gateway-filter.test.ts +97 -0
- package/src/components/resources/printer-columns.test.ts +298 -0
- package/src/components/resources/printer-columns.ts +249 -0
- package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -5
- package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
- package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
- package/src/components/resources/renderers/HPARenderer.tsx +6 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
- package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
- package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
- package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
- package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
- package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
- package/src/components/resources/renderers/kai-cells.tsx +67 -0
- package/src/components/resources/renderers/kaito-cells.tsx +62 -0
- package/src/components/resources/renderers/kserve-cells.tsx +145 -0
- package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
- package/src/components/resources/renderers/kueue-cells.tsx +176 -0
- package/src/components/resources/renderers/nim-cells.tsx +81 -0
- package/src/components/resources/renderers/ray-cells.tsx +124 -0
- package/src/components/resources/renderers/volcano-cells.tsx +140 -0
- package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +222 -0
- package/src/components/resources/resource-utils-hpa.test.ts +4 -0
- package/src/components/resources/resource-utils-hpa.ts +6 -2
- package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
- package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
- package/src/components/resources/resource-utils-kai.ts +105 -0
- package/src/components/resources/resource-utils-kaito.ts +114 -0
- package/src/components/resources/resource-utils-kserve.ts +186 -0
- package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
- package/src/components/resources/resource-utils-kueue.ts +198 -0
- package/src/components/resources/resource-utils-nim.ts +109 -0
- package/src/components/resources/resource-utils-ray.ts +214 -0
- package/src/components/resources/resource-utils-rollout.test.ts +183 -0
- package/src/components/resources/resource-utils-velero.test.ts +30 -0
- package/src/components/resources/resource-utils-volcano.ts +176 -0
- package/src/components/resources/resource-utils.ts +156 -47
- package/src/components/resources/resources-column-filter.test.ts +27 -1
- package/src/components/resources/workload-rollout-golden.test.ts +55 -0
- package/src/components/shared/CreateResourceDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.tsx +4 -2
- package/src/components/shared/ResourceActionsBar.tsx +170 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
- package/src/components/shared/SetImageDialog.test.ts +129 -0
- package/src/components/shared/SetImageDialog.tsx +577 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/shared/revision-role-badges.test.ts +56 -4
- package/src/components/timeline/TimelineList.tsx +19 -3
- package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
- package/src/components/trace/ReachabilityView.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +12 -0
- package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
- package/src/components/workload/WorkloadView.tsx +144 -81
- package/src/hooks/useProgressiveRefresh.ts +37 -0
- package/src/source-hygiene.test.ts +28 -0
- package/src/theme/tailwind-theme.css +4 -0
- package/src/types/core.ts +55 -0
- package/src/utils/api-resources.test.ts +40 -1
- package/src/utils/api-resources.ts +11 -1
- package/src/utils/applications.ts +50 -0
- package/src/utils/bulk-workload-actions.test.ts +20 -0
- package/src/utils/bulk-workload-actions.ts +15 -0
- package/src/utils/navigation.test.ts +31 -1
- package/src/utils/navigation.ts +21 -2
- package/src/utils/workload-rollout.ts +292 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { columnSettingsKey, hasCuratedColumns } from './ResourcesView'
|
|
3
|
+
import { getCellFilterValue } from './resource-utils'
|
|
4
|
+
import { getGenericResourceStatus } from './generic-status'
|
|
5
|
+
|
|
6
|
+
describe('columnSettingsKey', () => {
|
|
7
|
+
// Two uncurated CRDs sharing a plural across API groups — Crossplane ships
|
|
8
|
+
// two `Usage` kinds — shared one blob and leaked column visibility, widths
|
|
9
|
+
// and custom columns between them.
|
|
10
|
+
it('qualifies uncurated CRDs by their API group', () => {
|
|
11
|
+
expect(columnSettingsKey('usages', 'apiextensions.crossplane.io'))
|
|
12
|
+
.not.toBe(columnSettingsKey('usages', 'protection.crossplane.io'))
|
|
13
|
+
expect(columnSettingsKey('usages', 'apiextensions.crossplane.io'))
|
|
14
|
+
.toBe('usages.apiextensions.crossplane.io')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// Curated and core kinds keep the key their saved preferences are already
|
|
18
|
+
// under, so nobody's columns reset on upgrade.
|
|
19
|
+
it('leaves core kinds alone', () => {
|
|
20
|
+
expect(columnSettingsKey('pods')).toBe('pods')
|
|
21
|
+
expect(columnSettingsKey('deployments', 'apps')).toBe('deployments')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('leaves curated CRDs alone', () => {
|
|
25
|
+
expect(columnSettingsKey('rollouts', 'argoproj.io')).toBe('rollouts')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Crossplane managed resources are curated through a group heuristic, not a
|
|
29
|
+
// KNOWN_COLUMNS entry — curation has to be read from getColumnsForKind, not
|
|
30
|
+
// from key membership.
|
|
31
|
+
it('leaves Crossplane managed resources alone', () => {
|
|
32
|
+
const key = columnSettingsKey('buckets', 's3.aws.upbound.io')
|
|
33
|
+
expect(key).toBe('buckets')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('hasCuratedColumns', () => {
|
|
38
|
+
// One definition shared by the XOR, the storage key, and the host's decision
|
|
39
|
+
// whether to request a table. If they disagreed, the server would resolve a
|
|
40
|
+
// table the client discards — or two of them would put a kind on different
|
|
41
|
+
// column sets.
|
|
42
|
+
it('recognizes curated kinds, including group-routed and heuristic ones', () => {
|
|
43
|
+
expect(hasCuratedColumns('pods')).toBe(true)
|
|
44
|
+
expect(hasCuratedColumns('rollouts', 'argoproj.io')).toBe(true)
|
|
45
|
+
expect(hasCuratedColumns('clusters', 'postgresql.cnpg.io')).toBe(true)
|
|
46
|
+
// Crossplane managed resources are curated by group heuristic, not plural.
|
|
47
|
+
expect(hasCuratedColumns('buckets', 's3.aws.upbound.io')).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('reports uncurated CRDs as uncurated', () => {
|
|
51
|
+
expect(hasCuratedColumns('usages', 'protection.crossplane.io')).toBe(false)
|
|
52
|
+
expect(hasCuratedColumns('widgets', 'example.com')).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('agrees with columnSettingsKey about which kinds get group-qualified', () => {
|
|
56
|
+
for (const [kind, group] of [['usages', 'protection.crossplane.io'], ['widgets', 'example.com']] as const) {
|
|
57
|
+
expect(hasCuratedColumns(kind, group)).toBe(false)
|
|
58
|
+
expect(columnSettingsKey(kind, group)).toContain(group)
|
|
59
|
+
}
|
|
60
|
+
for (const [kind, group] of [['rollouts', 'argoproj.io'], ['buckets', 's3.aws.upbound.io']] as const) {
|
|
61
|
+
expect(hasCuratedColumns(kind, group)).toBe(true)
|
|
62
|
+
expect(columnSettingsKey(kind, group)).not.toContain(group)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('status sort scope', () => {
|
|
68
|
+
const pod = {
|
|
69
|
+
status: {
|
|
70
|
+
phase: 'Running',
|
|
71
|
+
conditions: [{ type: 'Ready', status: 'False' }],
|
|
72
|
+
containerStatuses: [{ name: 'app', ready: false, restartCount: 3, state: { running: {} } }],
|
|
73
|
+
},
|
|
74
|
+
spec: { containers: [{ name: 'app' }] },
|
|
75
|
+
}
|
|
76
|
+
const uncuratedCR = { status: { conditions: [{ type: 'Ready', status: 'False', reason: 'QuorumLost' }] } }
|
|
77
|
+
|
|
78
|
+
// Curated kinds keep phase-based ordering: routing them through the per-kind
|
|
79
|
+
// readers would reorder the most-used lists for a divergence that predates
|
|
80
|
+
// this work.
|
|
81
|
+
it('leaves a curated kind sorting on its phase', () => {
|
|
82
|
+
expect(hasCuratedColumns('pods')).toBe(true)
|
|
83
|
+
expect(pod.status.phase).toBe('Running')
|
|
84
|
+
// The displayed text differs from the phase — that gap is intentional here.
|
|
85
|
+
expect(getCellFilterValue(pod, 'status', 'pods')).not.toBe('Running')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// An uncurated CR has no phase at all, so phase sorted every row as equal.
|
|
89
|
+
it('sorts an uncurated kind on the text its badge shows', () => {
|
|
90
|
+
expect(hasCuratedColumns('widgets', 'example.com')).toBe(false)
|
|
91
|
+
expect((uncuratedCR.status as any).phase).toBeUndefined()
|
|
92
|
+
const shown = getGenericResourceStatus(uncuratedCR)?.text
|
|
93
|
+
expect(shown).toBe('QuorumLost')
|
|
94
|
+
// Sort and the filter dropdown must offer the same string, or the dropdown
|
|
95
|
+
// lists values that appear on no row.
|
|
96
|
+
expect(getCellFilterValue(uncuratedCR, 'status', 'widgets')).toBe(shown)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mergeSavedVisibleColumns, selectedKindIdentityOf } from './ResourcesView'
|
|
3
|
+
|
|
4
|
+
const printer = (key: string, defaultVisible = true) => ({ key, label: key, defaultVisible }) as any
|
|
5
|
+
|
|
6
|
+
describe('mergeSavedVisibleColumns', () => {
|
|
7
|
+
it('keeps the saved set when it already knows the printer columns', () => {
|
|
8
|
+
const merged = mergeSavedVisibleColumns(
|
|
9
|
+
['name', 'printer:Phase'],
|
|
10
|
+
[],
|
|
11
|
+
[printer('printer:Phase'), printer('printer:Host')],
|
|
12
|
+
)
|
|
13
|
+
// Host is absent from a blob that names another printer column, so the user
|
|
14
|
+
// hid it on purpose — seeding it back would make it un-hideable.
|
|
15
|
+
expect(merged.has('printer:Host')).toBe(false)
|
|
16
|
+
expect(merged.has('printer:Phase')).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('seeds printer defaults into a blob that predates them', () => {
|
|
20
|
+
const merged = mergeSavedVisibleColumns(
|
|
21
|
+
['name', 'namespace', 'age'],
|
|
22
|
+
[],
|
|
23
|
+
[printer('printer:Phase'), printer('printer:Host')],
|
|
24
|
+
)
|
|
25
|
+
// Without this the table would render Name + Namespace + Age and nothing
|
|
26
|
+
// else for every user who had ever touched this kind's columns.
|
|
27
|
+
expect(merged.has('printer:Phase')).toBe(true)
|
|
28
|
+
expect(merged.has('printer:Host')).toBe(true)
|
|
29
|
+
expect(merged.has('name')).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('does not seed a wide-tier column, which is hidden by default', () => {
|
|
33
|
+
const merged = mergeSavedVisibleColumns(['name'], [], [printer('printer:Wide', false)])
|
|
34
|
+
expect(merged.has('printer:Wide')).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('always seeds host extras, which the embedding app injects', () => {
|
|
38
|
+
const merged = mergeSavedVisibleColumns(['name'], ['cluster'], [])
|
|
39
|
+
expect(merged.has('cluster')).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('is a no-op for a kind with no printer columns', () => {
|
|
43
|
+
expect([...mergeSavedVisibleColumns(['name', 'status'], [], [])].sort()).toEqual(['name', 'status'])
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('selectedKindIdentityOf', () => {
|
|
48
|
+
// Two CRDs can ship the same plural in different API groups. Keyed on the
|
|
49
|
+
// plural alone, a `printer:*` sort or filter survives the switch onto a table
|
|
50
|
+
// with no such column, and every row silently fails the filter.
|
|
51
|
+
it('separates the same plural in different groups', () => {
|
|
52
|
+
expect(selectedKindIdentityOf({ name: 'widgets', group: 'a.io' }))
|
|
53
|
+
.not.toBe(selectedKindIdentityOf({ name: 'widgets', group: 'b.io' }))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('is stable for the same selection', () => {
|
|
57
|
+
expect(selectedKindIdentityOf({ name: 'widgets', group: 'a.io' }))
|
|
58
|
+
.toBe(selectedKindIdentityOf({ name: 'widgets', group: 'a.io' }))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('treats an absent group and an empty group alike', () => {
|
|
62
|
+
expect(selectedKindIdentityOf({ name: 'pods' })).toBe(selectedKindIdentityOf({ name: 'pods', group: '' }))
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { hasCuratedColumns } from './ResourcesView'
|
|
5
|
+
|
|
6
|
+
// Parsed from the source rather than exported: this asserts a property of the
|
|
7
|
+
// data as it is written, and an export would let the two drift.
|
|
8
|
+
const SRC = readFileSync(join(__dirname, 'ResourcesView.tsx'), 'utf8')
|
|
9
|
+
|
|
10
|
+
function objectBody(declName: string): string {
|
|
11
|
+
const start = SRC.indexOf(declName)
|
|
12
|
+
if (start < 0) throw new Error(`${declName} not found — the checks below would pass vacuously`)
|
|
13
|
+
const open = SRC.indexOf('{', start)
|
|
14
|
+
let depth = 1
|
|
15
|
+
let i = open + 1
|
|
16
|
+
while (depth > 0) {
|
|
17
|
+
if (SRC[i] === '{') depth++
|
|
18
|
+
else if (SRC[i] === '}') depth--
|
|
19
|
+
i++
|
|
20
|
+
}
|
|
21
|
+
return SRC.slice(open + 1, i)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function objectKeys(declName: string): string[] {
|
|
25
|
+
return [...objectBody(declName).matchAll(/^ {2}([a-z0-9_]+):/gm)].map(m => m[1])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CURATED = objectKeys('const KNOWN_COLUMNS')
|
|
29
|
+
const OWNED = new Set(objectKeys('const CURATED_COLUMN_GROUPS'))
|
|
30
|
+
|
|
31
|
+
// Curated sets keyed on a kind the API server reserves. A CRD cannot be created
|
|
32
|
+
// in a core group, so these can never be claimed by a foreign resource.
|
|
33
|
+
const CORE_KEYS = new Set([
|
|
34
|
+
'pods', 'services', 'deployments', 'replicasets', 'statefulsets', 'daemonsets', 'jobs',
|
|
35
|
+
'cronjobs', 'configmaps', 'secrets', 'ingresses', 'ingressclasses', 'endpoints',
|
|
36
|
+
'endpointslices', 'persistentvolumeclaims', 'persistentvolumes', 'storageclasses', 'nodes',
|
|
37
|
+
'namespaces', 'serviceaccounts', 'roles', 'rolebindings', 'clusterroles', 'clusterrolebindings',
|
|
38
|
+
'horizontalpodautoscalers', 'hpas', 'poddisruptionbudgets', 'networkpolicies', 'events',
|
|
39
|
+
'leases', 'priorityclasses', 'runtimeclasses', 'resourcequotas', 'limitranges', 'csidrivers',
|
|
40
|
+
'csinodes', 'volumeattachments', 'mutatingwebhookconfigurations', 'validatingwebhookconfigurations',
|
|
41
|
+
'certificatesigningrequests', 'apiservices', 'customresourcedefinitions', 'replicationcontrollers',
|
|
42
|
+
'controllerrevisions', 'podtemplates', 'volumeattributesclasses', 'validatingadmissionpolicies',
|
|
43
|
+
'validatingadmissionpolicybindings', 'flowschemas', 'prioritylevelconfigurations',
|
|
44
|
+
'componentstatuses', 'resourceclaims', 'resourceclaimtemplates', 'resourceslices', 'deviceclasses',
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
// Groups are unbounded (one CRD per provider service), so these are gated by a
|
|
48
|
+
// predicate instead of an enumerated list.
|
|
49
|
+
const UNBOUNDED_KEYS = new Set(['providerconfigs', 'crossplanemanagedresources'])
|
|
50
|
+
|
|
51
|
+
describe('curated column ownership', () => {
|
|
52
|
+
// The guard that matters: a curated set added without an owning group would
|
|
53
|
+
// silently claim every CRD that happens to share its plural.
|
|
54
|
+
it('declares an owning group for every non-core curated set', () => {
|
|
55
|
+
const undeclared = CURATED.filter(k => !CORE_KEYS.has(k) && !UNBOUNDED_KEYS.has(k) && !OWNED.has(k))
|
|
56
|
+
expect(undeclared).toEqual([])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('does not declare ownership for a set that no longer exists', () => {
|
|
60
|
+
expect([...OWNED].filter(k => !CURATED.includes(k))).toEqual([])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Brace-counting, not parsing: assert the extraction actually saw the whole
|
|
64
|
+
// table, or a silently-missed key would let the guard above pass vacuously.
|
|
65
|
+
it('extracted every curated key', () => {
|
|
66
|
+
expect(CURATED.length).toBe(236)
|
|
67
|
+
expect(OWNED.size).toBe(198)
|
|
68
|
+
expect(CURATED).toContain('pods')
|
|
69
|
+
expect(CURATED).toContain('crossplanemanagedresources')
|
|
70
|
+
expect([...OWNED]).toContain('awsmanagedcontrolplanes')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('curated sets are reachable', () => {
|
|
75
|
+
// Ownership answers "who may use this key". It does not answer "can anything
|
|
76
|
+
// reach this key at all" — and a curated set nothing routes to is invisible
|
|
77
|
+
// while every other check stays green. kyvernopolicies was exactly that: real
|
|
78
|
+
// columns, real cells, and no normalization entry to arrive through.
|
|
79
|
+
const QUALIFIED_TARGETS = new Set(
|
|
80
|
+
[...objectBody('const GROUP_QUALIFIED_COLUMN_KEYS').matchAll(/'[^']*':\s*'([a-z0-9_]+)'/g)].map(m => m[1]),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Keys named after a vendor are Radar's own invention rather than a real API
|
|
84
|
+
// plural, so something has to route to them explicitly.
|
|
85
|
+
const VENDOR_PREFIXES = /^(cnpg|capi|knative|calico|velero|kyverno|nvidia|crossplane|istio)/
|
|
86
|
+
|
|
87
|
+
// Reached by predicate rather than by the qualified map.
|
|
88
|
+
const PREDICATE_REACHED = new Set(['crossplanemanagedresources'])
|
|
89
|
+
// Genuine upstream plurals that happen to start with a vendor name.
|
|
90
|
+
const REAL_PLURALS = new Set(['nvidiadrivers'])
|
|
91
|
+
|
|
92
|
+
it('routes something to every vendor-named curated set', () => {
|
|
93
|
+
const unreachable = CURATED.filter(
|
|
94
|
+
k => VENDOR_PREFIXES.test(k) &&
|
|
95
|
+
!QUALIFIED_TARGETS.has(k) &&
|
|
96
|
+
!PREDICATE_REACHED.has(k) &&
|
|
97
|
+
!REAL_PLURALS.has(k),
|
|
98
|
+
)
|
|
99
|
+
expect(unreachable).toEqual([])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('resolves the real plural to the vendor key, not just the key to itself', () => {
|
|
103
|
+
// The inverse of the vacuity trap above: assert the plural a cluster would
|
|
104
|
+
// actually serve, not Radar's internal name for the column set.
|
|
105
|
+
expect(hasCuratedColumns('policies', 'kyverno.io')).toBe(true)
|
|
106
|
+
expect(hasCuratedColumns('gateways', 'networking.istio.io')).toBe(true)
|
|
107
|
+
expect(hasCuratedColumns('backups', 'postgresql.cnpg.io')).toBe(true)
|
|
108
|
+
expect(hasCuratedColumns('clusters', 'cluster.x-k8s.io')).toBe(true)
|
|
109
|
+
expect(hasCuratedColumns('services', 'serving.knative.dev')).toBe(true)
|
|
110
|
+
// ...and that an unrelated vendor still gets nothing.
|
|
111
|
+
expect(hasCuratedColumns('policies', 'someone-else.io')).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('every declaration resolves', () => {
|
|
116
|
+
// Size checks cannot catch a wrong group. Each declared pair has to actually
|
|
117
|
+
// return curated columns, and the same key under a group nobody claims has to
|
|
118
|
+
// not — run over all 160 rather than a hand-picked sample.
|
|
119
|
+
const DECLARED = [...objectBody('const CURATED_COLUMN_GROUPS')
|
|
120
|
+
.matchAll(/^ {2}([a-z0-9_]+): \[([^\]\n]+)\],$/gm)]
|
|
121
|
+
.map(m => [m[1], m[2].split(',').map(g => g.trim().replace(/'/g, ''))] as const)
|
|
122
|
+
|
|
123
|
+
it('covers the whole ownership table', () => {
|
|
124
|
+
expect(DECLARED.length).toBe(198)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it.each(DECLARED)('%s is curated under each group it claims', (key, groups) => {
|
|
128
|
+
for (const g of groups) expect(hasCuratedColumns(key, g)).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it.each(DECLARED)('%s is not curated under an unclaimed group', (key, _groups) => {
|
|
132
|
+
// Deliberately not a Crossplane/upbound suffix, which is matched by predicate.
|
|
133
|
+
expect(hasCuratedColumns(key, 'nobody-claims-this.invalid')).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('foreign CRDs do not inherit curated columns', () => {
|
|
138
|
+
// Each of these ships in the wild under a plural Radar curates for someone
|
|
139
|
+
// else. chaos-mesh Workflows are present on real clusters today.
|
|
140
|
+
it.each([
|
|
141
|
+
['workflows', 'chaos-mesh.org', 'argoproj.io'],
|
|
142
|
+
['providers', 'notification.toolkit.fluxcd.io', 'pkg.crossplane.io'],
|
|
143
|
+
['services', 'example.com', ''],
|
|
144
|
+
['backups', 'third-party.io', 'velero.io'],
|
|
145
|
+
['clusters', 'kubeblocks.io', ''],
|
|
146
|
+
['certificates', 'unrelated.io', 'cert-manager.io'],
|
|
147
|
+
])('%s.%s is not treated as curated', (plural, foreignGroup) => {
|
|
148
|
+
expect(hasCuratedColumns(plural, foreignGroup)).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it.each([
|
|
152
|
+
['workflows', 'argoproj.io'],
|
|
153
|
+
['gateways', 'gateway.networking.k8s.io'],
|
|
154
|
+
['providers', 'pkg.crossplane.io'],
|
|
155
|
+
['scaledobjects', 'keda.sh'],
|
|
156
|
+
['certificates', 'cert-manager.io'],
|
|
157
|
+
['nodepools', 'karpenter.sh'],
|
|
158
|
+
['externalsecrets', 'external-secrets.io'],
|
|
159
|
+
['kustomizations', 'kustomize.toolkit.fluxcd.io'],
|
|
160
|
+
['virtualservices', 'networking.istio.io'],
|
|
161
|
+
['vulnerabilityreports', 'aquasecurity.github.io'],
|
|
162
|
+
])('%s.%s keeps its curated columns', (plural, group) => {
|
|
163
|
+
expect(hasCuratedColumns(plural, group)).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// Core kinds carry no ownership entry and must not regress.
|
|
167
|
+
it.each([
|
|
168
|
+
['pods', ''],
|
|
169
|
+
['deployments', 'apps'],
|
|
170
|
+
['jobs', 'batch'],
|
|
171
|
+
['ingresses', 'networking.k8s.io'],
|
|
172
|
+
['roles', 'rbac.authorization.k8s.io'],
|
|
173
|
+
])('core %s.%s stays curated', (plural, group) => {
|
|
174
|
+
expect(hasCuratedColumns(plural, group)).toBe(true)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Both are curated, but not by the same set: an Istio Gateway has servers and
|
|
178
|
+
// a workload selector, a Gateway API Gateway has a class, listeners, routes
|
|
179
|
+
// and addresses, and neither vocabulary describes the other.
|
|
180
|
+
it('gives Istio and Gateway API their own gateway column sets', () => {
|
|
181
|
+
expect(hasCuratedColumns('gateways', 'networking.istio.io')).toBe(true)
|
|
182
|
+
expect(hasCuratedColumns('gateways', 'gateway.networking.k8s.io')).toBe(true)
|
|
183
|
+
|
|
184
|
+
// objectBody slices to the first `{`, which inside an array is its first
|
|
185
|
+
// element — so take the array literal itself.
|
|
186
|
+
const columnKeys = (key: string) => {
|
|
187
|
+
const start = SRC.indexOf(`\n ${key}: [`)
|
|
188
|
+
expect(start).toBeGreaterThan(-1)
|
|
189
|
+
const end = SRC.indexOf('\n ],', start)
|
|
190
|
+
return [...SRC.slice(start, end).matchAll(/key: '([a-zA-Z]+)'/g)].map(m => m[1])
|
|
191
|
+
}
|
|
192
|
+
const istio = columnKeys('istiogateways')
|
|
193
|
+
const gwapi = columnKeys('gateways')
|
|
194
|
+
expect(istio).toContain('servers')
|
|
195
|
+
expect(istio).toContain('selector')
|
|
196
|
+
expect(gwapi).toContain('listeners')
|
|
197
|
+
expect(istio).not.toContain('listeners')
|
|
198
|
+
expect(gwapi).not.toContain('servers')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// CAPA serves AWSManagedControlPlane in the control-plane group; CAPZ and CAPG
|
|
202
|
+
// serve their equivalents in the infrastructure group. Declaring AWS as
|
|
203
|
+
// infrastructure silently dropped the curated columns on every CAPA cluster.
|
|
204
|
+
it('claims AWSManagedControlPlane in the control-plane group', () => {
|
|
205
|
+
expect(hasCuratedColumns('awsmanagedcontrolplanes', 'controlplane.cluster.x-k8s.io')).toBe(true)
|
|
206
|
+
expect(hasCuratedColumns('awsmanagedcontrolplanes', 'infrastructure.cluster.x-k8s.io')).toBe(false)
|
|
207
|
+
expect(hasCuratedColumns('azuremanagedcontrolplanes', 'infrastructure.cluster.x-k8s.io')).toBe(true)
|
|
208
|
+
expect(hasCuratedColumns('gcpmanagedcontrolplanes', 'infrastructure.cluster.x-k8s.io')).toBe(true)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// metrics.k8s.io is an aggregated API, not a group the API server reserves,
|
|
212
|
+
// and PodMetrics/NodeMetrics carry the plurals `pods`/`nodes`. Treating it as
|
|
213
|
+
// core handed them the Pod and Node column sets, which read fields they lack.
|
|
214
|
+
it('does not treat the aggregated metrics API as a core group', () => {
|
|
215
|
+
expect(hasCuratedColumns('pods', 'metrics.k8s.io')).toBe(false)
|
|
216
|
+
expect(hasCuratedColumns('nodes', 'metrics.k8s.io')).toBe(false)
|
|
217
|
+
expect(hasCuratedColumns('pods', '')).toBe(true)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// A provider ships one CRD per service, so managed resources collide with
|
|
221
|
+
// curated plurals constantly. The MR check has to run before the curated
|
|
222
|
+
// lookup or the collision resolves to generic columns and never reaches it.
|
|
223
|
+
it('keeps a managed resource whose plural collides with a curated kind', () => {
|
|
224
|
+
expect(hasCuratedColumns('certificates', 'acm.aws.upbound.io')).toBe(true)
|
|
225
|
+
expect(hasCuratedColumns('services', 's3.aws.upbound.io')).toBe(true)
|
|
226
|
+
expect(hasCuratedColumns('certificates', 'cert-manager.io')).toBe(true)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// Crossplane ProviderConfigs have one CRD per provider, so their groups
|
|
230
|
+
// cannot be enumerated and are matched by suffix instead.
|
|
231
|
+
it('keeps ProviderConfig curated across unbounded provider groups', () => {
|
|
232
|
+
expect(hasCuratedColumns('providerconfigs', 'aws.upbound.io')).toBe(true)
|
|
233
|
+
expect(hasCuratedColumns('providerconfigs', 'kubernetes.crossplane.io')).toBe(true)
|
|
234
|
+
expect(hasCuratedColumns('providerconfigs', 'not-crossplane.io')).toBe(false)
|
|
235
|
+
})
|
|
236
|
+
})
|