@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
|
@@ -82,6 +82,9 @@ interface ResourceActionsBarProps {
|
|
|
82
82
|
revisionsError?: Error | null
|
|
83
83
|
onRollback?: (params: { kind: string; namespace: string; name: string; revision: number }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
84
84
|
isRollingBack?: boolean
|
|
85
|
+
// A rolled-back Rollout re-enters its strategy: canary replays every step,
|
|
86
|
+
// blueGreen parks the revision in preview. Absent when promote-full is denied.
|
|
87
|
+
onRolloutPromoteFull?: (params: { namespace: string; name: string }) => void | Promise<unknown>
|
|
85
88
|
|
|
86
89
|
// CronJob actions
|
|
87
90
|
onTriggerCronJob?: (params: { namespace: string; name: string }) => void
|
|
@@ -134,6 +137,7 @@ export function ResourceActionsBar({
|
|
|
134
137
|
onDelete, isDeleting, cascadeDependents, cascadeLoading, cascadeRootResolved,
|
|
135
138
|
onRestart, isRestarting,
|
|
136
139
|
revisions: revisionsList, revisionsLoading, revisionsError, onRollback, isRollingBack,
|
|
140
|
+
onRolloutPromoteFull,
|
|
137
141
|
onTriggerCronJob, isTriggeringCronJob,
|
|
138
142
|
onSuspendCronJob, isSuspendingCronJob,
|
|
139
143
|
onResumeCronJob, isResumingCronJob,
|
|
@@ -169,7 +173,7 @@ export function ResourceActionsBar({
|
|
|
169
173
|
|
|
170
174
|
// Rollback dialog state
|
|
171
175
|
const [showRevisions, setShowRevisions] = useState(false)
|
|
172
|
-
const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets'].includes(kind)
|
|
176
|
+
const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets', 'rollouts'].includes(kind)
|
|
173
177
|
const hasMultipleRevisions = (revisionsList?.length ?? 0) > 1
|
|
174
178
|
|
|
175
179
|
function handleDeleteConfirm(force: boolean) {
|
|
@@ -662,7 +666,7 @@ export function ResourceActionsBar({
|
|
|
662
666
|
</div>
|
|
663
667
|
</ConfirmDialog>
|
|
664
668
|
|
|
665
|
-
{showRevisions &&
|
|
669
|
+
{showRevisions && isRollbackKind && (
|
|
666
670
|
<RevisionHistoryDialog
|
|
667
671
|
kind={resource.kind}
|
|
668
672
|
namespace={resource.namespace}
|
|
@@ -674,6 +678,7 @@ export function ResourceActionsBar({
|
|
|
674
678
|
error={revisionsError}
|
|
675
679
|
onRollback={onRollback}
|
|
676
680
|
isRollingBack={isRollingBack}
|
|
681
|
+
onRolloutPromoteFull={onRolloutPromoteFull}
|
|
677
682
|
/>
|
|
678
683
|
)}
|
|
679
684
|
</div>
|
|
@@ -879,7 +884,54 @@ function RevisionImage({ image, displayImage }: { image: string; displayImage: s
|
|
|
879
884
|
)
|
|
880
885
|
}
|
|
881
886
|
|
|
882
|
-
|
|
887
|
+
// Accepts both the singular Kind and the plural route segment.
|
|
888
|
+
export function isRolloutKind(kind: string): boolean {
|
|
889
|
+
return kind.toLowerCase().startsWith('rollout')
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Deliberately not strategy-gated: canary replays its steps and blueGreen parks
|
|
893
|
+
// the revision in preview, so both need promote-full to land a rollback.
|
|
894
|
+
export function offersPromoteAfterRollback(kind: string, hasPromoteFull: boolean): boolean {
|
|
895
|
+
return isRolloutKind(kind) && hasPromoteFull
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
export interface RevisionRoleBadge {
|
|
899
|
+
label: 'Stable' | 'Current' | 'Rolling out'
|
|
900
|
+
tone: 'status-healthy' | 'status-degraded'
|
|
901
|
+
tip?: string
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Mid-canary a Rollout's current revision is not yet the stable one; for every
|
|
905
|
+
// other workload kind the two always coincide.
|
|
906
|
+
export function revisionRoleBadges(rev: WorkloadRevision, isRollout: boolean): RevisionRoleBadge[] {
|
|
907
|
+
const badges: RevisionRoleBadge[] = []
|
|
908
|
+
if (rev.isStable && !rev.isCurrent) {
|
|
909
|
+
badges.push({ label: 'Stable', tone: 'status-healthy', tip: 'Serving stable traffic — an abort reverts here' })
|
|
910
|
+
}
|
|
911
|
+
if (rev.isCurrent) {
|
|
912
|
+
const settled = rev.isStable || !isRollout
|
|
913
|
+
badges.push({ label: settled ? 'Current' : 'Rolling out', tone: settled ? 'status-healthy' : 'status-degraded' })
|
|
914
|
+
}
|
|
915
|
+
return badges
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Awaits promote-full so the dialog cannot close on a half-landed rollback. A
|
|
919
|
+
// failure is reported by the mutation's error toast, so it must not block the close.
|
|
920
|
+
export async function completePromoteAfterRollback(
|
|
921
|
+
promote: () => void | Promise<unknown>,
|
|
922
|
+
setPending: (pending: boolean) => void,
|
|
923
|
+
): Promise<void> {
|
|
924
|
+
setPending(true)
|
|
925
|
+
try {
|
|
926
|
+
await promote()
|
|
927
|
+
} catch {
|
|
928
|
+
// already surfaced to the user
|
|
929
|
+
} finally {
|
|
930
|
+
setPending(false)
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, revisions, isLoading, error, onRollback, isRollingBack, onRolloutPromoteFull }: {
|
|
883
935
|
kind: string
|
|
884
936
|
namespace: string
|
|
885
937
|
name: string
|
|
@@ -890,12 +942,22 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
890
942
|
error?: Error | null
|
|
891
943
|
onRollback?: (params: { kind: string; namespace: string; name: string; revision: number }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
892
944
|
isRollingBack?: boolean
|
|
945
|
+
onRolloutPromoteFull?: (params: { namespace: string; name: string }) => void | Promise<unknown>
|
|
893
946
|
}) {
|
|
894
947
|
const [confirmRevision, setConfirmRevision] = useState<number | null>(null)
|
|
895
948
|
const [diffRevision, setDiffRevision] = useState<number | null>(null)
|
|
949
|
+
const [promoteAfterRollback, setPromoteAfterRollback] = useState(false)
|
|
950
|
+
const [promotingFull, setPromotingFull] = useState(false)
|
|
896
951
|
|
|
897
952
|
const handleClose = () => { setDiffRevision(null); onClose() }
|
|
898
953
|
|
|
954
|
+
// The dialog must not close while promote-full is still in flight, or a rollback
|
|
955
|
+
// that only half-landed reads as complete.
|
|
956
|
+
const busy = Boolean(isRollingBack) || promotingFull
|
|
957
|
+
|
|
958
|
+
const isRollout = isRolloutKind(kind)
|
|
959
|
+
const canPromoteAfterRollback = offersPromoteAfterRollback(kind, Boolean(onRolloutPromoteFull))
|
|
960
|
+
|
|
899
961
|
const currentRevision = revisions?.find(r => r.isCurrent)
|
|
900
962
|
const selectedRevision = revisions?.find(r => r.number === diffRevision)
|
|
901
963
|
const hasDiffData = currentRevision?.template && selectedRevision?.template
|
|
@@ -904,7 +966,13 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
904
966
|
onRollback?.(
|
|
905
967
|
{ kind, namespace, name, revision },
|
|
906
968
|
{
|
|
907
|
-
onSuccess: () => {
|
|
969
|
+
onSuccess: async () => {
|
|
970
|
+
if (canPromoteAfterRollback && promoteAfterRollback && onRolloutPromoteFull) {
|
|
971
|
+
await completePromoteAfterRollback(
|
|
972
|
+
() => onRolloutPromoteFull({ namespace, name }),
|
|
973
|
+
setPromotingFull,
|
|
974
|
+
)
|
|
975
|
+
}
|
|
908
976
|
setConfirmRevision(null)
|
|
909
977
|
setDiffRevision(null)
|
|
910
978
|
onClose()
|
|
@@ -938,7 +1006,7 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
938
1006
|
<DialogPortal
|
|
939
1007
|
open={open}
|
|
940
1008
|
onClose={handleClose}
|
|
941
|
-
closable={!
|
|
1009
|
+
closable={!busy}
|
|
942
1010
|
className="flex max-h-[85vh] w-[calc(100vw-2rem)] max-w-5xl flex-col"
|
|
943
1011
|
>
|
|
944
1012
|
<div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
|
|
@@ -955,7 +1023,7 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
955
1023
|
<Tooltip content="Close" delay={150}>
|
|
956
1024
|
<button
|
|
957
1025
|
onClick={handleClose}
|
|
958
|
-
disabled={
|
|
1026
|
+
disabled={busy}
|
|
959
1027
|
aria-label="Close revision history"
|
|
960
1028
|
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50"
|
|
961
1029
|
>
|
|
@@ -1036,22 +1104,27 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
1036
1104
|
</button>
|
|
1037
1105
|
</Tooltip>
|
|
1038
1106
|
)}
|
|
1039
|
-
{rev.
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1107
|
+
{revisionRoleBadges(rev, isRollout).map(({ label, tone, tip }) =>
|
|
1108
|
+
tip ? (
|
|
1109
|
+
<Tooltip key={label} content={tip} delay={150}>
|
|
1110
|
+
<span className={clsx('badge', tone)}>{label}</span>
|
|
1111
|
+
</Tooltip>
|
|
1112
|
+
) : (
|
|
1113
|
+
<span key={label} className={clsx('badge', tone)}>{label}</span>
|
|
1114
|
+
)
|
|
1115
|
+
)}
|
|
1116
|
+
{rev.isCurrent ? null : confirmRevision === rev.number ? (
|
|
1044
1117
|
<>
|
|
1045
1118
|
<button
|
|
1046
1119
|
onClick={() => handleRollback(rev.number)}
|
|
1047
|
-
disabled={
|
|
1120
|
+
disabled={busy}
|
|
1048
1121
|
className="px-2 py-0.5 text-xs font-medium text-white bg-amber-600 hover:bg-amber-700 rounded transition-colors disabled:opacity-50"
|
|
1049
1122
|
>
|
|
1050
|
-
{isRollingBack ? 'Rolling back...' : 'Confirm'}
|
|
1123
|
+
{promotingFull ? 'Promoting...' : isRollingBack ? 'Rolling back...' : 'Confirm'}
|
|
1051
1124
|
</button>
|
|
1052
1125
|
<button
|
|
1053
1126
|
onClick={() => setConfirmRevision(null)}
|
|
1054
|
-
disabled={
|
|
1127
|
+
disabled={busy}
|
|
1055
1128
|
className="px-2 py-0.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary rounded transition-colors disabled:opacity-50"
|
|
1056
1129
|
>
|
|
1057
1130
|
Cancel
|
|
@@ -1084,10 +1157,22 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
1084
1157
|
)}
|
|
1085
1158
|
</div>
|
|
1086
1159
|
|
|
1087
|
-
<div className="flex items-center justify-
|
|
1160
|
+
<div className="flex items-center justify-between gap-4 p-4 border-t border-theme-border shrink-0">
|
|
1161
|
+
{canPromoteAfterRollback ? (
|
|
1162
|
+
<label className="flex items-center gap-2 text-sm text-theme-text-secondary">
|
|
1163
|
+
<input
|
|
1164
|
+
type="checkbox"
|
|
1165
|
+
checked={promoteAfterRollback}
|
|
1166
|
+
onChange={(e) => setPromoteAfterRollback(e.target.checked)}
|
|
1167
|
+
disabled={busy}
|
|
1168
|
+
className="rounded border-theme-border"
|
|
1169
|
+
/>
|
|
1170
|
+
Promote fully after rollback — skip pauses, steps, and analysis (emergency hotfix)
|
|
1171
|
+
</label>
|
|
1172
|
+
) : <span />}
|
|
1088
1173
|
<button
|
|
1089
1174
|
onClick={handleClose}
|
|
1090
|
-
disabled={
|
|
1175
|
+
disabled={busy}
|
|
1091
1176
|
className="px-4 py-2 text-sm font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors disabled:opacity-50"
|
|
1092
1177
|
>
|
|
1093
1178
|
Close
|
|
@@ -44,13 +44,14 @@ describe('ResourceRendererDispatch', () => {
|
|
|
44
44
|
})
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
function renderKind(kind: string, data: any, namespace = ''): string {
|
|
47
|
+
function renderKind(kind: string, data: any, namespace = '', onNavigate?: (ref: ResourceRef) => void): string {
|
|
48
48
|
return renderToString(
|
|
49
49
|
<ResourceRendererDispatch
|
|
50
50
|
resource={{ kind, namespace, name: data?.metadata?.name || 'x' }}
|
|
51
51
|
data={data}
|
|
52
52
|
onCopy={() => {}}
|
|
53
53
|
copied={null}
|
|
54
|
+
onNavigate={onNavigate}
|
|
54
55
|
showCommonSections={false}
|
|
55
56
|
/>,
|
|
56
57
|
)
|
|
@@ -153,11 +154,24 @@ describe('DRA renderers dispatch', () => {
|
|
|
153
154
|
// so any third operator's CRD inherited whichever branch was the fallback.
|
|
154
155
|
// ============================================================================
|
|
155
156
|
|
|
157
|
+
// The spec field is a probe: GenericRenderer renders spec into a Specification
|
|
158
|
+
// section, so its value appearing in the output is proof the fall-through
|
|
159
|
+
// reached a renderer. A blank drawer is not an empty string — the surrounding
|
|
160
|
+
// wrapper still emits ~50 characters of markup — so asserting non-emptiness
|
|
161
|
+
// passes even when the drawer shows the user nothing.
|
|
162
|
+
const COLLISION_PROBE = 'collision-probe-value'
|
|
163
|
+
|
|
156
164
|
function renderCollidingKind(kind: string, apiVersion: string): string {
|
|
157
165
|
return renderToString(
|
|
158
166
|
<ResourceRendererDispatch
|
|
159
167
|
resource={{ kind, namespace: 'default', name: 'thing' }}
|
|
160
|
-
data={{
|
|
168
|
+
data={{
|
|
169
|
+
apiVersion,
|
|
170
|
+
kind: 'Cluster',
|
|
171
|
+
metadata: { name: 'thing', namespace: 'default' },
|
|
172
|
+
spec: { collisionProbe: COLLISION_PROBE },
|
|
173
|
+
status: {},
|
|
174
|
+
}}
|
|
161
175
|
onCopy={() => {}}
|
|
162
176
|
copied={null}
|
|
163
177
|
showCommonSections={false}
|
|
@@ -225,8 +239,40 @@ describe('ResourceRendererDispatch — colliding plurals fall through', () => {
|
|
|
225
239
|
['backups', 'kubevirt.io/v1'],
|
|
226
240
|
['scheduledbackups', 'other.io/v1'],
|
|
227
241
|
['poolers', 'other.io/v1'],
|
|
242
|
+
// The CNPG declarative kinds and the barman-cloud store extend the same
|
|
243
|
+
// list. `databases` and `publications` ship with several database
|
|
244
|
+
// operators, `objectstores` is a generic plural, and `subscriptions` is
|
|
245
|
+
// Knative's as well — every one of them was blank until this fall-through
|
|
246
|
+
// covered it.
|
|
247
|
+
['objectstores', 'other.io/v1'],
|
|
248
|
+
['databases', 'mysql.oracle.com/v2'],
|
|
249
|
+
['publications', 'other.io/v1'],
|
|
250
|
+
['subscriptions', 'operators.coreos.com/v1alpha1'],
|
|
251
|
+
['imagecatalogs', 'other.io/v1'],
|
|
252
|
+
['clusterimagecatalogs', 'other.io/v1'],
|
|
253
|
+
['policies', 'operators.coreos.com/v1'],
|
|
254
|
+
// Velero's BackupRepository joined KNOWN_KINDS when it got a renderer, so
|
|
255
|
+
// it needs the same fall-through as its siblings.
|
|
256
|
+
['backuprepositories', 'other.io/v1'],
|
|
228
257
|
])('renders something for a foreign %s CRD', (kind, apiVersion) => {
|
|
229
|
-
expect(renderCollidingKind(kind, apiVersion)
|
|
258
|
+
expect(renderCollidingKind(kind, apiVersion)).toContain(COLLISION_PROBE)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// A Velero BackupRepository must reach its own renderer, not the generic one.
|
|
262
|
+
// It raises BackupRepositoryNotReady, and the reason Velero recorded is only
|
|
263
|
+
// on the dedicated page.
|
|
264
|
+
it('renders the Velero repository page for a velero.io backuprepositories', () => {
|
|
265
|
+
const html = renderCollidingKind('backuprepositories', 'velero.io/v1')
|
|
266
|
+
expect(html).toContain('Volume Namespace')
|
|
267
|
+
expect(html).not.toContain(COLLISION_PROBE)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
// Both owners of `subscriptions` still get their own renderer.
|
|
271
|
+
it.each([
|
|
272
|
+
['messaging.knative.dev/v1', 'Subscriber'],
|
|
273
|
+
['postgresql.cnpg.io/v1', 'Applied'],
|
|
274
|
+
])('still renders the owning engine for subscriptions/%s', (apiVersion, marker) => {
|
|
275
|
+
expect(renderCollidingKind('subscriptions', apiVersion)).toContain(marker)
|
|
230
276
|
})
|
|
231
277
|
|
|
232
278
|
it('does not double-render when a known engine matches', () => {
|
|
@@ -237,6 +283,273 @@ describe('ResourceRendererDispatch — colliding plurals fall through', () => {
|
|
|
237
283
|
})
|
|
238
284
|
})
|
|
239
285
|
|
|
286
|
+
describe('Calico IPPool collision handling', () => {
|
|
287
|
+
it.each(['crd.projectcalico.org/v1', 'projectcalico.org/v3'])('renders IPPool details for %s', (apiVersion) => {
|
|
288
|
+
const html = renderKind('ippools', {
|
|
289
|
+
apiVersion,
|
|
290
|
+
kind: 'IPPool',
|
|
291
|
+
metadata: { name: 'workloads' },
|
|
292
|
+
spec: {
|
|
293
|
+
allowedUses: ['Workload', 'Tunnel'],
|
|
294
|
+
assignmentMode: 'Automatic',
|
|
295
|
+
blockSize: 26,
|
|
296
|
+
cidr: '172.16.0.0/16',
|
|
297
|
+
ipipMode: 'CrossSubnet',
|
|
298
|
+
vxlanMode: 'Never',
|
|
299
|
+
natOutgoing: true,
|
|
300
|
+
disabled: false,
|
|
301
|
+
disableBGPExport: true,
|
|
302
|
+
nodeSelector: 'all()',
|
|
303
|
+
namespaceSelector: "environment == 'production'",
|
|
304
|
+
},
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
expect(html).toContain('IP Pool')
|
|
308
|
+
expect(html).toContain('Allowed Uses')
|
|
309
|
+
expect(html).toContain('Workload, Tunnel')
|
|
310
|
+
expect(html).toContain('Assignment Mode')
|
|
311
|
+
expect(html).toContain('Automatic')
|
|
312
|
+
expect(html).toContain('Block Size')
|
|
313
|
+
expect(html).toContain('26')
|
|
314
|
+
expect(html).toContain('CIDR')
|
|
315
|
+
expect(html).toContain('172.16.0.0/16')
|
|
316
|
+
expect(html).toContain('IP-in-IP Mode')
|
|
317
|
+
expect(html).toContain('CrossSubnet')
|
|
318
|
+
expect(html).toContain('VXLAN Mode')
|
|
319
|
+
expect(html).toContain('Never')
|
|
320
|
+
expect(html).toContain('NAT Outgoing')
|
|
321
|
+
expect(html).toContain('Yes')
|
|
322
|
+
expect(html).toContain('Disabled')
|
|
323
|
+
expect(html).toContain('No')
|
|
324
|
+
expect(html).toContain('BGP Export Disabled')
|
|
325
|
+
expect(html).toContain('Node Selector')
|
|
326
|
+
expect(html).toContain('all()')
|
|
327
|
+
expect(html).toContain('Namespace Selector')
|
|
328
|
+
expect(html).toContain('environment == 'production'')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('renders documented defaults when fields are omitted', () => {
|
|
332
|
+
const html = renderKind('ippools', {
|
|
333
|
+
apiVersion: 'projectcalico.org/v3',
|
|
334
|
+
kind: 'IPPool',
|
|
335
|
+
metadata: { name: 'default-ipv6' },
|
|
336
|
+
spec: { cidr: 'fd00::/48' },
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
expect(html).toContain('Workload, Tunnel')
|
|
340
|
+
expect(html).toContain('Automatic')
|
|
341
|
+
expect(html).toContain('122')
|
|
342
|
+
expect(html.match(/Never/g)).toHaveLength(2)
|
|
343
|
+
expect(html.match(/>No</g)).toHaveLength(3)
|
|
344
|
+
expect(html).toContain('all()')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it.each(['networking.example.io/v1', 'extension.projectcalico.org/v1'])('uses the generic renderer for %s', (apiVersion) => {
|
|
348
|
+
const html = renderKind('ippools', {
|
|
349
|
+
apiVersion,
|
|
350
|
+
kind: 'IPPool',
|
|
351
|
+
metadata: { name: 'foreign-pool' },
|
|
352
|
+
spec: { providerSpecificField: 'preserved' },
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
expect(html).toContain('Specification')
|
|
356
|
+
expect(html).toContain('Provider Specific Field')
|
|
357
|
+
expect(html).not.toContain('IP Pool')
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe('Calico HostEndpoint collision handling', () => {
|
|
362
|
+
it.each(['crd.projectcalico.org/v1', 'projectcalico.org/v3'])('renders HostEndpoint details for %s', (apiVersion) => {
|
|
363
|
+
const html = renderKind('hostendpoints', {
|
|
364
|
+
apiVersion,
|
|
365
|
+
kind: 'HostEndpoint',
|
|
366
|
+
metadata: { name: 'infra-1' },
|
|
367
|
+
spec: {
|
|
368
|
+
expectedIPs: ['172.20.16.133', '172.16.199.199'],
|
|
369
|
+
interfaceName: '*',
|
|
370
|
+
node: 'gdn-test-k8s-infra-1',
|
|
371
|
+
profiles: ['projectcalico-default-allow'],
|
|
372
|
+
ports: [{ name: 'ssh', protocol: 'TCP', port: 22 }],
|
|
373
|
+
},
|
|
374
|
+
}, '', () => {})
|
|
375
|
+
|
|
376
|
+
expect(html).toContain('Host Endpoint')
|
|
377
|
+
expect(html).toContain('Expected IPs')
|
|
378
|
+
expect(html).toContain('172.20.16.133')
|
|
379
|
+
expect(html).toContain('172.16.199.199')
|
|
380
|
+
expect(html).toContain('Interface Name')
|
|
381
|
+
expect(html).toContain('Node')
|
|
382
|
+
expect(html).toContain('<button')
|
|
383
|
+
expect(html).toContain('gdn-test-k8s-infra-1')
|
|
384
|
+
expect(html).toContain('Profiles')
|
|
385
|
+
expect(html).toContain('projectcalico-default-allow')
|
|
386
|
+
expect(html).toContain('Named Ports')
|
|
387
|
+
expect(html).toContain('ssh: TCP/22')
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it.each(['networking.example.io/v1', 'extension.projectcalico.org/v1'])('uses the generic renderer for %s', (apiVersion) => {
|
|
391
|
+
const html = renderKind('hostendpoints', {
|
|
392
|
+
apiVersion,
|
|
393
|
+
kind: 'HostEndpoint',
|
|
394
|
+
metadata: { name: 'foreign-endpoint' },
|
|
395
|
+
spec: { providerSpecificField: 'preserved' },
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
expect(html).toContain('Specification')
|
|
399
|
+
expect(html).toContain('Provider Specific Field')
|
|
400
|
+
expect(html).not.toContain('Host Endpoint')
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
describe('Calico Tier collision handling', () => {
|
|
405
|
+
it.each(['crd.projectcalico.org/v1', 'projectcalico.org/v3'])('renders Tier details for %s', (apiVersion) => {
|
|
406
|
+
const html = renderKind('tiers', {
|
|
407
|
+
apiVersion,
|
|
408
|
+
kind: 'Tier',
|
|
409
|
+
metadata: { name: 'default' },
|
|
410
|
+
spec: { defaultAction: 'Deny', order: 1000000 },
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
expect(html).toContain('Tier')
|
|
414
|
+
expect(html).toContain('Default Action')
|
|
415
|
+
expect(html).toContain('Deny')
|
|
416
|
+
expect(html).toContain('Order')
|
|
417
|
+
expect(html).toContain('1000000')
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('renders documented defaults when fields are omitted', () => {
|
|
421
|
+
const html = renderKind('tiers', {
|
|
422
|
+
apiVersion: 'projectcalico.org/v3',
|
|
423
|
+
kind: 'Tier',
|
|
424
|
+
metadata: { name: 'default' },
|
|
425
|
+
spec: {},
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
expect(html).toContain('Deny')
|
|
429
|
+
expect(html).toContain('Last (lowest precedence)')
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it.each(['networking.example.io/v1', 'extension.projectcalico.org/v1'])('uses the generic renderer for %s', (apiVersion) => {
|
|
433
|
+
const html = renderKind('tiers', {
|
|
434
|
+
apiVersion,
|
|
435
|
+
kind: 'Tier',
|
|
436
|
+
metadata: { name: 'foreign-tier' },
|
|
437
|
+
spec: { providerSpecificField: 'preserved' },
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
expect(html).toContain('Specification')
|
|
441
|
+
expect(html).toContain('Provider Specific Field')
|
|
442
|
+
expect(html).not.toContain('Default Action')
|
|
443
|
+
})
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
describe('Calico network policy collision handling', () => {
|
|
447
|
+
const calicoPolicies = [
|
|
448
|
+
['NetworkPolicy', 'networkpolicies', 'CalicoNetworkPolicy'],
|
|
449
|
+
['GlobalNetworkPolicy', 'globalnetworkpolicies', 'CalicoGlobalNetworkPolicy'],
|
|
450
|
+
['StagedNetworkPolicy', 'stagednetworkpolicies', 'CalicoStagedNetworkPolicy'],
|
|
451
|
+
['StagedGlobalNetworkPolicy', 'stagedglobalnetworkpolicies', 'CalicoStagedGlobalNetworkPolicy'],
|
|
452
|
+
['StagedKubernetesNetworkPolicy', 'stagedkubernetesnetworkpolicies', 'CalicoStagedKubernetesNetworkPolicy'],
|
|
453
|
+
] as const
|
|
454
|
+
|
|
455
|
+
it.each(calicoPolicies)('renders %s for both supported Calico groups', (kind, plural, label) => {
|
|
456
|
+
for (const apiVersion of ['crd.projectcalico.org/v1', 'projectcalico.org/v3']) {
|
|
457
|
+
const spec = kind === 'StagedKubernetesNetworkPolicy'
|
|
458
|
+
? {
|
|
459
|
+
podSelector: { matchLabels: { app: 'api' } },
|
|
460
|
+
policyTypes: ['Ingress'],
|
|
461
|
+
stagedAction: 'Deny',
|
|
462
|
+
ingress: [{ from: [{ ipBlock: { cidr: '10.0.0.0/8' } }] }],
|
|
463
|
+
}
|
|
464
|
+
: {
|
|
465
|
+
selector: "app == 'api'",
|
|
466
|
+
tier: 'security',
|
|
467
|
+
order: 100,
|
|
468
|
+
types: ['Ingress'],
|
|
469
|
+
stagedAction: 'Deny',
|
|
470
|
+
ingress: [{ action: 'Log', protocol: 'TCP', source: { nets: ['10.0.0.0/8'] } }],
|
|
471
|
+
}
|
|
472
|
+
const html = renderKind(plural, {
|
|
473
|
+
apiVersion,
|
|
474
|
+
kind,
|
|
475
|
+
metadata: { name: 'policy', namespace: 'default' },
|
|
476
|
+
spec,
|
|
477
|
+
}, 'default')
|
|
478
|
+
|
|
479
|
+
if (kind === 'StagedKubernetesNetworkPolicy') {
|
|
480
|
+
expect(html).toContain('Target')
|
|
481
|
+
expect(html).toContain('Ingress Rules')
|
|
482
|
+
expect(html).toContain('Pod Selector')
|
|
483
|
+
expect(html).toContain('Staged preview')
|
|
484
|
+
expect(html).toContain('Dashed paths are evaluated but not enforced')
|
|
485
|
+
expect(html).toContain('stroke-dasharray="4 3"')
|
|
486
|
+
expect(html).not.toContain('CalicoStagedKubernetesNetworkPolicy')
|
|
487
|
+
expect(html).not.toContain('Allow')
|
|
488
|
+
} else {
|
|
489
|
+
expect(html).toContain(label)
|
|
490
|
+
expect(html).toContain('security')
|
|
491
|
+
expect(html).toContain('Log')
|
|
492
|
+
}
|
|
493
|
+
expect(html).toContain('10.0.0.0/8')
|
|
494
|
+
if (kind.startsWith('Staged') && kind !== 'StagedKubernetesNetworkPolicy') expect(html).toContain('Deny')
|
|
495
|
+
}
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
it('keeps core networking.k8s.io NetworkPolicy on the core renderer', () => {
|
|
499
|
+
const html = renderKind('networkpolicies', {
|
|
500
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
501
|
+
kind: 'NetworkPolicy',
|
|
502
|
+
metadata: { name: 'core', namespace: 'default' },
|
|
503
|
+
spec: { podSelector: {}, policyTypes: ['Ingress'], ingress: [] },
|
|
504
|
+
}, 'default')
|
|
505
|
+
|
|
506
|
+
expect(html).toContain('Pod Selector')
|
|
507
|
+
expect(html).toContain('Deny all ingress')
|
|
508
|
+
expect(html).not.toContain('Calico NetworkPolicy')
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('uses the native staged Kubernetes presentation without an Allow action', () => {
|
|
512
|
+
const html = renderKind('stagedkubernetesnetworkpolicies', {
|
|
513
|
+
apiVersion: 'projectcalico.org/v3',
|
|
514
|
+
kind: 'StagedKubernetesNetworkPolicy',
|
|
515
|
+
metadata: { name: 'staged', namespace: 'default' },
|
|
516
|
+
spec: {
|
|
517
|
+
podSelector: { matchLabels: { app: 'api' } },
|
|
518
|
+
policyTypes: ['Ingress'],
|
|
519
|
+
ingress: [{}],
|
|
520
|
+
},
|
|
521
|
+
}, 'default')
|
|
522
|
+
|
|
523
|
+
expect(html).toContain('Target')
|
|
524
|
+
expect(html).toContain('Ingress Rules')
|
|
525
|
+
expect(html).toContain('All sources')
|
|
526
|
+
expect(html).toContain('Staged preview')
|
|
527
|
+
expect(html).toContain('Dashed paths are evaluated but not enforced')
|
|
528
|
+
expect(html).toContain('stroke-dasharray="4 3"')
|
|
529
|
+
expect(html).not.toContain('Allow')
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it.each([
|
|
533
|
+
['networkpolicies', 'other.example.io/v1', 'NetworkPolicy'],
|
|
534
|
+
['networkpolicies', 'extension.projectcalico.org/v1', 'NetworkPolicy'],
|
|
535
|
+
['globalnetworkpolicies', 'other.example.io/v1', 'GlobalNetworkPolicy'],
|
|
536
|
+
['stagednetworkpolicies', 'other.example.io/v1', 'StagedNetworkPolicy'],
|
|
537
|
+
['stagedglobalnetworkpolicies', 'other.example.io/v1', 'StagedGlobalNetworkPolicy'],
|
|
538
|
+
['stagedkubernetesnetworkpolicies', 'other.example.io/v1', 'StagedKubernetesNetworkPolicy'],
|
|
539
|
+
])('uses GenericRenderer for foreign %s/%s', (plural, apiVersion, kind) => {
|
|
540
|
+
const html = renderKind(plural, {
|
|
541
|
+
apiVersion,
|
|
542
|
+
kind,
|
|
543
|
+
metadata: { name: 'foreign', namespace: 'default' },
|
|
544
|
+
spec: { providerSpecificField: 'preserved' },
|
|
545
|
+
}, 'default')
|
|
546
|
+
|
|
547
|
+
expect(html).toContain('Specification')
|
|
548
|
+
expect(html).toContain('Provider Specific Field')
|
|
549
|
+
expect(html).not.toContain('Calico')
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
|
|
240
553
|
describe('colliding plurals — near-match API groups', () => {
|
|
241
554
|
// A substring guard would hand these to CNPG/Velero. They are different groups.
|
|
242
555
|
it.each([
|
|
@@ -264,3 +577,66 @@ describe('colliding plurals — near-match API groups', () => {
|
|
|
264
577
|
expect(html).not.toContain('Cluster Overview')
|
|
265
578
|
})
|
|
266
579
|
})
|
|
580
|
+
|
|
581
|
+
describe('shared plurals — the status path collides too', () => {
|
|
582
|
+
// The renderer and the status badge are two separate switches over the same
|
|
583
|
+
// plural, and fixing one leaves the other reading a foreign CRD's conditions
|
|
584
|
+
// as if they were Knative's.
|
|
585
|
+
it('does not give a foreign subscriptions CRD a Knative status', () => {
|
|
586
|
+
const s = getResourceStatus('subscriptions', {
|
|
587
|
+
apiVersion: 'operators.coreos.com/v1alpha1',
|
|
588
|
+
status: { conditions: [{ type: 'Ready', status: 'False' }], phase: 'AtLatestKnown' },
|
|
589
|
+
})
|
|
590
|
+
expect(s?.text).toBe('AtLatestKnown')
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('still gives Knative its own status', () => {
|
|
594
|
+
const s = getResourceStatus('subscriptions', {
|
|
595
|
+
apiVersion: 'messaging.knative.dev/v1',
|
|
596
|
+
status: { conditions: [{ type: 'Ready', status: 'True' }] },
|
|
597
|
+
})
|
|
598
|
+
expect(s?.text).toBe('Ready')
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
// The legacy namespaced Kyverno kind is served as `policies`, which several
|
|
602
|
+
// operators also use.
|
|
603
|
+
it('gives a Kyverno Policy its policy status and a foreign one the generic', () => {
|
|
604
|
+
const kyverno = getResourceStatus('policies', {
|
|
605
|
+
apiVersion: 'kyverno.io/v1',
|
|
606
|
+
spec: { validationFailureAction: 'Audit' },
|
|
607
|
+
status: { conditions: [{ type: 'Ready', status: 'True', reason: 'Succeeded' }] },
|
|
608
|
+
})
|
|
609
|
+
expect(kyverno?.text).toBeTruthy()
|
|
610
|
+
const foreign = getResourceStatus('policies', {
|
|
611
|
+
apiVersion: 'operators.coreos.com/v1',
|
|
612
|
+
status: { phase: 'Installed' },
|
|
613
|
+
})
|
|
614
|
+
expect(foreign?.text).toBe('Installed')
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
// A BackupRepository has custom table columns and raises its own issue, but no
|
|
619
|
+
// detail renderer — so it renders generically. Its status still has to resolve
|
|
620
|
+
// through the Velero mapping, or the drawer header prints the raw phase next to
|
|
621
|
+
// a table cell showing the human label for the same object.
|
|
622
|
+
describe('Velero BackupRepository status', () => {
|
|
623
|
+
const repo = (phase: string) => ({
|
|
624
|
+
apiVersion: 'velero.io/v1',
|
|
625
|
+
kind: 'BackupRepository',
|
|
626
|
+
metadata: { name: 'r', namespace: 'velero' },
|
|
627
|
+
spec: { repositoryType: 'kopia' },
|
|
628
|
+
status: { phase },
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
it('resolves through the Velero mapping, not the raw phase', () => {
|
|
632
|
+
expect(getResourceStatus('backuprepositories', repo('NotReady'))?.text).toBe('Not ready')
|
|
633
|
+
expect(getResourceStatus('backuprepositories', repo('Ready'))?.text).toBe('Ready')
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
// The plural is not Velero's alone; another group's repository must not pick
|
|
637
|
+
// up Velero's mapping.
|
|
638
|
+
it('leaves a foreign backuprepositories kind alone', () => {
|
|
639
|
+
const foreign = { ...repo('NotReady'), apiVersion: 'example.com/v1' }
|
|
640
|
+
expect(getResourceStatus('backuprepositories', foreign)?.text).not.toBe('Not ready')
|
|
641
|
+
})
|
|
642
|
+
})
|