@skyhook-io/k8s-ui 1.11.0 → 1.13.0
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 +4 -4
- package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
- package/src/components/gitops/GitOpsTableView.tsx +22 -2
- package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
- package/src/components/gitops/detail-helpers.ts +4 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +412 -5
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/PodRenderer.tsx +13 -0
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
- package/src/components/resources/renderers/PolicySection.tsx +197 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
- package/src/components/resources/resource-utils-cnpg.ts +324 -3
- package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +12 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +3 -0
- package/src/types/core.ts +29 -0
- package/src/types/gitops-flux-status.test.ts +163 -0
- package/src/types/gitops.ts +80 -24
- package/src/types/index.ts +1 -0
- package/src/types/policy.ts +193 -0
- package/src/utils/navigation.test.ts +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- package/src/utils/resource-icons.ts +29 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { VeleroBackupRenderer } from './VeleroBackupRenderer'
|
|
4
|
+
import { VeleroRestoreRenderer } from './VeleroRestoreRenderer'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Every screen has to end somewhere the reader can act.
|
|
8
|
+
*
|
|
9
|
+
* These two pages each name another object as the reason the reader should care
|
|
10
|
+
* — a storage location that is holding a backup hostage, and the backup a
|
|
11
|
+
* restore came from — and each rendered that name as plain text. The page said
|
|
12
|
+
* "go look at dr-replica" and gave no way to get there.
|
|
13
|
+
*
|
|
14
|
+
* The link has to disappear cleanly when the host does not wire navigation:
|
|
15
|
+
* library consumers render these renderers without it, and a button that goes
|
|
16
|
+
* nowhere is worse than text.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const backup = (location: string) => ({
|
|
20
|
+
metadata: { name: 'nightly', namespace: 'velero' },
|
|
21
|
+
spec: { storageLocation: location },
|
|
22
|
+
status: { phase: 'Completed' },
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const restore = {
|
|
26
|
+
metadata: { name: 'recovery', namespace: 'velero' },
|
|
27
|
+
spec: { backupName: 'nightly' },
|
|
28
|
+
status: { phase: 'Completed' },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('a backup that blames its storage location', () => {
|
|
32
|
+
it('lets the reader get to the location', () => {
|
|
33
|
+
const html = renderToString(
|
|
34
|
+
<VeleroBackupRenderer
|
|
35
|
+
data={backup('dr-replica')}
|
|
36
|
+
storageLocationPhase="Unavailable"
|
|
37
|
+
onNavigate={vi.fn()}
|
|
38
|
+
/>,
|
|
39
|
+
)
|
|
40
|
+
// The name is a control, not a label.
|
|
41
|
+
expect(html).toMatch(/<button[^>]*>dr-replica<\/button>/)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('renders the location as text when navigation is not wired', () => {
|
|
45
|
+
const html = renderToString(
|
|
46
|
+
<VeleroBackupRenderer data={backup('dr-replica')} storageLocationPhase="Unavailable" />,
|
|
47
|
+
)
|
|
48
|
+
expect(html).toContain('dr-replica')
|
|
49
|
+
expect(html).not.toMatch(/<button[^>]*>dr-replica<\/button>/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// The link is the way to the location whether or not it is broken — a healthy
|
|
53
|
+
// one is still the thing you check when deciding where a backup lives.
|
|
54
|
+
it('links the location even when it is healthy', () => {
|
|
55
|
+
const html = renderToString(
|
|
56
|
+
<VeleroBackupRenderer data={backup('default')} storageLocationPhase="Available" onNavigate={vi.fn()} />,
|
|
57
|
+
)
|
|
58
|
+
expect(html).toMatch(/<button[^>]*>default<\/button>/)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('a restore that names its backup', () => {
|
|
63
|
+
it('lets the reader get to the backup it restored from', () => {
|
|
64
|
+
const html = renderToString(<VeleroRestoreRenderer data={restore} onNavigate={vi.fn()} />)
|
|
65
|
+
expect(html).toMatch(/<button[^>]*>nightly<\/button>/)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('renders the backup as text when navigation is not wired', () => {
|
|
69
|
+
const html = renderToString(<VeleroRestoreRenderer data={restore} />)
|
|
70
|
+
expect(html).toContain('nightly')
|
|
71
|
+
expect(html).not.toMatch(/<button[^>]*>nightly<\/button>/)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The stalled-run issue says a run is "past the 1m Velero allows for one
|
|
77
|
+
* operation". That budget is spec.itemOperationTimeout, and the Options section
|
|
78
|
+
* listed every sibling spec option except it — so the page quoted a number whose
|
|
79
|
+
* source it did not show, next to the place that source belongs.
|
|
80
|
+
*/
|
|
81
|
+
describe('the budget the stall message measures against', () => {
|
|
82
|
+
it('shows the timeout the message quotes', () => {
|
|
83
|
+
const html = renderToString(
|
|
84
|
+
<VeleroBackupRenderer
|
|
85
|
+
data={{
|
|
86
|
+
metadata: { name: 'stuck', namespace: 'velero' },
|
|
87
|
+
spec: { storageLocation: 'default', itemOperationTimeout: '1m0s' },
|
|
88
|
+
status: { phase: 'InProgress' },
|
|
89
|
+
}}
|
|
90
|
+
/>,
|
|
91
|
+
)
|
|
92
|
+
expect(html).toContain('Item Operation Timeout')
|
|
93
|
+
expect(html).toContain('1m0s')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Unset is the common case, and it is not "none" — a default applies. But the
|
|
97
|
+
// number is not ours to print: Velero's built-in four hours can be overridden
|
|
98
|
+
// on the controller, so a page that renders "4h" would be stating an install
|
|
99
|
+
// setting it cannot read.
|
|
100
|
+
it('does not invent a number when the field is unset', () => {
|
|
101
|
+
const html = renderToString(
|
|
102
|
+
<VeleroBackupRenderer
|
|
103
|
+
data={{
|
|
104
|
+
metadata: { name: 'plain', namespace: 'velero' },
|
|
105
|
+
spec: { storageLocation: 'default' },
|
|
106
|
+
status: { phase: 'InProgress' },
|
|
107
|
+
}}
|
|
108
|
+
/>,
|
|
109
|
+
)
|
|
110
|
+
// 'default' is this file's own word for "unset, and something else decides".
|
|
111
|
+
expect(html).toContain('default')
|
|
112
|
+
expect(html).not.toContain('4h0m0s')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Claims the phase does not support.
|
|
118
|
+
*
|
|
119
|
+
* The warning banner fired on any run with warnings that was not failed or
|
|
120
|
+
* partially failed — which includes runs still in flight — and said the run
|
|
121
|
+
* "completed". A count taken mid-run is not a final count, and saying otherwise
|
|
122
|
+
* is the same defect as calling a Completed backup restorable.
|
|
123
|
+
*/
|
|
124
|
+
describe('a run that has warnings but has not finished', () => {
|
|
125
|
+
const withWarnings = (phase: string) => ({
|
|
126
|
+
metadata: { name: 'w', namespace: 'velero' },
|
|
127
|
+
spec: { storageLocation: 'default' },
|
|
128
|
+
status: { phase, warnings: 2 },
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('does not say a run in flight completed', () => {
|
|
132
|
+
const html = renderToString(<VeleroBackupRenderer data={withWarnings('InProgress')} />)
|
|
133
|
+
expect(html).not.toMatch(/completed, with/)
|
|
134
|
+
expect(html).toContain('not a final count')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('still says completed when it did', () => {
|
|
138
|
+
const html = renderToString(<VeleroBackupRenderer data={withWarnings('Completed')} />)
|
|
139
|
+
expect(html).toContain('completed, with 2 warning(s)')
|
|
140
|
+
expect(html).not.toContain('not a final count')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
CALICO_SELECTOR_NOT_APPLICABLE,
|
|
4
|
+
getCalicoApiGroup,
|
|
5
|
+
getCalicoIPPoolAllowedUses,
|
|
6
|
+
getCalicoIPPoolBlockSize,
|
|
7
|
+
getCalicoIPPoolEncapsulation,
|
|
8
|
+
getCalicoPolicyKindLabel,
|
|
9
|
+
getCalicoPolicySelector,
|
|
10
|
+
getCalicoPolicyTypes,
|
|
11
|
+
getCalicoTierRef,
|
|
12
|
+
isCalicoApiVersion,
|
|
13
|
+
isCalicoPolicyResource,
|
|
14
|
+
isCalicoStagedDeletion,
|
|
15
|
+
isCalicoStagedKubernetesNetworkPolicyKind,
|
|
16
|
+
isCoreNetworkPolicyKind,
|
|
17
|
+
} from './resource-utils-calico'
|
|
18
|
+
|
|
19
|
+
describe('Calico API group matching', () => {
|
|
20
|
+
it('extracts only supported Calico API groups', () => {
|
|
21
|
+
expect(getCalicoApiGroup('projectcalico.org/v3')).toBe('projectcalico.org')
|
|
22
|
+
expect(getCalicoApiGroup('crd.projectcalico.org/v1')).toBe(
|
|
23
|
+
'crd.projectcalico.org',
|
|
24
|
+
)
|
|
25
|
+
expect(getCalicoApiGroup('extension.projectcalico.org/v1')).toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('builds a cluster-scoped Tier reference with the policy API group', () => {
|
|
29
|
+
expect(
|
|
30
|
+
getCalicoTierRef({
|
|
31
|
+
apiVersion: 'crd.projectcalico.org/v1',
|
|
32
|
+
spec: { tier: 'security' },
|
|
33
|
+
}),
|
|
34
|
+
).toEqual({
|
|
35
|
+
kind: 'Tier',
|
|
36
|
+
namespace: '',
|
|
37
|
+
name: 'security',
|
|
38
|
+
group: 'crd.projectcalico.org',
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it.each(['projectcalico.org/v3', 'crd.projectcalico.org/v1'])(
|
|
43
|
+
'accepts the exact Calico group: %s',
|
|
44
|
+
(apiVersion) => {
|
|
45
|
+
expect(isCalicoApiVersion(apiVersion)).toBe(true)
|
|
46
|
+
expect(
|
|
47
|
+
isCalicoPolicyResource({ apiVersion, kind: 'NetworkPolicy' }),
|
|
48
|
+
).toBe(true)
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
it.each(['projectcalico.org/v3', 'crd.projectcalico.org/v1'])(
|
|
53
|
+
'recognizes staged Kubernetes network policies in %s',
|
|
54
|
+
(apiVersion) => {
|
|
55
|
+
const policy = {
|
|
56
|
+
apiVersion,
|
|
57
|
+
kind: 'StagedKubernetesNetworkPolicy',
|
|
58
|
+
spec: {
|
|
59
|
+
podSelector: { matchLabels: { app: 'frontend' } },
|
|
60
|
+
policyTypes: ['Ingress'],
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
expect(isCalicoStagedKubernetesNetworkPolicyKind(policy.kind)).toBe(true)
|
|
64
|
+
expect(isCalicoPolicyResource(policy)).toBe(true)
|
|
65
|
+
expect(getCalicoPolicySelector(policy)).toBe('app=frontend')
|
|
66
|
+
expect(getCalicoPolicyKindLabel(policy.kind)).toBe(
|
|
67
|
+
'CalicoStagedKubernetesNetworkPolicy',
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
it.each([
|
|
73
|
+
'extension.projectcalico.org/v1',
|
|
74
|
+
'projectcalico.org.evil/v1',
|
|
75
|
+
'networking.example.io/v1',
|
|
76
|
+
'v1',
|
|
77
|
+
])('rejects a non-Calico group: %s', (apiVersion) => {
|
|
78
|
+
expect(isCalicoApiVersion(apiVersion)).toBe(false)
|
|
79
|
+
expect(isCalicoPolicyResource({ apiVersion, kind: 'NetworkPolicy' })).toBe(
|
|
80
|
+
false,
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('recognizes only the core networking.k8s.io policy', () => {
|
|
85
|
+
expect(
|
|
86
|
+
isCoreNetworkPolicyKind('NetworkPolicy', 'networking.k8s.io/v1'),
|
|
87
|
+
).toBe(true)
|
|
88
|
+
expect(
|
|
89
|
+
isCoreNetworkPolicyKind('NetworkPolicy', 'projectcalico.org/v3'),
|
|
90
|
+
).toBe(false)
|
|
91
|
+
expect(
|
|
92
|
+
isCoreNetworkPolicyKind('NetworkPolicy', 'other.example.io/v1'),
|
|
93
|
+
).toBe(false)
|
|
94
|
+
expect(isCoreNetworkPolicyKind('NetworkPolicy', undefined)).toBe(true)
|
|
95
|
+
expect(
|
|
96
|
+
isCoreNetworkPolicyKind('NetworkPolicy', undefined, 'networking.k8s.io'),
|
|
97
|
+
).toBe(true)
|
|
98
|
+
expect(
|
|
99
|
+
isCoreNetworkPolicyKind('NetworkPolicy', undefined, 'other.example.io'),
|
|
100
|
+
).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('does not let the exact core fallback classify foreign kinds', () => {
|
|
104
|
+
expect(
|
|
105
|
+
isCoreNetworkPolicyKind('GlobalNetworkPolicy', 'networking.k8s.io/v1'),
|
|
106
|
+
).toBe(false)
|
|
107
|
+
expect(isCoreNetworkPolicyKind('Widget', undefined)).toBe(false)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('staged deletions', () => {
|
|
112
|
+
const deletion = {
|
|
113
|
+
apiVersion: 'projectcalico.org/v3',
|
|
114
|
+
kind: 'StagedNetworkPolicy',
|
|
115
|
+
metadata: { name: 'retire-frontend', namespace: 'demo' },
|
|
116
|
+
// The Calico API rejects any other spec field alongside a Delete action.
|
|
117
|
+
spec: { stagedAction: 'Delete', tier: 'default' },
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
it('does not read an absent selector as every workload', () => {
|
|
121
|
+
expect(isCalicoStagedDeletion(deletion)).toBe(true)
|
|
122
|
+
expect(getCalicoPolicySelector(deletion)).toBe(
|
|
123
|
+
CALICO_SELECTOR_NOT_APPLICABLE,
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('leaves a staged Set reading its real selector', () => {
|
|
128
|
+
const set = {
|
|
129
|
+
apiVersion: 'projectcalico.org/v3',
|
|
130
|
+
kind: 'StagedNetworkPolicy',
|
|
131
|
+
metadata: { name: 'tighten', namespace: 'demo' },
|
|
132
|
+
spec: { stagedAction: 'Set', selector: "app == 'web'" },
|
|
133
|
+
}
|
|
134
|
+
expect(isCalicoStagedDeletion(set)).toBe(false)
|
|
135
|
+
expect(getCalicoPolicySelector(set)).toBe("app == 'web'")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('still reads an enforced policy with no selector as every workload', () => {
|
|
139
|
+
const enforced = {
|
|
140
|
+
apiVersion: 'projectcalico.org/v3',
|
|
141
|
+
kind: 'NetworkPolicy',
|
|
142
|
+
metadata: { name: 'catch-all', namespace: 'demo' },
|
|
143
|
+
spec: {},
|
|
144
|
+
}
|
|
145
|
+
expect(getCalicoPolicySelector(enforced)).toBe('all workloads')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('IPPool derivations', () => {
|
|
150
|
+
it("applies Calico's block size default per address family", () => {
|
|
151
|
+
expect(getCalicoIPPoolBlockSize({ spec: { cidr: '192.168.0.0/16' } })).toBe(
|
|
152
|
+
26,
|
|
153
|
+
)
|
|
154
|
+
expect(getCalicoIPPoolBlockSize({ spec: { cidr: 'fd00::/48' } })).toBe(122)
|
|
155
|
+
expect(
|
|
156
|
+
getCalicoIPPoolBlockSize({ spec: { cidr: '10.0.0.0/8', blockSize: 24 } }),
|
|
157
|
+
).toBe(24)
|
|
158
|
+
expect(getCalicoIPPoolBlockSize({ spec: {} })).toBeUndefined()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('names the tunnel a pool encapsulates through', () => {
|
|
162
|
+
expect(getCalicoIPPoolEncapsulation({ spec: {} })).toBe('None')
|
|
163
|
+
expect(getCalicoIPPoolEncapsulation({ spec: { ipipMode: 'Always' } })).toBe(
|
|
164
|
+
'IPIP Always',
|
|
165
|
+
)
|
|
166
|
+
expect(
|
|
167
|
+
getCalicoIPPoolEncapsulation({ spec: { vxlanMode: 'CrossSubnet' } }),
|
|
168
|
+
).toBe('VXLAN CrossSubnet')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("falls back to Calico's default allowed uses", () => {
|
|
172
|
+
expect(getCalicoIPPoolAllowedUses({ spec: {} })).toBe('Workload, Tunnel')
|
|
173
|
+
expect(
|
|
174
|
+
getCalicoIPPoolAllowedUses({ spec: { allowedUses: ['Tunnel'] } }),
|
|
175
|
+
).toBe('Tunnel')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('derived policy types', () => {
|
|
180
|
+
// Verified against Calico v3.32.1: applying these without spec.types and
|
|
181
|
+
// reading them back yields exactly these values.
|
|
182
|
+
const policy = (spec: any) => ({
|
|
183
|
+
apiVersion: 'projectcalico.org/v3',
|
|
184
|
+
kind: 'NetworkPolicy',
|
|
185
|
+
spec: { selector: "app == 'x'", ...spec },
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('derives the types from which rule lists exist', () => {
|
|
189
|
+
expect(getCalicoPolicyTypes(policy({ egress: [{ action: 'Allow' }] }))).toEqual(['Egress'])
|
|
190
|
+
expect(getCalicoPolicyTypes(policy({ ingress: [{ action: 'Allow' }] }))).toEqual(['Ingress'])
|
|
191
|
+
expect(
|
|
192
|
+
getCalicoPolicyTypes(policy({ ingress: [{ action: 'Allow' }], egress: [{ action: 'Allow' }] })),
|
|
193
|
+
).toEqual(['Ingress', 'Egress'])
|
|
194
|
+
expect(getCalicoPolicyTypes(policy({}))).toEqual(['Ingress'])
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('follows the Kubernetes rule for the Kubernetes-shaped staged kind', () => {
|
|
198
|
+
// Kubernetes: every policy affects ingress, egress only when an egress
|
|
199
|
+
// section exists. Calico leaves policyTypes empty on this kind, so the
|
|
200
|
+
// derivation below is what the UI renders.
|
|
201
|
+
const staged = (spec: any) => ({
|
|
202
|
+
apiVersion: 'projectcalico.org/v3',
|
|
203
|
+
kind: 'StagedKubernetesNetworkPolicy',
|
|
204
|
+
spec: { podSelector: { matchLabels: { app: 'x' } }, stagedAction: 'Set', ...spec },
|
|
205
|
+
})
|
|
206
|
+
expect(getCalicoPolicyTypes(staged({ egress: [{ to: [] }] }))).toEqual(['Ingress', 'Egress'])
|
|
207
|
+
expect(getCalicoPolicyTypes(staged({ ingress: [{ from: [] }] }))).toEqual(['Ingress'])
|
|
208
|
+
expect(getCalicoPolicyTypes(staged({}))).toEqual(['Ingress'])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('prefers what the policy actually declares', () => {
|
|
212
|
+
expect(getCalicoPolicyTypes(policy({ types: ['Egress'], ingress: [{ action: 'Allow' }] }))).toEqual(['Egress'])
|
|
213
|
+
expect(getCalicoPolicyTypes({ spec: undefined })).toEqual([])
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { ResourceRef } from '../../types'
|
|
2
|
+
import { isApiGroup } from './resource-utils-cnpg'
|
|
3
|
+
|
|
4
|
+
export const CALICO_GROUPS = [
|
|
5
|
+
'projectcalico.org',
|
|
6
|
+
'crd.projectcalico.org',
|
|
7
|
+
] as const
|
|
8
|
+
|
|
9
|
+
export type CalicoApiGroup = (typeof CALICO_GROUPS)[number]
|
|
10
|
+
|
|
11
|
+
const CALICO_POLICY_KINDS = new Set([
|
|
12
|
+
'networkpolicy',
|
|
13
|
+
'networkpolicies',
|
|
14
|
+
'globalnetworkpolicy',
|
|
15
|
+
'globalnetworkpolicies',
|
|
16
|
+
'stagednetworkpolicy',
|
|
17
|
+
'stagednetworkpolicies',
|
|
18
|
+
'stagedglobalnetworkpolicy',
|
|
19
|
+
'stagedglobalnetworkpolicies',
|
|
20
|
+
'stagedkubernetesnetworkpolicy',
|
|
21
|
+
'stagedkubernetesnetworkpolicies',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
const CALICO_POLICY_LABELS: Record<string, string> = {
|
|
25
|
+
networkpolicy: 'CalicoNetworkPolicy',
|
|
26
|
+
networkpolicies: 'CalicoNetworkPolicy',
|
|
27
|
+
globalnetworkpolicy: 'CalicoGlobalNetworkPolicy',
|
|
28
|
+
globalnetworkpolicies: 'CalicoGlobalNetworkPolicy',
|
|
29
|
+
stagednetworkpolicy: 'CalicoStagedNetworkPolicy',
|
|
30
|
+
stagednetworkpolicies: 'CalicoStagedNetworkPolicy',
|
|
31
|
+
stagedglobalnetworkpolicy: 'CalicoStagedGlobalNetworkPolicy',
|
|
32
|
+
stagedglobalnetworkpolicies: 'CalicoStagedGlobalNetworkPolicy',
|
|
33
|
+
stagedkubernetesnetworkpolicy: 'CalicoStagedKubernetesNetworkPolicy',
|
|
34
|
+
stagedkubernetesnetworkpolicies: 'CalicoStagedKubernetesNetworkPolicy',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function apiGroup(apiVersion: unknown): string {
|
|
38
|
+
if (typeof apiVersion !== 'string') return ''
|
|
39
|
+
const parts = apiVersion.split('/')
|
|
40
|
+
return parts.length === 2 ? parts[0] : ''
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isCalicoApiGroup(group: unknown): group is CalicoApiGroup {
|
|
44
|
+
return (
|
|
45
|
+
typeof group === 'string' &&
|
|
46
|
+
(CALICO_GROUPS as readonly string[]).includes(group)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isCalicoApiVersion(apiVersion: unknown): boolean {
|
|
51
|
+
return isCalicoApiGroup(apiGroup(apiVersion))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getCalicoApiGroup(
|
|
55
|
+
apiVersion: unknown,
|
|
56
|
+
): CalicoApiGroup | undefined {
|
|
57
|
+
const group = apiGroup(apiVersion)
|
|
58
|
+
return isCalicoApiGroup(group) ? group : undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getCalicoTierRef(policy: any): ResourceRef {
|
|
62
|
+
return {
|
|
63
|
+
kind: 'Tier',
|
|
64
|
+
namespace: '',
|
|
65
|
+
name: String(policy?.spec?.tier ?? 'default'),
|
|
66
|
+
group: getCalicoApiGroup(policy?.apiVersion),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isCalicoPolicyKind(kind: unknown): boolean {
|
|
71
|
+
return typeof kind === 'string' && CALICO_POLICY_KINDS.has(kind.toLowerCase())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isCalicoNetworkPolicyKind(kind: unknown): boolean {
|
|
75
|
+
return (
|
|
76
|
+
typeof kind === 'string' &&
|
|
77
|
+
['networkpolicy', 'networkpolicies'].includes(kind.toLowerCase())
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isCalicoStagedKubernetesNetworkPolicyKind(
|
|
82
|
+
kind: unknown,
|
|
83
|
+
): boolean {
|
|
84
|
+
return (
|
|
85
|
+
typeof kind === 'string' &&
|
|
86
|
+
[
|
|
87
|
+
'stagedkubernetesnetworkpolicy',
|
|
88
|
+
'stagedkubernetesnetworkpolicies',
|
|
89
|
+
].includes(kind.toLowerCase())
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isCalicoPolicyResource(data: any): boolean {
|
|
94
|
+
return isCalicoApiVersion(data?.apiVersion) && isCalicoPolicyKind(data?.kind)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isCoreNetworkPolicyApiVersion(apiVersion: unknown): boolean {
|
|
98
|
+
return isApiGroup(apiVersion, 'networking.k8s.io')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function isCoreNetworkPolicyResource(data: any): boolean {
|
|
102
|
+
return isCoreNetworkPolicyKind(data?.kind, data?.apiVersion)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isCoreNetworkPolicyKind(
|
|
106
|
+
kind: unknown,
|
|
107
|
+
apiVersion: unknown,
|
|
108
|
+
group?: string,
|
|
109
|
+
): boolean {
|
|
110
|
+
if (!isCalicoNetworkPolicyKind(kind)) return false
|
|
111
|
+
if (apiVersion !== undefined && apiVersion !== null && apiVersion !== '') {
|
|
112
|
+
return isCoreNetworkPolicyApiVersion(apiVersion)
|
|
113
|
+
}
|
|
114
|
+
return group === undefined || group === '' || group === 'networking.k8s.io'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getCalicoPolicyKindLabel(kind: unknown): string {
|
|
118
|
+
if (typeof kind !== 'string') return 'CalicoNetworkPolicy'
|
|
119
|
+
return CALICO_POLICY_LABELS[kind.toLowerCase()] ?? 'CalicoNetworkPolicy'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getCalicoPolicyTypes(policy: any): string[] {
|
|
123
|
+
const raw = policy?.spec?.types ?? policy?.spec?.policyTypes
|
|
124
|
+
if (Array.isArray(raw) && raw.length > 0) return raw.map(String)
|
|
125
|
+
if (typeof raw === 'string' && raw !== '') return [raw]
|
|
126
|
+
if (policy?.spec === undefined) return []
|
|
127
|
+
const hasIngress = Array.isArray(policy.spec.ingress) && policy.spec.ingress.length > 0
|
|
128
|
+
const hasEgress = Array.isArray(policy.spec.egress) && policy.spec.egress.length > 0
|
|
129
|
+
|
|
130
|
+
// The two families default this field by different rules, and a
|
|
131
|
+
// StagedKubernetesNetworkPolicy carries the Kubernetes shape. Kubernetes
|
|
132
|
+
// treats every policy as affecting ingress and adds egress only when an
|
|
133
|
+
// egress section exists; Calico derives from which rule lists exist, so an
|
|
134
|
+
// egress-only Calico policy is egress ONLY. Calico stores its own derivation
|
|
135
|
+
// but leaves policyTypes empty on the Kubernetes-shaped kind, so this runs.
|
|
136
|
+
if (isCalicoStagedKubernetesNetworkPolicyKind(policy?.kind)) {
|
|
137
|
+
return hasEgress ? ['Ingress', 'Egress'] : ['Ingress']
|
|
138
|
+
}
|
|
139
|
+
if (hasEgress && !hasIngress) return ['Egress']
|
|
140
|
+
return hasEgress ? ['Ingress', 'Egress'] : ['Ingress']
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getCalicoPolicyRuleCount(policy: any): {
|
|
144
|
+
ingress: number
|
|
145
|
+
egress: number
|
|
146
|
+
} {
|
|
147
|
+
return {
|
|
148
|
+
ingress: Array.isArray(policy?.spec?.ingress)
|
|
149
|
+
? policy.spec.ingress.length
|
|
150
|
+
: 0,
|
|
151
|
+
egress: Array.isArray(policy?.spec?.egress) ? policy.spec.egress.length : 0,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The staged action, lower-cased. Absent means Set, which is Calico's default. */
|
|
156
|
+
export function getCalicoStagedAction(policy: any): string {
|
|
157
|
+
const action = policy?.spec?.stagedAction
|
|
158
|
+
return typeof action === 'string' ? action.toLowerCase() : ''
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A staged policy that stages a removal rather than a change. Calico requires
|
|
163
|
+
* the rest of its spec to be empty, so it has no selector and no rules — an
|
|
164
|
+
* absent selector here means "not applicable", never "every workload".
|
|
165
|
+
*/
|
|
166
|
+
export function isCalicoStagedDeletion(policy: any): boolean {
|
|
167
|
+
return getCalicoStagedAction(policy) === 'delete'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A staged policy Calico skips entirely. It previews nothing, so presenting its
|
|
172
|
+
* rules as a preview of protection is the same overstatement as reading a
|
|
173
|
+
* deletion's absent selector as "every workload".
|
|
174
|
+
*/
|
|
175
|
+
export function isCalicoStagedIgnored(policy: any): boolean {
|
|
176
|
+
return getCalicoStagedAction(policy) === 'ignore'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const CALICO_SELECTOR_NOT_APPLICABLE = '-'
|
|
180
|
+
|
|
181
|
+
export function getCalicoPolicySelector(policy: any): string {
|
|
182
|
+
if (isCalicoStagedDeletion(policy)) return CALICO_SELECTOR_NOT_APPLICABLE
|
|
183
|
+
const kubernetesPolicy = isCalicoStagedKubernetesNetworkPolicyKind(
|
|
184
|
+
policy?.kind,
|
|
185
|
+
)
|
|
186
|
+
const selector = kubernetesPolicy
|
|
187
|
+
? policy?.spec?.podSelector
|
|
188
|
+
: policy?.spec?.selector
|
|
189
|
+
if (kubernetesPolicy) return formatKubernetesLabelSelector(selector)
|
|
190
|
+
return selector === undefined || selector === null || selector === ''
|
|
191
|
+
? 'all workloads'
|
|
192
|
+
: String(selector)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function formatKubernetesLabelSelector(selector: any): string {
|
|
196
|
+
if (!selector || typeof selector !== 'object') return 'all workloads'
|
|
197
|
+
const parts: string[] = []
|
|
198
|
+
for (const [key, value] of Object.entries(selector.matchLabels ?? {})) {
|
|
199
|
+
parts.push(`${key}=${String(value)}`)
|
|
200
|
+
}
|
|
201
|
+
for (const expression of Array.isArray(selector.matchExpressions)
|
|
202
|
+
? selector.matchExpressions
|
|
203
|
+
: []) {
|
|
204
|
+
if (!expression || typeof expression !== 'object') continue
|
|
205
|
+
const item = expression as {
|
|
206
|
+
key?: string
|
|
207
|
+
operator?: string
|
|
208
|
+
values?: unknown[]
|
|
209
|
+
}
|
|
210
|
+
const values = Array.isArray(item.values)
|
|
211
|
+
? item.values.map(String).join(',')
|
|
212
|
+
: ''
|
|
213
|
+
parts.push(
|
|
214
|
+
values
|
|
215
|
+
? `${item.key ?? '?'} ${item.operator ?? '?'} (${values})`
|
|
216
|
+
: `${item.key ?? '?'} ${item.operator ?? '?'}`,
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
return parts.length > 0 ? parts.join(', ') : 'all workloads'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function getCalicoPolicyNamespaceSelector(policy: any): string {
|
|
223
|
+
const selector = policy?.spec?.namespaceSelector
|
|
224
|
+
return selector === undefined || selector === null || selector === ''
|
|
225
|
+
? '-'
|
|
226
|
+
: String(selector)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function getCalicoPolicyServiceAccountSelector(policy: any): string {
|
|
230
|
+
const selector = policy?.spec?.serviceAccountSelector
|
|
231
|
+
return selector === undefined || selector === null || selector === ''
|
|
232
|
+
? '-'
|
|
233
|
+
: String(selector)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Calico's own defaults for an IPPool, applied when a field is absent. The pool
|
|
238
|
+
* behaves this way whether or not the field is written, so a reader that shows
|
|
239
|
+
* only what is set describes the pool inaccurately.
|
|
240
|
+
*/
|
|
241
|
+
export function getCalicoIPPoolBlockSize(pool: any): number | undefined {
|
|
242
|
+
if (pool?.spec?.blockSize !== undefined) return Number(pool.spec.blockSize)
|
|
243
|
+
const cidr = pool?.spec?.cidr
|
|
244
|
+
if (typeof cidr !== 'string') return undefined
|
|
245
|
+
return cidr.includes(':') ? 122 : 26
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The tunnel a pool encapsulates through, or "None" when it routes directly. */
|
|
249
|
+
export function getCalicoIPPoolEncapsulation(pool: any): string {
|
|
250
|
+
const ipip = String(pool?.spec?.ipipMode ?? 'Never')
|
|
251
|
+
const vxlan = String(pool?.spec?.vxlanMode ?? 'Never')
|
|
252
|
+
if (ipip !== 'Never') return `IPIP ${ipip}`
|
|
253
|
+
if (vxlan !== 'Never') return `VXLAN ${vxlan}`
|
|
254
|
+
return 'None'
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function getCalicoIPPoolAllowedUses(pool: any): string {
|
|
258
|
+
const uses = pool?.spec?.allowedUses
|
|
259
|
+
if (!Array.isArray(uses)) return 'Workload, Tunnel'
|
|
260
|
+
return uses.length > 0 ? uses.map(String).join(', ') : '-'
|
|
261
|
+
}
|