@skyhook-io/k8s-ui 1.10.5 → 1.12.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 +1 -1
- 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/IssuesView.tsx +9 -0
- package/src/components/issues/diagnostic.ts +3 -0
- package/src/components/issues/issues.test.ts +11 -0
- package/src/components/issues/severity.ts +3 -0
- package/src/components/issues/types.ts +3 -0
- package/src/components/resources/ResourcesSidebar.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +261 -30
- package/src/components/resources/index.ts +2 -0
- package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
- package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +133 -9
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +19 -4
- package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
- package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/EventRenderer.tsx +10 -18
- 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 +317 -0
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
- package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/PodRenderer.tsx +13 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +102 -0
- package/src/components/resources/renderers/PolicySection.tsx +170 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
- package/src/components/resources/renderers/index.ts +5 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +9 -0
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
- package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
- package/src/components/resources/renderers/velero-cells.tsx +128 -9
- package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
- package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +590 -1
- package/src/components/resources/resource-utils-cnpg.ts +433 -56
- package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
- package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
- package/src/components/resources/resource-utils-velero.test.ts +237 -0
- package/src/components/resources/resource-utils-velero.ts +214 -22
- package/src/components/resources/resource-utils.ts +70 -3
- package/src/components/shared/ResourceActionsBar.tsx +3 -1
- package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
- package/src/components/topology/K8sResourceNode.tsx +6 -0
- package/src/components/trace/ReachabilityGraph.tsx +20 -6
- package/src/components/trace/ReachabilityView.tsx +253 -52
- package/src/components/trace/problemRows.test.ts +107 -0
- package/src/components/trace/reachGraphModel.test.ts +16 -0
- package/src/components/trace/reachInspector.test.ts +233 -10
- package/src/components/trace/reachInspector.ts +166 -36
- package/src/components/trace/reachMarks.test.ts +32 -1
- package/src/components/trace/reachMarks.ts +44 -0
- package/src/components/trace/reachOrigins.ts +12 -0
- package/src/components/trace/types.ts +25 -0
- package/src/components/ui/Badge.tsx +36 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
- package/src/theme/components.css +26 -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 +42 -0
- package/src/utils/api-resources.ts +2 -0
- package/src/utils/resource-icons.test.ts +33 -0
- package/src/utils/resource-icons.ts +65 -2
|
@@ -153,9 +153,14 @@ import { ResourceClaimCell, ResourceClaimTemplateCell, DeviceClassCell, Resource
|
|
|
153
153
|
import { NvidiaClusterPolicyCell, NvidiaDriverCell } from './renderers/nvidia-cells'
|
|
154
154
|
import { ServiceMonitorCell, PrometheusRuleCell, PodMonitorCell } from './renderers/prometheus-cells'
|
|
155
155
|
import { PolicyReportCell, ClusterPolicyReportCell, KyvernoPolicyCell, ClusterPolicyCell } from './renderers/kyverno-cells'
|
|
156
|
+
import { KyvernoModernPolicyCell, KyvernoPolicyExceptionCell, KyvernoCleanupPolicyCell } from './renderers/kyverno-modern-cells'
|
|
157
|
+
import { KYVERNO_MODERN_PLURALS, isModernKyvernoPolicy } from './resource-utils-kyverno-modern'
|
|
158
|
+
import { isAnyKyvernoPolicyException } from './resource-utils-kyverno-exceptions'
|
|
156
159
|
import { ExternalSecretCell, ClusterExternalSecretCell, SecretStoreCell, ClusterSecretStoreCell } from './renderers/eso-cells'
|
|
157
|
-
import { BackupCell, RestoreCell, ScheduleCell, BackupStorageLocationCell } from './renderers/velero-cells'
|
|
160
|
+
import { BackupCell, RestoreCell, ScheduleCell, BackupStorageLocationCell, VolumeSnapshotLocationCell, BackupRepositoryCell } from './renderers/velero-cells'
|
|
161
|
+
import { isVeleroResource } from './resource-utils-velero'
|
|
158
162
|
import { CNPGClusterCell, CNPGBackupCell, CNPGScheduledBackupCell, CNPGPoolerCell } from './renderers/cnpg-cells'
|
|
163
|
+
import { isApiGroup, CNPG_GROUP } from './resource-utils-cnpg'
|
|
159
164
|
import { ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
|
|
160
165
|
import { isManagedResource, isComposite } from './resource-utils-crossplane'
|
|
161
166
|
import { VirtualServiceCell, DestinationRuleCell, IstioGatewayCell, ServiceEntryCell, PeerAuthenticationCell, AuthorizationPolicyCell } from './renderers/istio-cells'
|
|
@@ -702,20 +707,22 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
702
707
|
policyreports: [
|
|
703
708
|
{ key: 'name', label: 'Name' },
|
|
704
709
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
705
|
-
{ key: '
|
|
710
|
+
{ key: 'scope', label: 'Subject', width: 'w-56' },
|
|
711
|
+
{ key: 'status', label: 'Status', width: 'w-32' },
|
|
706
712
|
{ key: 'pass', label: 'Pass', width: 'w-16' },
|
|
707
713
|
{ key: 'fail', label: 'Fail', width: 'w-16' },
|
|
708
|
-
{ key: 'warn', label: 'Warn', width: 'w-
|
|
714
|
+
{ key: 'warn', label: 'Warn', width: 'w-20' },
|
|
709
715
|
{ key: 'error', label: 'Err', width: 'w-16' },
|
|
710
716
|
{ key: 'skip', label: 'Skip', width: 'w-16' },
|
|
711
717
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
712
718
|
],
|
|
713
719
|
clusterpolicyreports: [
|
|
714
720
|
{ key: 'name', label: 'Name' },
|
|
715
|
-
{ key: '
|
|
721
|
+
{ key: 'scope', label: 'Subject', width: 'w-56' },
|
|
722
|
+
{ key: 'status', label: 'Status', width: 'w-32' },
|
|
716
723
|
{ key: 'pass', label: 'Pass', width: 'w-16' },
|
|
717
724
|
{ key: 'fail', label: 'Fail', width: 'w-16' },
|
|
718
|
-
{ key: 'warn', label: 'Warn', width: 'w-
|
|
725
|
+
{ key: 'warn', label: 'Warn', width: 'w-20' },
|
|
719
726
|
{ key: 'error', label: 'Err', width: 'w-16' },
|
|
720
727
|
{ key: 'skip', label: 'Skip', width: 'w-16' },
|
|
721
728
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -735,6 +742,119 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
735
742
|
{ key: 'rules', label: 'Rules', width: 'w-16' },
|
|
736
743
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
737
744
|
],
|
|
745
|
+
// Kyverno modern CEL family (policies.kyverno.io). "Enforcement" is the
|
|
746
|
+
// effective posture, not spec.validationActions verbatim — a policy that
|
|
747
|
+
// declares Deny with admission evaluation disabled blocks nothing.
|
|
748
|
+
validatingpolicies: [
|
|
749
|
+
{ key: 'name', label: 'Name' },
|
|
750
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44', tooltip: 'Effective enforcement posture, accounting for whether admission evaluation is enabled' },
|
|
751
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40', tooltip: 'Resources matched by spec.matchConstraints' },
|
|
752
|
+
{ key: 'rules', label: 'Rules', width: 'w-20', tooltip: 'CEL validation expressions' },
|
|
753
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
754
|
+
],
|
|
755
|
+
namespacedvalidatingpolicies: [
|
|
756
|
+
{ key: 'name', label: 'Name' },
|
|
757
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
758
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44', tooltip: 'Effective enforcement posture, accounting for whether admission evaluation is enabled' },
|
|
759
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
760
|
+
{ key: 'rules', label: 'Rules', width: 'w-20' },
|
|
761
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
762
|
+
],
|
|
763
|
+
imagevalidatingpolicies: [
|
|
764
|
+
{ key: 'name', label: 'Name' },
|
|
765
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
766
|
+
{ key: 'images', label: 'Images', width: 'min-w-40', tooltip: 'Image references this policy verifies' },
|
|
767
|
+
{ key: 'attestors', label: 'Attestors', width: 'w-20', tooltip: 'Trusted signing authorities' },
|
|
768
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
769
|
+
],
|
|
770
|
+
namespacedimagevalidatingpolicies: [
|
|
771
|
+
{ key: 'name', label: 'Name' },
|
|
772
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
773
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
774
|
+
{ key: 'images', label: 'Images', width: 'min-w-40' },
|
|
775
|
+
{ key: 'attestors', label: 'Attestors', width: 'w-20' },
|
|
776
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
777
|
+
],
|
|
778
|
+
mutatingpolicies: [
|
|
779
|
+
{ key: 'name', label: 'Name' },
|
|
780
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
781
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
782
|
+
{ key: 'rules', label: 'Mutations', width: 'w-20' },
|
|
783
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
784
|
+
],
|
|
785
|
+
namespacedmutatingpolicies: [
|
|
786
|
+
{ key: 'name', label: 'Name' },
|
|
787
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
788
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
789
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
790
|
+
{ key: 'rules', label: 'Mutations', width: 'w-20' },
|
|
791
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
792
|
+
],
|
|
793
|
+
generatingpolicies: [
|
|
794
|
+
{ key: 'name', label: 'Name' },
|
|
795
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
796
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
797
|
+
{ key: 'rules', label: 'Generates', width: 'w-20' },
|
|
798
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
799
|
+
],
|
|
800
|
+
namespacedgeneratingpolicies: [
|
|
801
|
+
{ key: 'name', label: 'Name' },
|
|
802
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
803
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
804
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
805
|
+
{ key: 'rules', label: 'Generates', width: 'w-20' },
|
|
806
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
807
|
+
],
|
|
808
|
+
// The Deleting kinds are the only modern family whose headline column is the
|
|
809
|
+
// schedule rather than the enforcement posture, so they carry their own
|
|
810
|
+
// health signal. It is Last Run, NOT readiness: Kyverno 1.18.2 declares a
|
|
811
|
+
// READY printer column on these CRDs but never populates
|
|
812
|
+
// status.conditionStatus.ready, so `kubectl get deletingpolicies` prints it
|
|
813
|
+
// blank and a Ready column here would read "unknown" on every row forever.
|
|
814
|
+
// It also has nothing to catch — an uncompilable policy is rejected at
|
|
815
|
+
// admission and never exists. What does go wrong is a policy that exists and
|
|
816
|
+
// silently never fires, which lastExecutionTime shows.
|
|
817
|
+
//
|
|
818
|
+
// The other eight kinds deliberately have no readiness column either: their
|
|
819
|
+
// status cell already returns "Not Ready" IN PLACE OF the posture (see
|
|
820
|
+
// getModernKyvernoPolicyStatus), so a second one would be redundant.
|
|
821
|
+
deletingpolicies: [
|
|
822
|
+
{ key: 'name', label: 'Name' },
|
|
823
|
+
{ key: 'lastRun', label: 'Last Run', width: 'w-28', tooltip: 'When the schedule last fired. A scheduled policy that has never run is the failure worth catching — Kyverno rejects uncompilable policies at admission, so a broken one never exists to flag.' },
|
|
824
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-32', tooltip: 'Cron schedule on which matched resources are deleted' },
|
|
825
|
+
{ key: 'appliesTo', label: 'Deletes', width: 'min-w-40' },
|
|
826
|
+
{ key: 'rules', label: 'Conditions', width: 'w-28', tooltip: 'CEL conditions narrowing what is deleted; none means every matched resource' },
|
|
827
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
828
|
+
],
|
|
829
|
+
namespaceddeletingpolicies: [
|
|
830
|
+
{ key: 'name', label: 'Name' },
|
|
831
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
832
|
+
{ key: 'lastRun', label: 'Last Run', width: 'w-28', tooltip: 'When the schedule last fired. A scheduled policy that has never run is the failure worth catching — Kyverno rejects uncompilable policies at admission, so a broken one never exists to flag.' },
|
|
833
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-32' },
|
|
834
|
+
{ key: 'appliesTo', label: 'Deletes', width: 'min-w-40' },
|
|
835
|
+
{ key: 'rules', label: 'Conditions', width: 'w-28' },
|
|
836
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
837
|
+
],
|
|
838
|
+
policyexceptions: [
|
|
839
|
+
{ key: 'name', label: 'Name' },
|
|
840
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
841
|
+
{ key: 'status', label: 'Exempts', width: 'w-28', tooltip: 'How many policies this exception bypasses' },
|
|
842
|
+
{ key: 'policies', label: 'Policies', width: 'min-w-40' },
|
|
843
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
844
|
+
],
|
|
845
|
+
cleanuppolicies: [
|
|
846
|
+
{ key: 'name', label: 'Name' },
|
|
847
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
848
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-32' },
|
|
849
|
+
{ key: 'appliesTo', label: 'Deletes', width: 'min-w-40' },
|
|
850
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
851
|
+
],
|
|
852
|
+
clustercleanuppolicies: [
|
|
853
|
+
{ key: 'name', label: 'Name' },
|
|
854
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-32' },
|
|
855
|
+
{ key: 'appliesTo', label: 'Deletes', width: 'min-w-40' },
|
|
856
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
857
|
+
],
|
|
738
858
|
grpcroutes: [
|
|
739
859
|
{ key: 'name', label: 'Name' },
|
|
740
860
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
@@ -1100,39 +1220,64 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1100
1220
|
backups: [
|
|
1101
1221
|
{ key: 'name', label: 'Name' },
|
|
1102
1222
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1103
|
-
{ key: 'status', label: 'Status', width: 'w-
|
|
1223
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1104
1224
|
{ key: 'storageLocation', label: 'Storage', width: 'w-36' },
|
|
1105
1225
|
{ key: 'namespaces', label: 'Scope', width: 'w-24', tooltip: 'Included namespaces (* = all)' },
|
|
1106
1226
|
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
1107
1227
|
{ key: 'expiry', label: 'Expires', width: 'w-24' },
|
|
1108
|
-
{ key: 'errors', label: 'Errors', width: 'w-
|
|
1228
|
+
{ key: 'errors', label: 'Errors', width: 'w-28' },
|
|
1109
1229
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1110
1230
|
],
|
|
1111
|
-
restores
|
|
1231
|
+
// `restores` and `schedules` are keyed group-qualified (see
|
|
1232
|
+
// GROUP_QUALIFIED_COLUMN_KEYS): rancher/backup-restore-operator ships
|
|
1233
|
+
// restores.resources.cattle.io and several operators ship their own
|
|
1234
|
+
// `schedules` kind. Only velero.io resolves to these column sets;
|
|
1235
|
+
// everything else falls through to the generic columns.
|
|
1236
|
+
velerorestores: [
|
|
1112
1237
|
{ key: 'name', label: 'Name' },
|
|
1113
1238
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1114
|
-
{ key: 'status', label: 'Status', width: 'w-
|
|
1239
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1115
1240
|
{ key: 'backupName', label: 'Backup', width: 'w-40' },
|
|
1116
1241
|
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
1117
|
-
{ key: 'errors', label: 'Errors', width: 'w-
|
|
1242
|
+
{ key: 'errors', label: 'Errors', width: 'w-28' },
|
|
1118
1243
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1119
1244
|
],
|
|
1120
|
-
|
|
1245
|
+
veleroschedules: [
|
|
1121
1246
|
{ key: 'name', label: 'Name' },
|
|
1122
1247
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1123
|
-
{ key: 'status', label: 'Status', width: 'w-
|
|
1124
|
-
{ key: 'schedule', label: 'Schedule', width: 'w-
|
|
1125
|
-
{ key: 'lastBackup', label: 'Last Backup', width: 'w-
|
|
1126
|
-
{ key: '
|
|
1248
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1249
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-40' },
|
|
1250
|
+
{ key: 'lastBackup', label: 'Last Backup', width: 'w-32' },
|
|
1251
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1252
|
+
],
|
|
1253
|
+
// No status column on purpose: the VSL controller never populates
|
|
1254
|
+
// status.phase, so a badge would read "Unknown" on every row forever.
|
|
1255
|
+
volumesnapshotlocations: [
|
|
1256
|
+
{ key: 'name', label: 'Name' },
|
|
1257
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1258
|
+
{ key: 'provider', label: 'Provider', width: 'w-32' },
|
|
1259
|
+
{ key: 'config', label: 'Config' },
|
|
1260
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1261
|
+
],
|
|
1262
|
+
// repositoryType is kopia|restic, and it is what `kubectl get
|
|
1263
|
+
// backuprepositories` shows. It earns a column because restic is being retired
|
|
1264
|
+
// (no new backups since v1.17, restore dropped in v1.19), which makes it a
|
|
1265
|
+
// migration liability worth spotting by scanning rather than by opening each
|
|
1266
|
+
// repository one at a time.
|
|
1267
|
+
backuprepositories: [
|
|
1268
|
+
{ key: 'name', label: 'Name' },
|
|
1269
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1270
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1271
|
+
{ key: 'repositoryType', label: 'Type', width: 'w-24' },
|
|
1127
1272
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1128
1273
|
],
|
|
1129
1274
|
backupstoragelocations: [
|
|
1130
1275
|
{ key: 'name', label: 'Name' },
|
|
1131
1276
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1132
|
-
{ key: 'status', label: 'Status', width: 'w-
|
|
1277
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1133
1278
|
{ key: 'provider', label: 'Provider', width: 'w-24' },
|
|
1134
1279
|
{ key: 'bucket', label: 'Bucket', width: 'w-40' },
|
|
1135
|
-
{ key: 'default', label: 'Default', width: 'w-
|
|
1280
|
+
{ key: 'default', label: 'Default', width: 'w-24' },
|
|
1136
1281
|
{ key: 'lastValidation', label: 'Validated', width: 'w-28' },
|
|
1137
1282
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1138
1283
|
],
|
|
@@ -1142,11 +1287,34 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1142
1287
|
cnpgclusters: [
|
|
1143
1288
|
{ key: 'name', label: 'Name' },
|
|
1144
1289
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1145
|
-
|
|
1290
|
+
// w-28 leaves the "Status" label 0.23px short once the sort AND filter
|
|
1291
|
+
// affordances are both present, which renders "STAT…" — a sub-pixel miss
|
|
1292
|
+
// costs two characters because the ellipsis needs its own room. The filter
|
|
1293
|
+
// icon appears as soon as a column holds more than one distinct value, so
|
|
1294
|
+
// this is latent on any status column, not specific to the current data.
|
|
1295
|
+
// w-44 fits "WAL Archiving Failing" (120.2px against a 127px label budget).
|
|
1296
|
+
// CNPG's own phases are mapped to short display states — see
|
|
1297
|
+
// getCNPGClusterDisplayState; a 315px sentence fits no column at all.
|
|
1298
|
+
{ key: 'status', label: 'Status', width: 'w-44' },
|
|
1146
1299
|
{ key: 'instances', label: 'Instances', width: 'w-28', tooltip: 'Ready/Total' },
|
|
1147
1300
|
{ key: 'primary', label: 'Primary', width: 'w-36' },
|
|
1148
1301
|
{ key: 'image', label: 'Image', width: 'w-28' },
|
|
1149
|
-
|
|
1302
|
+
// No Storage column: it rendered spec.storage.size, the configured REQUEST.
|
|
1303
|
+
// The useful number is actual usage (kubectl cnpg status shows "Size: 158M")
|
|
1304
|
+
// and that isn't in the CR. A plausible-but-wrong-meaning number is worse
|
|
1305
|
+
// than an absent one — the reader can't tell which meaning they're getting.
|
|
1306
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1307
|
+
],
|
|
1308
|
+
cnpgbackups: [
|
|
1309
|
+
{ key: 'name', label: 'Name' },
|
|
1310
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1311
|
+
{ key: 'status', label: 'Status', width: 'w-44' },
|
|
1312
|
+
{ key: 'cluster', label: 'Cluster', width: 'w-36' },
|
|
1313
|
+
// `barmanObjectStore` is the longest method and the field that decides how
|
|
1314
|
+
// the rest of the row reads; at w-36 it was permanently ellipsised.
|
|
1315
|
+
{ key: 'method', label: 'Method', width: 'w-40' },
|
|
1316
|
+
{ key: 'started', label: 'Started', width: 'w-24' },
|
|
1317
|
+
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
1150
1318
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1151
1319
|
],
|
|
1152
1320
|
scheduledbackups: [
|
|
@@ -1162,11 +1330,19 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1162
1330
|
poolers: [
|
|
1163
1331
|
{ key: 'name', label: 'Name' },
|
|
1164
1332
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1165
|
-
|
|
1333
|
+
// "Not Scheduled" (83.5px) is kept rather than shortened to "Unscheduled":
|
|
1334
|
+
// ScheduledBackup also renders a literal "Scheduled" badge in this same
|
|
1335
|
+
// column, so a standalone adjective would read as "has no cron" — a claim
|
|
1336
|
+
// about a field Poolers don't have. w-36 buys the words.
|
|
1337
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1166
1338
|
{ key: 'cluster', label: 'Cluster', width: 'w-36' },
|
|
1167
|
-
|
|
1339
|
+
// Sized for the HEADER, not the value: `rw`/`ro` need 27px, but "Type" plus
|
|
1340
|
+
// the sort affordance needs more than w-16 leaves, and the label rendered
|
|
1341
|
+
// as "T…".
|
|
1342
|
+
{ key: 'type', label: 'Type', width: 'w-24' },
|
|
1168
1343
|
{ key: 'poolMode', label: 'Pool Mode', width: 'w-32' },
|
|
1169
|
-
|
|
1344
|
+
// status.instances counts pods trying to be scheduled, not ready ones.
|
|
1345
|
+
{ key: 'instances', label: 'Instances', width: 'w-28', tooltip: 'Scheduled/Total' },
|
|
1170
1346
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1171
1347
|
],
|
|
1172
1348
|
// ============================================================================
|
|
@@ -1694,6 +1870,9 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1694
1870
|
// Map (plural, group) → KNOWN_COLUMNS key for kinds that collide with core K8s
|
|
1695
1871
|
const GROUP_QUALIFIED_COLUMN_KEYS: Record<string, Record<string, string>> = {
|
|
1696
1872
|
clusters: { 'postgresql.cnpg.io': 'cnpgclusters', 'cluster.x-k8s.io': 'capiclusters' },
|
|
1873
|
+
// Velero owns the unqualified `backups` column set; CNPG Backups carry a
|
|
1874
|
+
// completely different shape (cluster + method, no storage location/expiry).
|
|
1875
|
+
backups: { 'postgresql.cnpg.io': 'cnpgbackups' },
|
|
1697
1876
|
clusterpolicies: { 'nvidia.com': 'nvidiaclusterpolicies' },
|
|
1698
1877
|
services: { 'serving.knative.dev': 'knativeservices' },
|
|
1699
1878
|
configurations: { 'serving.knative.dev': 'knativeconfigurations' },
|
|
@@ -1701,6 +1880,8 @@ const GROUP_QUALIFIED_COLUMN_KEYS: Record<string, Record<string, string>> = {
|
|
|
1701
1880
|
routes: { 'serving.knative.dev': 'knativeroutes' },
|
|
1702
1881
|
ingresses: { 'networking.internal.knative.dev': 'knativeingresses' },
|
|
1703
1882
|
certificates: { 'networking.internal.knative.dev': 'knativecertificates' },
|
|
1883
|
+
restores: { 'velero.io': 'velerorestores' },
|
|
1884
|
+
schedules: { 'velero.io': 'veleroschedules' },
|
|
1704
1885
|
}
|
|
1705
1886
|
|
|
1706
1887
|
// Normalize a kind name to its plural API form used in KNOWN_COLUMNS keys.
|
|
@@ -5455,6 +5636,29 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5455
5636
|
|
|
5456
5637
|
// Kind-specific columns (normalize CRD singular names like 'ScaledObject' → 'scaledobjects')
|
|
5457
5638
|
const kindLower = normalizeKindToPlural(kind, group)
|
|
5639
|
+
|
|
5640
|
+
// Kyverno cells are group-gated ahead of the switch so a non-matching CR
|
|
5641
|
+
// falls through to the switch's default (GenericCell) instead of being
|
|
5642
|
+
// described in Kyverno's vocabulary. These plurals are generic enough that
|
|
5643
|
+
// another vendor could ship them, and `policyexceptions` is served by BOTH
|
|
5644
|
+
// Kyverno API families with different spec shapes. The stakes are higher
|
|
5645
|
+
// here than in the drawer: an absent `validationActions` legitimately means
|
|
5646
|
+
// Deny for a real Kyverno policy, so an ungated foreign CR would render a
|
|
5647
|
+
// red "Deny" badge purely because it lacks a field it never had.
|
|
5648
|
+
if (KYVERNO_MODERN_PLURALS.has(kindLower) && isModernKyvernoPolicy(resource)) {
|
|
5649
|
+
return <KyvernoModernPolicyCell resource={resource} column={column} />
|
|
5650
|
+
}
|
|
5651
|
+
if (kindLower === 'policyexceptions' && isAnyKyvernoPolicyException(resource)) {
|
|
5652
|
+
return <KyvernoPolicyExceptionCell resource={resource} column={column} />
|
|
5653
|
+
}
|
|
5654
|
+
if (
|
|
5655
|
+
(kindLower === 'cleanuppolicies' || kindLower === 'clustercleanuppolicies') &&
|
|
5656
|
+
typeof resource?.apiVersion === 'string' &&
|
|
5657
|
+
resource.apiVersion.startsWith('kyverno.io/')
|
|
5658
|
+
) {
|
|
5659
|
+
return <KyvernoCleanupPolicyCell resource={resource} column={column} />
|
|
5660
|
+
}
|
|
5661
|
+
|
|
5458
5662
|
switch (kindLower) {
|
|
5459
5663
|
case 'pods':
|
|
5460
5664
|
return <PodCell resource={resource} column={column} />
|
|
@@ -5644,26 +5848,53 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5644
5848
|
case 'clustersecretstores':
|
|
5645
5849
|
return <ClusterSecretStoreCell resource={resource} column={column} />
|
|
5646
5850
|
// Velero
|
|
5851
|
+
case 'cnpgbackups':
|
|
5852
|
+
return <CNPGBackupCell resource={resource} column={column} />
|
|
5647
5853
|
case 'backups':
|
|
5648
|
-
//
|
|
5649
|
-
|
|
5854
|
+
// Reached only when the group is unknown to normalizeKindToPlural. Both
|
|
5855
|
+
// engines are matched positively so a third `backups` CRD renders generic
|
|
5856
|
+
// rather than inheriting whichever branch happened to be the fallback.
|
|
5857
|
+
if (isApiGroup(resource.apiVersion, CNPG_GROUP)) {
|
|
5650
5858
|
return <CNPGBackupCell resource={resource} column={column} />
|
|
5651
5859
|
}
|
|
5652
|
-
|
|
5653
|
-
|
|
5860
|
+
if (isApiGroup(resource.apiVersion, 'velero.io')) {
|
|
5861
|
+
return <BackupCell resource={resource} column={column} />
|
|
5862
|
+
}
|
|
5863
|
+
return <GenericCell resource={resource} column={column} />
|
|
5864
|
+
case 'velerorestores':
|
|
5654
5865
|
return <RestoreCell resource={resource} column={column} />
|
|
5655
|
-
case '
|
|
5866
|
+
case 'veleroschedules':
|
|
5656
5867
|
return <ScheduleCell resource={resource} column={column} />
|
|
5657
5868
|
case 'backupstoragelocations':
|
|
5869
|
+
if (!isVeleroResource(resource)) return <GenericCell resource={resource} column={column} />
|
|
5658
5870
|
return <BackupStorageLocationCell resource={resource} column={column} />
|
|
5871
|
+
case 'volumesnapshotlocations':
|
|
5872
|
+
if (!isVeleroResource(resource)) return <GenericCell resource={resource} column={column} />
|
|
5873
|
+
return <VolumeSnapshotLocationCell resource={resource} column={column} />
|
|
5874
|
+
case 'backuprepositories':
|
|
5875
|
+
if (!isVeleroResource(resource)) return <GenericCell resource={resource} column={column} />
|
|
5876
|
+
return <BackupRepositoryCell resource={resource} column={column} />
|
|
5659
5877
|
// CloudNativePG
|
|
5660
5878
|
case 'cnpgclusters':
|
|
5661
|
-
case 'clusters':
|
|
5662
5879
|
return <CNPGClusterCell resource={resource} column={column} />
|
|
5880
|
+
case 'clusters':
|
|
5881
|
+
// Positive guard: `clusters` is one of the most collided CRD plurals
|
|
5882
|
+
// (CNPG, CAPI, KubeBlocks, Redis/Valkey operators). Anything else gets
|
|
5883
|
+
// the generic cell instead of a fabricated Postgres status.
|
|
5884
|
+
if (isApiGroup(resource.apiVersion, CNPG_GROUP)) {
|
|
5885
|
+
return <CNPGClusterCell resource={resource} column={column} />
|
|
5886
|
+
}
|
|
5887
|
+
return <GenericCell resource={resource} column={column} />
|
|
5663
5888
|
case 'scheduledbackups':
|
|
5664
|
-
|
|
5889
|
+
if (isApiGroup(resource.apiVersion, CNPG_GROUP)) {
|
|
5890
|
+
return <CNPGScheduledBackupCell resource={resource} column={column} />
|
|
5891
|
+
}
|
|
5892
|
+
return <GenericCell resource={resource} column={column} />
|
|
5665
5893
|
case 'poolers':
|
|
5666
|
-
|
|
5894
|
+
if (isApiGroup(resource.apiVersion, CNPG_GROUP)) {
|
|
5895
|
+
return <CNPGPoolerCell resource={resource} column={column} />
|
|
5896
|
+
}
|
|
5897
|
+
return <GenericCell resource={resource} column={column} />
|
|
5667
5898
|
// Istio Service Mesh
|
|
5668
5899
|
case 'virtualservices':
|
|
5669
5900
|
return <VirtualServiceCell resource={resource} column={column} />
|
|
@@ -11,6 +11,8 @@ export * from './resource-utils-karpenter'
|
|
|
11
11
|
export * from './resource-utils-keda'
|
|
12
12
|
export * from './resource-utils-knative'
|
|
13
13
|
export * from './resource-utils-kyverno'
|
|
14
|
+
export * from './resource-utils-kyverno-modern'
|
|
15
|
+
export * from './resource-utils-kyverno-exceptions'
|
|
14
16
|
export * from './resource-utils-prometheus'
|
|
15
17
|
export * from './resource-utils-trivy'
|
|
16
18
|
export * from './resource-utils-traefik'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
KYVERNO_MODERN_PLURALS,
|
|
4
|
+
getKyvernoLastExecutionTime,
|
|
5
|
+
getKyvernoPolicyReady,
|
|
6
|
+
getModernKyvernoPolicyStatus,
|
|
7
|
+
isModernKyvernoPolicy,
|
|
8
|
+
} from './resource-utils-kyverno-modern'
|
|
9
|
+
import { isAnyKyvernoPolicyException } from './resource-utils-kyverno-exceptions'
|
|
10
|
+
|
|
11
|
+
// Bugbot #4 (MEDIUM): the ResourcesView cell dispatch matched Kyverno plurals
|
|
12
|
+
// without checking the API group, so a foreign CRD sharing a plural picked up
|
|
13
|
+
// Kyverno columns and badges while its drawer correctly stayed generic.
|
|
14
|
+
//
|
|
15
|
+
// The stakes are higher in the table than in the drawer: an absent
|
|
16
|
+
// `validationActions` legitimately means Deny for a real Kyverno policy, so an
|
|
17
|
+
// ungated foreign CR renders a red "Deny" purely for lacking a field it never
|
|
18
|
+
// had. These pin the predicates the cell dispatch now gates on.
|
|
19
|
+
describe('kyverno cell group gating', () => {
|
|
20
|
+
const foreignValidatingPolicy = {
|
|
21
|
+
apiVersion: 'policy.example.com/v1',
|
|
22
|
+
kind: 'ValidatingPolicy',
|
|
23
|
+
metadata: { name: 'not-kyverno' },
|
|
24
|
+
spec: {},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it('rejects a foreign CR that merely shares the plural', () => {
|
|
28
|
+
expect(KYVERNO_MODERN_PLURALS.has('validatingpolicies')).toBe(true)
|
|
29
|
+
expect(isModernKyvernoPolicy(foreignValidatingPolicy)).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Proof the gate is load-bearing rather than defensive: ungated, this exact
|
|
33
|
+
// foreign object renders as Deny.
|
|
34
|
+
it('would read as Deny without the gate', () => {
|
|
35
|
+
expect(getModernKyvernoPolicyStatus(foreignValidatingPolicy).text).toBe('Deny')
|
|
36
|
+
expect(getModernKyvernoPolicyStatus(foreignValidatingPolicy).level).toBe('unhealthy')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('accepts a genuine modern Kyverno policy', () => {
|
|
40
|
+
const real = { apiVersion: 'policies.kyverno.io/v1', kind: 'ValidatingPolicy', spec: { validationActions: ['Audit'] } }
|
|
41
|
+
expect(isModernKyvernoPolicy(real)).toBe(true)
|
|
42
|
+
expect(getModernKyvernoPolicyStatus(real).text).toBe('Audit')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// PolicyException is served by BOTH Kyverno families, so the gate accepts
|
|
46
|
+
// either group while still rejecting a foreign one.
|
|
47
|
+
it('accepts PolicyException from both Kyverno groups, rejects foreign', () => {
|
|
48
|
+
expect(isAnyKyvernoPolicyException({ apiVersion: 'kyverno.io/v2' })).toBe(true)
|
|
49
|
+
expect(isAnyKyvernoPolicyException({ apiVersion: 'policies.kyverno.io/v1' })).toBe(true)
|
|
50
|
+
expect(isAnyKyvernoPolicyException({ apiVersion: 'exceptions.example.com/v1' })).toBe(false)
|
|
51
|
+
expect(isAnyKyvernoPolicyException({})).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// The legacy cleanup pair gates on the legacy group specifically.
|
|
55
|
+
it('gates CleanupPolicy on the legacy kyverno.io group', () => {
|
|
56
|
+
expect('kyverno.io/v2'.startsWith('kyverno.io/')).toBe(true)
|
|
57
|
+
expect('cleanup.example.com/v1'.startsWith('kyverno.io/')).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// The Deleting kinds carry a Last Run column rather than a readiness one.
|
|
62
|
+
// Verified on Kyverno 1.18.2: the CRD declares a READY printer column but the
|
|
63
|
+
// controller never populates status.conditionStatus.ready — even after the
|
|
64
|
+
// cron has fired, status is {conditionStatus:{message:""}, lastExecutionTime}.
|
|
65
|
+
// A Ready column would read "unknown" on every row forever. lastExecutionTime
|
|
66
|
+
// is populated, varies, and catches the failure that actually happens: a
|
|
67
|
+
// scheduled policy that silently never runs.
|
|
68
|
+
describe('deleting-policy last-run signal', () => {
|
|
69
|
+
const withLastRun = (t?: string) => ({
|
|
70
|
+
apiVersion: 'policies.kyverno.io/v1',
|
|
71
|
+
kind: 'DeletingPolicy',
|
|
72
|
+
spec: { schedule: '0 2 * * *' },
|
|
73
|
+
status: t ? { lastExecutionTime: t } : { conditionStatus: { message: '' } },
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('reads lastExecutionTime when the schedule has fired', () => {
|
|
77
|
+
expect(getKyvernoLastExecutionTime(withLastRun('2026-08-08T13:38:00Z'))).toBe('2026-08-08T13:38:00Z')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Must stay empty rather than defaulting to anything — the cell renders
|
|
81
|
+
// "Never run" for this, which is the state worth catching.
|
|
82
|
+
it('returns empty when the policy has never run', () => {
|
|
83
|
+
expect(getKyvernoLastExecutionTime(withLastRun())).toBe('')
|
|
84
|
+
expect(getKyvernoLastExecutionTime({})).toBe('')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// The status shape Kyverno 1.18.2 actually writes after a real cron tick.
|
|
88
|
+
it('is empty for the real post-tick shape that omits ready', () => {
|
|
89
|
+
const real = { status: { conditionStatus: { message: '' }, lastExecutionTime: '2026-08-08T13:38:00Z' } }
|
|
90
|
+
expect(getKyvernoLastExecutionTime(real)).toBe('2026-08-08T13:38:00Z')
|
|
91
|
+
expect(getKyvernoPolicyReady(real)).toBeUndefined()
|
|
92
|
+
})
|
|
93
|
+
})
|