@skyhook-io/k8s-ui 1.10.5 → 1.11.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/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 +259 -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/CNPGClusterRenderer.test.tsx +85 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
- package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/EventRenderer.tsx +10 -18
- 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/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/badge-no-handrolled.test.tsx +1 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -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 +527 -1
- package/src/components/resources/resource-utils-cnpg.ts +373 -56
- 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/utils/api-resources.ts +2 -0
- package/src/utils/resource-icons.test.ts +33 -0
- package/src/utils/resource-icons.ts +65 -2
package/package.json
CHANGED
|
@@ -240,6 +240,13 @@ export function IssueRow({
|
|
|
240
240
|
</Tooltip>
|
|
241
241
|
) : null}
|
|
242
242
|
</>
|
|
243
|
+
) : issue.onset_unknown ? (
|
|
244
|
+
<Tooltip content="Radar can confirm this issue is active, but current Kubernetes state does not reveal when it began." delay={200} wrapperClassName="shrink-0">
|
|
245
|
+
<span className="flex items-center gap-1 text-xs text-theme-text-tertiary">
|
|
246
|
+
<Clock className="h-3 w-3" aria-hidden />
|
|
247
|
+
Onset unknown
|
|
248
|
+
</span>
|
|
249
|
+
</Tooltip>
|
|
243
250
|
) : null}
|
|
244
251
|
</div>
|
|
245
252
|
);
|
|
@@ -451,6 +458,8 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
|
|
|
451
458
|
meta.push(timing.meta);
|
|
452
459
|
} else if (issue.first_seen) {
|
|
453
460
|
meta.push(`started ${formatRelativeAgeTime(issue.first_seen)}`);
|
|
461
|
+
} else if (issue.onset_unknown) {
|
|
462
|
+
meta.push('onset unknown');
|
|
454
463
|
}
|
|
455
464
|
if (issue.first_seen) {
|
|
456
465
|
if (issue.last_seen && timing?.kind !== 'creation') meta.push(`last seen ${formatRelativeAgeTime(issue.last_seen)}`);
|
|
@@ -46,6 +46,8 @@ export function diagnosticFactLabel(type: string): string {
|
|
|
46
46
|
return 'Stalled autoscalers';
|
|
47
47
|
case 'secret_not_ready':
|
|
48
48
|
return 'Dependent pods';
|
|
49
|
+
case 'admission_webhook_backend':
|
|
50
|
+
return 'Admission dependency';
|
|
49
51
|
default:
|
|
50
52
|
return type.replace(/_/g, ' ');
|
|
51
53
|
}
|
|
@@ -59,6 +61,7 @@ export function incidentParentLabel(factType?: string, confidence?: string): str
|
|
|
59
61
|
switch (factType) {
|
|
60
62
|
case 'pvc_blast_radius':
|
|
61
63
|
case 'secret_not_ready':
|
|
64
|
+
case 'admission_webhook_backend':
|
|
62
65
|
return 'Caused by';
|
|
63
66
|
case 'apiservice_hpa':
|
|
64
67
|
return 'Likely cause';
|
|
@@ -126,6 +126,17 @@ describe('IssueRow', () => {
|
|
|
126
126
|
expect(html).toContain('Control plane')
|
|
127
127
|
expect(html).toContain(groupBadgeClass('control_plane'))
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
it('shows an explicit unknown-onset state instead of inventing an age', () => {
|
|
131
|
+
const html = renderToString(createElement(IssueRow, {
|
|
132
|
+
issue: mk({ onset_unknown: true }),
|
|
133
|
+
open: false,
|
|
134
|
+
onToggle: () => undefined,
|
|
135
|
+
}))
|
|
136
|
+
|
|
137
|
+
expect(html).toContain('Onset unknown')
|
|
138
|
+
expect(html).not.toContain('0s')
|
|
139
|
+
})
|
|
129
140
|
})
|
|
130
141
|
|
|
131
142
|
describe('issueTiming', () => {
|
|
@@ -71,6 +71,7 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
71
71
|
secret_sync_failed: 'Secret sync failed',
|
|
72
72
|
service_no_endpoints: 'No endpoints',
|
|
73
73
|
ingress_backend_missing: 'Ingress backend missing',
|
|
74
|
+
ingress_class_missing: 'Ingress class missing',
|
|
74
75
|
load_balancer_pending: 'Load balancer pending',
|
|
75
76
|
gateway_not_ready: 'Gateway not ready',
|
|
76
77
|
gateway_route_invalid: 'Gateway route invalid',
|
|
@@ -82,6 +83,8 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
82
83
|
pvc_resize_failed: 'PVC resize failed',
|
|
83
84
|
volume_mount_failed: 'Volume mount failed',
|
|
84
85
|
volume_access_mode_conflict: 'Volume access conflict',
|
|
86
|
+
backup_failed: 'Backup failed',
|
|
87
|
+
backup_target_unavailable: 'Backup target unavailable',
|
|
85
88
|
job_failed: 'Job failed',
|
|
86
89
|
cronjob_failed: 'CronJob failed',
|
|
87
90
|
rollout_stalled: 'Rollout stalled',
|
|
@@ -198,6 +198,9 @@ export interface Issue {
|
|
|
198
198
|
* Capacity / Demand view, so a generic scheduling failure never links. */
|
|
199
199
|
capacity_relevant?: boolean;
|
|
200
200
|
first_seen?: string;
|
|
201
|
+
/** Radar can confirm the issue is active, but current cluster evidence does
|
|
202
|
+
* not establish when the failing state began. */
|
|
203
|
+
onset_unknown?: boolean;
|
|
201
204
|
last_seen?: string;
|
|
202
205
|
/** Affected-resource fan-out, EXCLUDING the subject (the row header).
|
|
203
206
|
* 0/omitted for a single-resource issue; e.g. 50 for one Deployment's
|
|
@@ -107,7 +107,7 @@ interface ResourceTypeButtonProps {
|
|
|
107
107
|
|
|
108
108
|
const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps>(
|
|
109
109
|
function ResourceTypeButton({ resource, count, isSelected, isHighlighted, isForbidden: forbidden, isPinned, onTogglePin, onClick }, ref) {
|
|
110
|
-
const Icon = getResourceIcon(resource.kind)
|
|
110
|
+
const Icon = getResourceIcon(resource.kind, resource.group)
|
|
111
111
|
return (
|
|
112
112
|
<button
|
|
113
113
|
ref={ref}
|
|
@@ -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,20 @@ 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: 'status', label: 'Status', width: 'w-
|
|
710
|
+
{ key: 'status', label: 'Status', width: 'w-32' },
|
|
706
711
|
{ key: 'pass', label: 'Pass', width: 'w-16' },
|
|
707
712
|
{ key: 'fail', label: 'Fail', width: 'w-16' },
|
|
708
|
-
{ key: 'warn', label: 'Warn', width: 'w-
|
|
713
|
+
{ key: 'warn', label: 'Warn', width: 'w-20' },
|
|
709
714
|
{ key: 'error', label: 'Err', width: 'w-16' },
|
|
710
715
|
{ key: 'skip', label: 'Skip', width: 'w-16' },
|
|
711
716
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
712
717
|
],
|
|
713
718
|
clusterpolicyreports: [
|
|
714
719
|
{ key: 'name', label: 'Name' },
|
|
715
|
-
{ key: 'status', label: 'Status', width: 'w-
|
|
720
|
+
{ key: 'status', label: 'Status', width: 'w-32' },
|
|
716
721
|
{ key: 'pass', label: 'Pass', width: 'w-16' },
|
|
717
722
|
{ key: 'fail', label: 'Fail', width: 'w-16' },
|
|
718
|
-
{ key: 'warn', label: 'Warn', width: 'w-
|
|
723
|
+
{ key: 'warn', label: 'Warn', width: 'w-20' },
|
|
719
724
|
{ key: 'error', label: 'Err', width: 'w-16' },
|
|
720
725
|
{ key: 'skip', label: 'Skip', width: 'w-16' },
|
|
721
726
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -735,6 +740,119 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
735
740
|
{ key: 'rules', label: 'Rules', width: 'w-16' },
|
|
736
741
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
737
742
|
],
|
|
743
|
+
// Kyverno modern CEL family (policies.kyverno.io). "Enforcement" is the
|
|
744
|
+
// effective posture, not spec.validationActions verbatim — a policy that
|
|
745
|
+
// declares Deny with admission evaluation disabled blocks nothing.
|
|
746
|
+
validatingpolicies: [
|
|
747
|
+
{ key: 'name', label: 'Name' },
|
|
748
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44', tooltip: 'Effective enforcement posture, accounting for whether admission evaluation is enabled' },
|
|
749
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40', tooltip: 'Resources matched by spec.matchConstraints' },
|
|
750
|
+
{ key: 'rules', label: 'Rules', width: 'w-20', tooltip: 'CEL validation expressions' },
|
|
751
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
752
|
+
],
|
|
753
|
+
namespacedvalidatingpolicies: [
|
|
754
|
+
{ key: 'name', label: 'Name' },
|
|
755
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
756
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44', tooltip: 'Effective enforcement posture, accounting for whether admission evaluation is enabled' },
|
|
757
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
758
|
+
{ key: 'rules', label: 'Rules', width: 'w-20' },
|
|
759
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
760
|
+
],
|
|
761
|
+
imagevalidatingpolicies: [
|
|
762
|
+
{ key: 'name', label: 'Name' },
|
|
763
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
764
|
+
{ key: 'images', label: 'Images', width: 'min-w-40', tooltip: 'Image references this policy verifies' },
|
|
765
|
+
{ key: 'attestors', label: 'Attestors', width: 'w-20', tooltip: 'Trusted signing authorities' },
|
|
766
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
767
|
+
],
|
|
768
|
+
namespacedimagevalidatingpolicies: [
|
|
769
|
+
{ key: 'name', label: 'Name' },
|
|
770
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
771
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
772
|
+
{ key: 'images', label: 'Images', width: 'min-w-40' },
|
|
773
|
+
{ key: 'attestors', label: 'Attestors', width: 'w-20' },
|
|
774
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
775
|
+
],
|
|
776
|
+
mutatingpolicies: [
|
|
777
|
+
{ key: 'name', label: 'Name' },
|
|
778
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
779
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
780
|
+
{ key: 'rules', label: 'Mutations', width: 'w-20' },
|
|
781
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
782
|
+
],
|
|
783
|
+
namespacedmutatingpolicies: [
|
|
784
|
+
{ key: 'name', label: 'Name' },
|
|
785
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
786
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
787
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
788
|
+
{ key: 'rules', label: 'Mutations', width: 'w-20' },
|
|
789
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
790
|
+
],
|
|
791
|
+
generatingpolicies: [
|
|
792
|
+
{ key: 'name', label: 'Name' },
|
|
793
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
794
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
795
|
+
{ key: 'rules', label: 'Generates', width: 'w-20' },
|
|
796
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
797
|
+
],
|
|
798
|
+
namespacedgeneratingpolicies: [
|
|
799
|
+
{ key: 'name', label: 'Name' },
|
|
800
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
801
|
+
{ key: 'status', label: 'Enforcement', width: 'w-44' },
|
|
802
|
+
{ key: 'appliesTo', label: 'Applies To', width: 'min-w-40' },
|
|
803
|
+
{ key: 'rules', label: 'Generates', width: 'w-20' },
|
|
804
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
805
|
+
],
|
|
806
|
+
// The Deleting kinds are the only modern family whose headline column is the
|
|
807
|
+
// schedule rather than the enforcement posture, so they carry their own
|
|
808
|
+
// health signal. It is Last Run, NOT readiness: Kyverno 1.18.2 declares a
|
|
809
|
+
// READY printer column on these CRDs but never populates
|
|
810
|
+
// status.conditionStatus.ready, so `kubectl get deletingpolicies` prints it
|
|
811
|
+
// blank and a Ready column here would read "unknown" on every row forever.
|
|
812
|
+
// It also has nothing to catch — an uncompilable policy is rejected at
|
|
813
|
+
// admission and never exists. What does go wrong is a policy that exists and
|
|
814
|
+
// silently never fires, which lastExecutionTime shows.
|
|
815
|
+
//
|
|
816
|
+
// The other eight kinds deliberately have no readiness column either: their
|
|
817
|
+
// status cell already returns "Not Ready" IN PLACE OF the posture (see
|
|
818
|
+
// getModernKyvernoPolicyStatus), so a second one would be redundant.
|
|
819
|
+
deletingpolicies: [
|
|
820
|
+
{ key: 'name', label: 'Name' },
|
|
821
|
+
{ 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.' },
|
|
822
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-32', tooltip: 'Cron schedule on which matched resources are deleted' },
|
|
823
|
+
{ key: 'appliesTo', label: 'Deletes', width: 'min-w-40' },
|
|
824
|
+
{ key: 'rules', label: 'Conditions', width: 'w-28', tooltip: 'CEL conditions narrowing what is deleted; none means every matched resource' },
|
|
825
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
826
|
+
],
|
|
827
|
+
namespaceddeletingpolicies: [
|
|
828
|
+
{ key: 'name', label: 'Name' },
|
|
829
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
830
|
+
{ 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.' },
|
|
831
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-32' },
|
|
832
|
+
{ key: 'appliesTo', label: 'Deletes', width: 'min-w-40' },
|
|
833
|
+
{ key: 'rules', label: 'Conditions', width: 'w-28' },
|
|
834
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
835
|
+
],
|
|
836
|
+
policyexceptions: [
|
|
837
|
+
{ key: 'name', label: 'Name' },
|
|
838
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
839
|
+
{ key: 'status', label: 'Exempts', width: 'w-28', tooltip: 'How many policies this exception bypasses' },
|
|
840
|
+
{ key: 'policies', label: 'Policies', width: 'min-w-40' },
|
|
841
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
842
|
+
],
|
|
843
|
+
cleanuppolicies: [
|
|
844
|
+
{ key: 'name', label: 'Name' },
|
|
845
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
846
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-32' },
|
|
847
|
+
{ key: 'appliesTo', label: 'Deletes', width: 'min-w-40' },
|
|
848
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
849
|
+
],
|
|
850
|
+
clustercleanuppolicies: [
|
|
851
|
+
{ key: 'name', label: 'Name' },
|
|
852
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-32' },
|
|
853
|
+
{ key: 'appliesTo', label: 'Deletes', width: 'min-w-40' },
|
|
854
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
855
|
+
],
|
|
738
856
|
grpcroutes: [
|
|
739
857
|
{ key: 'name', label: 'Name' },
|
|
740
858
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
@@ -1100,39 +1218,64 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1100
1218
|
backups: [
|
|
1101
1219
|
{ key: 'name', label: 'Name' },
|
|
1102
1220
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1103
|
-
{ key: 'status', label: 'Status', width: 'w-
|
|
1221
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1104
1222
|
{ key: 'storageLocation', label: 'Storage', width: 'w-36' },
|
|
1105
1223
|
{ key: 'namespaces', label: 'Scope', width: 'w-24', tooltip: 'Included namespaces (* = all)' },
|
|
1106
1224
|
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
1107
1225
|
{ key: 'expiry', label: 'Expires', width: 'w-24' },
|
|
1108
|
-
{ key: 'errors', label: 'Errors', width: 'w-
|
|
1226
|
+
{ key: 'errors', label: 'Errors', width: 'w-28' },
|
|
1109
1227
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1110
1228
|
],
|
|
1111
|
-
restores
|
|
1229
|
+
// `restores` and `schedules` are keyed group-qualified (see
|
|
1230
|
+
// GROUP_QUALIFIED_COLUMN_KEYS): rancher/backup-restore-operator ships
|
|
1231
|
+
// restores.resources.cattle.io and several operators ship their own
|
|
1232
|
+
// `schedules` kind. Only velero.io resolves to these column sets;
|
|
1233
|
+
// everything else falls through to the generic columns.
|
|
1234
|
+
velerorestores: [
|
|
1112
1235
|
{ key: 'name', label: 'Name' },
|
|
1113
1236
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1114
|
-
{ key: 'status', label: 'Status', width: 'w-
|
|
1237
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1115
1238
|
{ key: 'backupName', label: 'Backup', width: 'w-40' },
|
|
1116
1239
|
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
1117
|
-
{ key: 'errors', label: 'Errors', width: 'w-
|
|
1240
|
+
{ key: 'errors', label: 'Errors', width: 'w-28' },
|
|
1118
1241
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1119
1242
|
],
|
|
1120
|
-
|
|
1243
|
+
veleroschedules: [
|
|
1121
1244
|
{ key: 'name', label: 'Name' },
|
|
1122
1245
|
{ 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: '
|
|
1246
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1247
|
+
{ key: 'schedule', label: 'Schedule', width: 'w-40' },
|
|
1248
|
+
{ key: 'lastBackup', label: 'Last Backup', width: 'w-32' },
|
|
1249
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1250
|
+
],
|
|
1251
|
+
// No status column on purpose: the VSL controller never populates
|
|
1252
|
+
// status.phase, so a badge would read "Unknown" on every row forever.
|
|
1253
|
+
volumesnapshotlocations: [
|
|
1254
|
+
{ key: 'name', label: 'Name' },
|
|
1255
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1256
|
+
{ key: 'provider', label: 'Provider', width: 'w-32' },
|
|
1257
|
+
{ key: 'config', label: 'Config' },
|
|
1258
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1259
|
+
],
|
|
1260
|
+
// repositoryType is kopia|restic, and it is what `kubectl get
|
|
1261
|
+
// backuprepositories` shows. It earns a column because restic is being retired
|
|
1262
|
+
// (no new backups since v1.17, restore dropped in v1.19), which makes it a
|
|
1263
|
+
// migration liability worth spotting by scanning rather than by opening each
|
|
1264
|
+
// repository one at a time.
|
|
1265
|
+
backuprepositories: [
|
|
1266
|
+
{ key: 'name', label: 'Name' },
|
|
1267
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1268
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1269
|
+
{ key: 'repositoryType', label: 'Type', width: 'w-24' },
|
|
1127
1270
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1128
1271
|
],
|
|
1129
1272
|
backupstoragelocations: [
|
|
1130
1273
|
{ key: 'name', label: 'Name' },
|
|
1131
1274
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1132
|
-
{ key: 'status', label: 'Status', width: 'w-
|
|
1275
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1133
1276
|
{ key: 'provider', label: 'Provider', width: 'w-24' },
|
|
1134
1277
|
{ key: 'bucket', label: 'Bucket', width: 'w-40' },
|
|
1135
|
-
{ key: 'default', label: 'Default', width: 'w-
|
|
1278
|
+
{ key: 'default', label: 'Default', width: 'w-24' },
|
|
1136
1279
|
{ key: 'lastValidation', label: 'Validated', width: 'w-28' },
|
|
1137
1280
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1138
1281
|
],
|
|
@@ -1142,11 +1285,34 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1142
1285
|
cnpgclusters: [
|
|
1143
1286
|
{ key: 'name', label: 'Name' },
|
|
1144
1287
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1145
|
-
|
|
1288
|
+
// w-28 leaves the "Status" label 0.23px short once the sort AND filter
|
|
1289
|
+
// affordances are both present, which renders "STAT…" — a sub-pixel miss
|
|
1290
|
+
// costs two characters because the ellipsis needs its own room. The filter
|
|
1291
|
+
// icon appears as soon as a column holds more than one distinct value, so
|
|
1292
|
+
// this is latent on any status column, not specific to the current data.
|
|
1293
|
+
// w-44 fits "WAL Archiving Failing" (120.2px against a 127px label budget).
|
|
1294
|
+
// CNPG's own phases are mapped to short display states — see
|
|
1295
|
+
// getCNPGClusterDisplayState; a 315px sentence fits no column at all.
|
|
1296
|
+
{ key: 'status', label: 'Status', width: 'w-44' },
|
|
1146
1297
|
{ key: 'instances', label: 'Instances', width: 'w-28', tooltip: 'Ready/Total' },
|
|
1147
1298
|
{ key: 'primary', label: 'Primary', width: 'w-36' },
|
|
1148
1299
|
{ key: 'image', label: 'Image', width: 'w-28' },
|
|
1149
|
-
|
|
1300
|
+
// No Storage column: it rendered spec.storage.size, the configured REQUEST.
|
|
1301
|
+
// The useful number is actual usage (kubectl cnpg status shows "Size: 158M")
|
|
1302
|
+
// and that isn't in the CR. A plausible-but-wrong-meaning number is worse
|
|
1303
|
+
// than an absent one — the reader can't tell which meaning they're getting.
|
|
1304
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1305
|
+
],
|
|
1306
|
+
cnpgbackups: [
|
|
1307
|
+
{ key: 'name', label: 'Name' },
|
|
1308
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1309
|
+
{ key: 'status', label: 'Status', width: 'w-44' },
|
|
1310
|
+
{ key: 'cluster', label: 'Cluster', width: 'w-36' },
|
|
1311
|
+
// `barmanObjectStore` is the longest method and the field that decides how
|
|
1312
|
+
// the rest of the row reads; at w-36 it was permanently ellipsised.
|
|
1313
|
+
{ key: 'method', label: 'Method', width: 'w-40' },
|
|
1314
|
+
{ key: 'started', label: 'Started', width: 'w-24' },
|
|
1315
|
+
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
1150
1316
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1151
1317
|
],
|
|
1152
1318
|
scheduledbackups: [
|
|
@@ -1162,11 +1328,19 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1162
1328
|
poolers: [
|
|
1163
1329
|
{ key: 'name', label: 'Name' },
|
|
1164
1330
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1165
|
-
|
|
1331
|
+
// "Not Scheduled" (83.5px) is kept rather than shortened to "Unscheduled":
|
|
1332
|
+
// ScheduledBackup also renders a literal "Scheduled" badge in this same
|
|
1333
|
+
// column, so a standalone adjective would read as "has no cron" — a claim
|
|
1334
|
+
// about a field Poolers don't have. w-36 buys the words.
|
|
1335
|
+
{ key: 'status', label: 'Status', width: 'w-36' },
|
|
1166
1336
|
{ key: 'cluster', label: 'Cluster', width: 'w-36' },
|
|
1167
|
-
|
|
1337
|
+
// Sized for the HEADER, not the value: `rw`/`ro` need 27px, but "Type" plus
|
|
1338
|
+
// the sort affordance needs more than w-16 leaves, and the label rendered
|
|
1339
|
+
// as "T…".
|
|
1340
|
+
{ key: 'type', label: 'Type', width: 'w-24' },
|
|
1168
1341
|
{ key: 'poolMode', label: 'Pool Mode', width: 'w-32' },
|
|
1169
|
-
|
|
1342
|
+
// status.instances counts pods trying to be scheduled, not ready ones.
|
|
1343
|
+
{ key: 'instances', label: 'Instances', width: 'w-28', tooltip: 'Scheduled/Total' },
|
|
1170
1344
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1171
1345
|
],
|
|
1172
1346
|
// ============================================================================
|
|
@@ -1694,6 +1868,9 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1694
1868
|
// Map (plural, group) → KNOWN_COLUMNS key for kinds that collide with core K8s
|
|
1695
1869
|
const GROUP_QUALIFIED_COLUMN_KEYS: Record<string, Record<string, string>> = {
|
|
1696
1870
|
clusters: { 'postgresql.cnpg.io': 'cnpgclusters', 'cluster.x-k8s.io': 'capiclusters' },
|
|
1871
|
+
// Velero owns the unqualified `backups` column set; CNPG Backups carry a
|
|
1872
|
+
// completely different shape (cluster + method, no storage location/expiry).
|
|
1873
|
+
backups: { 'postgresql.cnpg.io': 'cnpgbackups' },
|
|
1697
1874
|
clusterpolicies: { 'nvidia.com': 'nvidiaclusterpolicies' },
|
|
1698
1875
|
services: { 'serving.knative.dev': 'knativeservices' },
|
|
1699
1876
|
configurations: { 'serving.knative.dev': 'knativeconfigurations' },
|
|
@@ -1701,6 +1878,8 @@ const GROUP_QUALIFIED_COLUMN_KEYS: Record<string, Record<string, string>> = {
|
|
|
1701
1878
|
routes: { 'serving.knative.dev': 'knativeroutes' },
|
|
1702
1879
|
ingresses: { 'networking.internal.knative.dev': 'knativeingresses' },
|
|
1703
1880
|
certificates: { 'networking.internal.knative.dev': 'knativecertificates' },
|
|
1881
|
+
restores: { 'velero.io': 'velerorestores' },
|
|
1882
|
+
schedules: { 'velero.io': 'veleroschedules' },
|
|
1704
1883
|
}
|
|
1705
1884
|
|
|
1706
1885
|
// Normalize a kind name to its plural API form used in KNOWN_COLUMNS keys.
|
|
@@ -5455,6 +5634,29 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5455
5634
|
|
|
5456
5635
|
// Kind-specific columns (normalize CRD singular names like 'ScaledObject' → 'scaledobjects')
|
|
5457
5636
|
const kindLower = normalizeKindToPlural(kind, group)
|
|
5637
|
+
|
|
5638
|
+
// Kyverno cells are group-gated ahead of the switch so a non-matching CR
|
|
5639
|
+
// falls through to the switch's default (GenericCell) instead of being
|
|
5640
|
+
// described in Kyverno's vocabulary. These plurals are generic enough that
|
|
5641
|
+
// another vendor could ship them, and `policyexceptions` is served by BOTH
|
|
5642
|
+
// Kyverno API families with different spec shapes. The stakes are higher
|
|
5643
|
+
// here than in the drawer: an absent `validationActions` legitimately means
|
|
5644
|
+
// Deny for a real Kyverno policy, so an ungated foreign CR would render a
|
|
5645
|
+
// red "Deny" badge purely because it lacks a field it never had.
|
|
5646
|
+
if (KYVERNO_MODERN_PLURALS.has(kindLower) && isModernKyvernoPolicy(resource)) {
|
|
5647
|
+
return <KyvernoModernPolicyCell resource={resource} column={column} />
|
|
5648
|
+
}
|
|
5649
|
+
if (kindLower === 'policyexceptions' && isAnyKyvernoPolicyException(resource)) {
|
|
5650
|
+
return <KyvernoPolicyExceptionCell resource={resource} column={column} />
|
|
5651
|
+
}
|
|
5652
|
+
if (
|
|
5653
|
+
(kindLower === 'cleanuppolicies' || kindLower === 'clustercleanuppolicies') &&
|
|
5654
|
+
typeof resource?.apiVersion === 'string' &&
|
|
5655
|
+
resource.apiVersion.startsWith('kyverno.io/')
|
|
5656
|
+
) {
|
|
5657
|
+
return <KyvernoCleanupPolicyCell resource={resource} column={column} />
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5458
5660
|
switch (kindLower) {
|
|
5459
5661
|
case 'pods':
|
|
5460
5662
|
return <PodCell resource={resource} column={column} />
|
|
@@ -5644,26 +5846,53 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5644
5846
|
case 'clustersecretstores':
|
|
5645
5847
|
return <ClusterSecretStoreCell resource={resource} column={column} />
|
|
5646
5848
|
// Velero
|
|
5849
|
+
case 'cnpgbackups':
|
|
5850
|
+
return <CNPGBackupCell resource={resource} column={column} />
|
|
5647
5851
|
case 'backups':
|
|
5648
|
-
//
|
|
5649
|
-
|
|
5852
|
+
// Reached only when the group is unknown to normalizeKindToPlural. Both
|
|
5853
|
+
// engines are matched positively so a third `backups` CRD renders generic
|
|
5854
|
+
// rather than inheriting whichever branch happened to be the fallback.
|
|
5855
|
+
if (isApiGroup(resource.apiVersion, CNPG_GROUP)) {
|
|
5650
5856
|
return <CNPGBackupCell resource={resource} column={column} />
|
|
5651
5857
|
}
|
|
5652
|
-
|
|
5653
|
-
|
|
5858
|
+
if (isApiGroup(resource.apiVersion, 'velero.io')) {
|
|
5859
|
+
return <BackupCell resource={resource} column={column} />
|
|
5860
|
+
}
|
|
5861
|
+
return <GenericCell resource={resource} column={column} />
|
|
5862
|
+
case 'velerorestores':
|
|
5654
5863
|
return <RestoreCell resource={resource} column={column} />
|
|
5655
|
-
case '
|
|
5864
|
+
case 'veleroschedules':
|
|
5656
5865
|
return <ScheduleCell resource={resource} column={column} />
|
|
5657
5866
|
case 'backupstoragelocations':
|
|
5867
|
+
if (!isVeleroResource(resource)) return <GenericCell resource={resource} column={column} />
|
|
5658
5868
|
return <BackupStorageLocationCell resource={resource} column={column} />
|
|
5869
|
+
case 'volumesnapshotlocations':
|
|
5870
|
+
if (!isVeleroResource(resource)) return <GenericCell resource={resource} column={column} />
|
|
5871
|
+
return <VolumeSnapshotLocationCell resource={resource} column={column} />
|
|
5872
|
+
case 'backuprepositories':
|
|
5873
|
+
if (!isVeleroResource(resource)) return <GenericCell resource={resource} column={column} />
|
|
5874
|
+
return <BackupRepositoryCell resource={resource} column={column} />
|
|
5659
5875
|
// CloudNativePG
|
|
5660
5876
|
case 'cnpgclusters':
|
|
5661
|
-
case 'clusters':
|
|
5662
5877
|
return <CNPGClusterCell resource={resource} column={column} />
|
|
5878
|
+
case 'clusters':
|
|
5879
|
+
// Positive guard: `clusters` is one of the most collided CRD plurals
|
|
5880
|
+
// (CNPG, CAPI, KubeBlocks, Redis/Valkey operators). Anything else gets
|
|
5881
|
+
// the generic cell instead of a fabricated Postgres status.
|
|
5882
|
+
if (isApiGroup(resource.apiVersion, CNPG_GROUP)) {
|
|
5883
|
+
return <CNPGClusterCell resource={resource} column={column} />
|
|
5884
|
+
}
|
|
5885
|
+
return <GenericCell resource={resource} column={column} />
|
|
5663
5886
|
case 'scheduledbackups':
|
|
5664
|
-
|
|
5887
|
+
if (isApiGroup(resource.apiVersion, CNPG_GROUP)) {
|
|
5888
|
+
return <CNPGScheduledBackupCell resource={resource} column={column} />
|
|
5889
|
+
}
|
|
5890
|
+
return <GenericCell resource={resource} column={column} />
|
|
5665
5891
|
case 'poolers':
|
|
5666
|
-
|
|
5892
|
+
if (isApiGroup(resource.apiVersion, CNPG_GROUP)) {
|
|
5893
|
+
return <CNPGPoolerCell resource={resource} column={column} />
|
|
5894
|
+
}
|
|
5895
|
+
return <GenericCell resource={resource} column={column} />
|
|
5667
5896
|
// Istio Service Mesh
|
|
5668
5897
|
case 'virtualservices':
|
|
5669
5898
|
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
|
+
})
|