@skyhook-io/k8s-ui 1.12.0 → 1.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/applications/ApplicationDetail.tsx +2 -2
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +32 -7
- 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 +419 -9
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- 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 +47 -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/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/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/NamespaceRenderer.test.ts +30 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- 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 +59 -2
- package/src/components/resources/renderers/PolicySection.tsx +32 -5
- 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/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +81 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -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 +54 -0
- package/src/components/resources/resource-utils-cnpg.ts +264 -3
- 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 +13 -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/InClusterConsentDialog.tsx +5 -4
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/SelectMenu.tsx +202 -28
- 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 +5 -2
- package/src/types/core.ts +36 -4
- package/src/types/policy.ts +153 -2
- package/src/utils/clipboard.test.ts +86 -0
- package/src/utils/clipboard.ts +50 -0
- package/src/utils/extended-resources.ts +4 -7
- package/src/utils/format.test.ts +83 -1
- package/src/utils/format.ts +80 -1
- package/src/utils/inClusterConsent.test.ts +9 -0
- package/src/utils/inClusterConsent.ts +9 -9
- package/src/utils/index.ts +1 -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,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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
splitCNPGDeclarativeByApplied,
|
|
4
|
+
getCNPGDeclarativeStatus,
|
|
5
|
+
getCNPGDeclarativeMessage,
|
|
6
|
+
getCNPGReclaimPolicy,
|
|
7
|
+
getCNPGImageCatalogEntries,
|
|
8
|
+
} from './resource-utils-cnpg'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `status.applied` has three meanings and the middle one is easy to lose.
|
|
12
|
+
* Verified against a live CNPG 1.27 operator: `demo-app` came back true,
|
|
13
|
+
* `demo-broken` came back false with `role "nobody-owns-this" does not exist`,
|
|
14
|
+
* and every object is briefly absent-status before the operator reaches it.
|
|
15
|
+
*/
|
|
16
|
+
describe('declarative object status', () => {
|
|
17
|
+
it('is healthy once the operator has applied it', () => {
|
|
18
|
+
expect(getCNPGDeclarativeStatus({ status: { applied: true } }).level).toBe('healthy')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('is unhealthy when the operator tried and failed', () => {
|
|
22
|
+
expect(getCNPGDeclarativeStatus({ status: { applied: false } }).level).toBe('unhealthy')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// The one that matters: a freshly created object has no `applied` at all, and
|
|
26
|
+
// reporting that as failed condemns everything in its first seconds.
|
|
27
|
+
it('is unknown — not failed — before the operator has reconciled', () => {
|
|
28
|
+
expect(getCNPGDeclarativeStatus({}).level).toBe('unknown')
|
|
29
|
+
expect(getCNPGDeclarativeStatus({ status: {} }).level).toBe('unknown')
|
|
30
|
+
expect(getCNPGDeclarativeStatus({ status: { applied: undefined } }).text).toBe('Pending')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('passes the operator’s message through untouched', () => {
|
|
34
|
+
const msg = 'while creating database "demo_broken": ERROR: role "nobody-owns-this" does not exist'
|
|
35
|
+
expect(getCNPGDeclarativeMessage({ status: { message: msg } })).toBe(msg)
|
|
36
|
+
expect(getCNPGDeclarativeMessage({ status: { message: '' } })).toBeUndefined()
|
|
37
|
+
expect(getCNPGDeclarativeMessage({})).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The reclaim policy decides whether deleting a manifest drops real data, so
|
|
43
|
+
* the destructive case has to be identified rather than printed.
|
|
44
|
+
*/
|
|
45
|
+
describe('reclaim policy', () => {
|
|
46
|
+
it('flags delete as destructive across all three kinds', () => {
|
|
47
|
+
expect(getCNPGReclaimPolicy({ spec: { databaseReclaimPolicy: 'delete' } }).destructive).toBe(true)
|
|
48
|
+
expect(getCNPGReclaimPolicy({ spec: { publicationReclaimPolicy: 'delete' } }).destructive).toBe(true)
|
|
49
|
+
expect(getCNPGReclaimPolicy({ spec: { subscriptionReclaimPolicy: 'delete' } }).destructive).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('does not flag retain', () => {
|
|
53
|
+
expect(getCNPGReclaimPolicy({ spec: { databaseReclaimPolicy: 'retain' } }).destructive).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// CNPG defaults to retain, and defaulting the other way in the UI would
|
|
57
|
+
// warn about data loss that is not going to happen.
|
|
58
|
+
it('defaults to retain when unset', () => {
|
|
59
|
+
const r = getCNPGReclaimPolicy({ spec: {} })
|
|
60
|
+
expect(r.value).toBe('retain')
|
|
61
|
+
expect(r.destructive).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('image catalog entries', () => {
|
|
66
|
+
it('sorts by major so the list does not reshuffle between reads', () => {
|
|
67
|
+
const e = getCNPGImageCatalogEntries({
|
|
68
|
+
spec: { images: [{ major: 17, image: 'pg:17' }, { major: 15, image: 'pg:15' }, { major: 16, image: 'pg:16' }] },
|
|
69
|
+
})
|
|
70
|
+
expect(e.map((x) => x.major)).toEqual([15, 16, 17])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('drops entries with no image rather than rendering a blank row', () => {
|
|
74
|
+
expect(getCNPGImageCatalogEntries({ spec: { images: [{ major: 16 }] } })).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('returns nothing for a catalog with no images', () => {
|
|
78
|
+
expect(getCNPGImageCatalogEntries({ spec: {} })).toEqual([])
|
|
79
|
+
expect(getCNPGImageCatalogEntries({})).toEqual([])
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The Database page renders these three buckets with three different claims, so
|
|
85
|
+
* collapsing pending into failed puts "this exists in Kubernetes and not in
|
|
86
|
+
* PostgreSQL" under an object nothing has looked at yet.
|
|
87
|
+
*/
|
|
88
|
+
describe('splitCNPGDeclarativeByApplied', () => {
|
|
89
|
+
it('keeps pending out of the failures', () => {
|
|
90
|
+
const live = { status: { applied: true } }
|
|
91
|
+
const broken = { status: { applied: false, message: 'boom' } }
|
|
92
|
+
const fresh = {}
|
|
93
|
+
const alsoFresh = { status: {} }
|
|
94
|
+
|
|
95
|
+
const out = splitCNPGDeclarativeByApplied([live, broken, fresh, alsoFresh])
|
|
96
|
+
expect(out.applied).toEqual([live])
|
|
97
|
+
expect(out.notApplied).toEqual([broken])
|
|
98
|
+
expect(out.pending).toEqual([fresh, alsoFresh])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('returns three empty buckets for nothing', () => {
|
|
102
|
+
expect(splitCNPGDeclarativeByApplied([])).toEqual({ applied: [], notApplied: [], pending: [] })
|
|
103
|
+
})
|
|
104
|
+
})
|