@skyhook-io/k8s-ui 1.16.3 → 1.17.1

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.
Files changed (309) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  4. package/src/components/applications/ApplicationDetail.tsx +17 -15
  5. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  6. package/src/components/charts/AreaChart.test.tsx +25 -0
  7. package/src/components/charts/AreaChart.tsx +141 -12
  8. package/src/components/charts/index.ts +1 -0
  9. package/src/components/checks/ChecksView.tsx +18 -1
  10. package/src/components/checks/types.ts +2 -13
  11. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  12. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  13. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  14. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  15. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  16. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  17. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  18. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  19. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  20. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  21. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  22. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  23. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  24. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  25. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  26. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  27. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  28. package/src/components/cnpg/backupRuns.test.ts +17 -0
  29. package/src/components/cnpg/backupRuns.ts +19 -0
  30. package/src/components/cnpg/connect.test.ts +124 -0
  31. package/src/components/cnpg/connect.ts +155 -0
  32. package/src/components/cnpg/databaseRole.test.ts +38 -0
  33. package/src/components/cnpg/databaseRole.ts +80 -0
  34. package/src/components/cnpg/ha.test.ts +446 -0
  35. package/src/components/cnpg/ha.ts +621 -0
  36. package/src/components/cnpg/index.ts +18 -0
  37. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  38. package/src/components/cnpg/logicalReplication.ts +252 -0
  39. package/src/components/cnpg/pooler.test.ts +84 -0
  40. package/src/components/cnpg/pooler.ts +225 -0
  41. package/src/components/cnpg/primitives.tsx +21 -117
  42. package/src/components/cnpg/problems.ts +12 -0
  43. package/src/components/cnpg/relations.test.ts +63 -13
  44. package/src/components/cnpg/relations.ts +61 -52
  45. package/src/components/cnpg/schedule.ts +35 -0
  46. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  47. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  48. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  49. package/src/components/cnpg/workspace.test.ts +380 -9
  50. package/src/components/cnpg/workspace.ts +1137 -136
  51. package/src/components/dock/DockContext.test.tsx +32 -0
  52. package/src/components/dock/DockContext.tsx +18 -4
  53. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  54. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  55. package/src/components/dock/TerminalTab.tsx +20 -8
  56. package/src/components/facts/certainty.tsx +36 -0
  57. package/src/components/facts/facts.test.tsx +24 -0
  58. package/src/components/facts/facts.tsx +62 -0
  59. package/src/components/facts/index.ts +6 -0
  60. package/src/components/facts/managed-by.test.tsx +17 -0
  61. package/src/components/facts/managed-by.tsx +36 -0
  62. package/src/components/facts/public-exports.test.tsx +35 -0
  63. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  64. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  65. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  66. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  67. package/src/components/gitops/detail-helpers.ts +1 -1
  68. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  69. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  70. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  71. package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
  72. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  73. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
  74. package/src/components/gitops/tree/layout.test.ts +111 -0
  75. package/src/components/gitops/tree/layout.ts +128 -0
  76. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  77. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  78. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  79. package/src/components/issues/IssuesView.tsx +40 -5
  80. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  81. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  82. package/src/components/issues/index.ts +3 -1
  83. package/src/components/issues/issue-timing.ts +14 -3
  84. package/src/components/issues/issues.test.ts +50 -2
  85. package/src/components/issues/severity.ts +21 -0
  86. package/src/components/issues/types.ts +23 -0
  87. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  88. package/src/components/logs/LogCore.tsx +25 -17
  89. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  90. package/src/components/logs/LogsViewer.test.tsx +291 -0
  91. package/src/components/logs/LogsViewer.tsx +72 -11
  92. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  93. package/src/components/logs/StructuredLogLine.tsx +22 -30
  94. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  95. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  96. package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
  97. package/src/components/logs/useLogBuffer.test.ts +8 -0
  98. package/src/components/logs/useLogStream.ts +28 -1
  99. package/src/components/problems/index.ts +1 -0
  100. package/src/components/problems/problems.test.tsx +63 -0
  101. package/src/components/problems/problems.tsx +165 -0
  102. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  103. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  104. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  105. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  106. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  107. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  108. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  109. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  110. package/src/components/resources/ResourcesView.tsx +309 -117
  111. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  112. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  113. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  114. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  115. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
  116. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  117. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  118. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  119. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  120. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  121. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  122. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  124. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
  125. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  127. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
  128. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  130. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  131. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  132. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  133. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  134. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  135. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  136. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  137. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  138. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
  139. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
  140. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  141. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  142. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  143. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  144. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  145. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  146. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  147. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  148. package/src/components/resources/renderers/HPARenderer.tsx +57 -17
  149. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  150. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  151. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  152. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  153. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  154. package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
  155. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  156. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  157. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  158. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  159. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  160. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
  161. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  163. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  164. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  165. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  166. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  167. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  170. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  171. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  172. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  173. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  174. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  175. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  176. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  177. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  178. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  179. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  180. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  181. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  182. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  183. package/src/components/resources/renderers/PodRenderer.tsx +62 -36
  184. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  185. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  186. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  187. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  188. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  189. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  190. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  191. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
  192. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  193. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  194. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  195. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  196. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  197. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  198. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  199. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  200. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  201. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  202. package/src/components/resources/resource-utils-capi.ts +1 -1
  203. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  204. package/src/components/resources/resource-utils-cnpg.ts +51 -24
  205. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  206. package/src/components/resources/resource-utils-contour.ts +16 -0
  207. package/src/components/resources/resource-utils-istio.ts +12 -8
  208. package/src/components/resources/resource-utils-keda.ts +1 -7
  209. package/src/components/resources/resource-utils-knative.ts +18 -0
  210. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  211. package/src/components/resources/resource-utils-velero.ts +0 -12
  212. package/src/components/resources/resource-utils.ts +99 -39
  213. package/src/components/resources/rules-column-width.test.ts +1 -1
  214. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  215. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  216. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  217. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  218. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  219. package/src/components/shared/DetailShell.test.tsx +80 -0
  220. package/src/components/shared/DetailShell.tsx +85 -10
  221. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  222. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  223. package/src/components/shared/EditableYamlView.tsx +1 -1
  224. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  225. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  226. package/src/components/shared/ResourceActionsBar.tsx +20 -24
  227. package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
  228. package/src/components/shared/SetImageDialog.test.ts +75 -6
  229. package/src/components/shared/SetImageDialog.tsx +92 -84
  230. package/src/components/shared/index.ts +4 -1
  231. package/src/components/timeline/TimelineList.tsx +2 -15
  232. package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
  233. package/src/components/timeline/timeline-filters.ts +1 -0
  234. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  235. package/src/components/topology/K8sResourceNode.tsx +44 -2
  236. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  237. package/src/components/topology/TopologyGraph.tsx +8 -74
  238. package/src/components/topology/TopologySearch.tsx +1 -0
  239. package/src/components/topology/internet-groups.test.ts +35 -0
  240. package/src/components/topology/internet-groups.ts +55 -0
  241. package/src/components/topology/layout.ts +1 -1
  242. package/src/components/ui/Badge.tsx +1 -0
  243. package/src/components/ui/FoldSection.tsx +68 -0
  244. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  245. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  246. package/src/components/ui/RefLink.tsx +20 -0
  247. package/src/components/ui/RowActionMenu.tsx +8 -3
  248. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  249. package/src/components/ui/ScheduleValue.tsx +3 -2
  250. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  251. package/src/components/ui/drawer-components.test.tsx +175 -1
  252. package/src/components/ui/drawer-components.tsx +103 -36
  253. package/src/components/ui/index.ts +3 -1
  254. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  255. package/src/components/ui/monacoRuntime.ts +14 -19
  256. package/src/components/ui/status-tone.test.ts +10 -1
  257. package/src/components/ui/status-tone.tsx +29 -0
  258. package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
  259. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  260. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  261. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  262. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  263. package/src/components/workload/WorkloadView.tsx +297 -104
  264. package/src/index.ts +14 -1
  265. package/src/monaco-deep.d.ts +19 -12
  266. package/src/theme/components.css +13 -0
  267. package/src/types/capacity.ts +21 -2
  268. package/src/types/core.ts +60 -2
  269. package/src/types/gitops-flux-status.test.ts +32 -1
  270. package/src/types/gitops-insights.ts +19 -2
  271. package/src/types/gitops-tree.ts +10 -0
  272. package/src/types/gitops.ts +26 -6
  273. package/src/utils/api-resources.ts +7 -0
  274. package/src/utils/application-history.ts +1 -0
  275. package/src/utils/cnpg-backup.test.ts +26 -0
  276. package/src/utils/cnpg-backup.ts +70 -0
  277. package/src/utils/format.test.ts +49 -2
  278. package/src/utils/format.ts +9 -1
  279. package/src/utils/gateway-references.test.ts +17 -0
  280. package/src/utils/gateway-references.ts +29 -0
  281. package/src/utils/gitops-owner.ts +3 -2
  282. package/src/utils/gitops-write-guard.test.ts +401 -0
  283. package/src/utils/gitops-write-guard.ts +509 -0
  284. package/src/utils/grant.test.ts +19 -0
  285. package/src/utils/grant.ts +35 -0
  286. package/src/utils/index.ts +6 -0
  287. package/src/utils/ingress-class-references.ts +22 -0
  288. package/src/utils/k8s-errors.ts +7 -7
  289. package/src/utils/log-format.test.ts +41 -0
  290. package/src/utils/log-format.ts +71 -15
  291. package/src/utils/log-level.ts +12 -8
  292. package/src/utils/navigation.test.ts +70 -0
  293. package/src/utils/navigation.ts +91 -8
  294. package/src/utils/node-lifecycle.test.ts +36 -0
  295. package/src/utils/node-lifecycle.ts +77 -0
  296. package/src/utils/object-reference-navigation.test.ts +43 -0
  297. package/src/utils/resource-hierarchy.test.ts +69 -0
  298. package/src/utils/resource-hierarchy.ts +41 -7
  299. package/src/utils/resource-icons.ts +2 -0
  300. package/src/utils/resource-refs.test.ts +23 -0
  301. package/src/utils/resource-refs.ts +30 -0
  302. package/src/utils/skeleton-yaml.test.ts +21 -0
  303. package/src/utils/skeleton-yaml.ts +16 -0
  304. package/src/utils/url-path.test.ts +35 -0
  305. package/src/utils/url-path.ts +21 -0
  306. package/src/utils/validators.test.ts +4 -13
  307. package/src/utils/validators.ts +5 -19
  308. package/src/utils/view-transition.test.ts +35 -1
  309. package/src/utils/view-transition.ts +16 -1
@@ -0,0 +1,509 @@
1
+ // Classifies a direct write to a live object against the GitOps (or Helm)
2
+ // source that manages it: will the owner put the old value back, might it, or
3
+ // does a policy exclude the field?
4
+ //
5
+ // Evidence comes from the server (POST /api/gitops/write-evidence), because
6
+ // the facts that matter — the last client-side apply payload and
7
+ // managedFields — are stripped from every cached object Radar serves. Neither
8
+ // is proof of what Git declares today: last-applied is a PREVIOUS apply, and a
9
+ // field missing from both can still be in the source. So absence of evidence
10
+ // is "unknown" (may be overwritten), never "safe".
11
+
12
+ import type { GitOpsOwnerRef, GitOpsOwnerTool } from './gitops-owner'
13
+
14
+ export type GitOpsWriteScope = 'status' | 'metadata' | 'spec' | 'create-child' | 'delete'
15
+
16
+ export interface GitOpsWrite {
17
+ scope: GitOpsWriteScope
18
+ /** Field paths, e.g. `spec.suspend`, `metadata.annotations["cnpg.io/hibernation"]`,
19
+ * `spec.template.spec.containers[name=app].image`, `spec.template.spec.containers[*].image`.
20
+ * Omitted = the fields aren't known. */
21
+ paths?: string[]
22
+ description?: string
23
+ }
24
+
25
+ export interface GitOpsWriteTarget {
26
+ /** Singular Kind, e.g. `Deployment`. */
27
+ kind: string
28
+ /** API group; `''` for core. */
29
+ group: string
30
+ namespace: string
31
+ name: string
32
+ }
33
+
34
+ export type GitOpsWriteLevel = 'none' | 'info' | 'may-revert' | 'will-revert'
35
+
36
+ export interface GitOpsSyncPolicy {
37
+ tool: GitOpsOwnerTool
38
+ auto: boolean | null
39
+ selfHeal: boolean | null
40
+ prune: boolean | null
41
+ suspended: boolean | null
42
+ interval?: string
43
+ }
44
+
45
+ export interface GitOpsWriteHelmRelease {
46
+ namespace: string
47
+ name: string
48
+ }
49
+
50
+ export interface GitOpsWriteOperatorOwner {
51
+ kind: string
52
+ name: string
53
+ }
54
+
55
+ // --- Wire types of POST /api/gitops/write-evidence -------------------------
56
+
57
+ export interface GitOpsFieldOwnerEvidence {
58
+ manager: string
59
+ operation: string
60
+ subresource?: string
61
+ tool?: 'argocd' | 'fluxcd' | 'helm'
62
+ approximate?: boolean
63
+ }
64
+
65
+ export interface GitOpsPathEvidence {
66
+ path: string
67
+ error?: string
68
+ lastApplied: 'present' | 'absent' | 'no-annotation'
69
+ ownedBy: GitOpsFieldOwnerEvidence[]
70
+ /** A manager of the owner's controller owns the field. */
71
+ ownedByGitOps: boolean
72
+ approximate?: boolean
73
+ /** The path has a wildcard, and the source declares the field on some of
74
+ * the entries it matches (one container's image but not another's). */
75
+ partial?: boolean
76
+ /** The wildcard matches nothing in the live object (no init containers),
77
+ * so the write can't touch this path. */
78
+ empty?: boolean
79
+ /** `effective`: the owner is not expected to overwrite it. `comparison-only`: Argo
80
+ * ignoreDifferences without RespectIgnoreDifferences. `unevaluated`: a
81
+ * matching rule uses jqPathExpressions, which Radar doesn't evaluate, or
82
+ * covers only some of the entries a wildcard path matches. */
83
+ ignored?: 'effective' | 'comparison-only' | 'unevaluated'
84
+ ignoredBy?: string
85
+ }
86
+
87
+ export interface GitOpsWritePolicyEvidence {
88
+ tool: GitOpsOwnerTool
89
+ auto: boolean | null
90
+ selfHeal: boolean | null
91
+ prune: boolean | null
92
+ suspended: boolean | null
93
+ interval?: string
94
+ respectIgnoreDifferences?: boolean
95
+ serverSideApply?: boolean
96
+ replace?: boolean
97
+ /** HelmRelease `spec.driftDetection.mode`. */
98
+ driftDetection?: string
99
+ objectReconcile?: 'ignore' | 'if-not-present'
100
+ }
101
+
102
+ export interface GitOpsWriteEvidence {
103
+ uid: string
104
+ resourceVersion: string
105
+ owner: { kind: string; group?: string; namespace: string; name: string } | null
106
+ policy: GitOpsWritePolicyEvidence | null
107
+ policyError?: string
108
+ controllerOwner?: { apiVersion: string; kind: string; name: string }
109
+ /** The Argo CD Application's status lists the object among its resources. */
110
+ ownerTracksTarget?: boolean
111
+ paths: GitOpsPathEvidence[]
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+
116
+ export interface GitOpsWriteGuardInput {
117
+ target: GitOpsWriteTarget
118
+ owner: GitOpsOwnerRef | null
119
+ /** Native Helm release that manages the object when no GitOps tool does. */
120
+ helmRelease?: GitOpsWriteHelmRelease | null
121
+ /** A non-GitOps controller (e.g. a CNPG Cluster owning a Pod). Defaults to
122
+ * the evidence's controllerOwner. Only affects `delete`. */
123
+ operatorOwner?: GitOpsWriteOperatorOwner | null
124
+ /** Ownership or evidence is still loading. */
125
+ ownerPending?: boolean
126
+ /** The host could not determine ownership: one sentence saying why, shown as the reason. */
127
+ ownershipError?: string | null
128
+ /** The owner was matched by name only (an Argo CD tracking label without a
129
+ * namespace), so its policy can't waive an acknowledgment. */
130
+ ownerMatchedByName?: boolean
131
+ evidence?: GitOpsWriteEvidence | null
132
+ /** The evidence request failed. */
133
+ evidenceError?: string | null
134
+ writes: GitOpsWrite[]
135
+ }
136
+
137
+ export interface GitOpsWriteAssessment {
138
+ write: GitOpsWrite
139
+ level: GitOpsWriteLevel
140
+ reason: string
141
+ }
142
+
143
+ export interface GitOpsWriteGuard {
144
+ owner: GitOpsOwnerRef | null
145
+ helmRelease: GitOpsWriteHelmRelease | null
146
+ pending: boolean
147
+ level: GitOpsWriteLevel
148
+ perWrite: GitOpsWriteAssessment[]
149
+ summary: string
150
+ syncPolicy: GitOpsSyncPolicy | null
151
+ requiresAck: boolean
152
+ ownershipError: string | null
153
+ }
154
+
155
+ const LEVEL_RANK: Record<GitOpsWriteLevel, number> = {
156
+ none: 0,
157
+ info: 1,
158
+ 'may-revert': 2,
159
+ 'will-revert': 3,
160
+ }
161
+
162
+ function maxLevel(a: GitOpsWriteLevel, b: GitOpsWriteLevel): GitOpsWriteLevel {
163
+ return LEVEL_RANK[b] > LEVEL_RANK[a] ? b : a
164
+ }
165
+
166
+ export function gitOpsToolLabel(tool: GitOpsOwnerTool | 'helm'): string {
167
+ if (tool === 'argocd') return 'Argo CD'
168
+ if (tool === 'fluxcd') return 'Flux'
169
+ return 'Helm'
170
+ }
171
+
172
+ const OWNER_KIND_LABEL: Record<GitOpsOwnerRef['kind'], string> = {
173
+ applications: 'Application',
174
+ kustomizations: 'Kustomization',
175
+ helmreleases: 'HelmRelease',
176
+ }
177
+
178
+ /** `Argo CD Application argocd/api` */
179
+ export function describeGitOpsOwner(owner: GitOpsOwnerRef): string {
180
+ const ref = owner.namespace ? `${owner.namespace}/${owner.name}` : owner.name
181
+ return `${gitOpsToolLabel(owner.tool)} ${OWNER_KIND_LABEL[owner.kind]} ${ref}`
182
+ }
183
+
184
+ /** `the Application`: the owner as reasons name it, after a headline that already named it in full. */
185
+ function ownerKindRef(owner: GitOpsOwnerRef): string {
186
+ return `the ${OWNER_KIND_LABEL[owner.kind]}`
187
+ }
188
+
189
+ function capitalize(text: string): string {
190
+ return text.charAt(0).toUpperCase() + text.slice(1)
191
+ }
192
+
193
+ /** The tool named in acknowledgments: `Argo CD`, `Flux`, `Helm`. */
194
+ export function guardToolLabel(guard: Pick<GitOpsWriteGuard, 'owner' | 'helmRelease'>): string {
195
+ if (guard.owner) return gitOpsToolLabel(guard.owner.tool)
196
+ if (guard.helmRelease) return 'Helm'
197
+ return 'a GitOps tool'
198
+ }
199
+
200
+ /** Identifies what an acknowledgment was given for; it changes when the
201
+ * verdict, owner, reasons or the fields being written do. */
202
+ export function gitOpsWriteGuardKey(guard: GitOpsWriteGuard | undefined): string {
203
+ if (!guard || guard.pending) return ''
204
+ const owner = guard.owner ? `${guard.owner.kind}/${guard.owner.namespace ?? ''}/${guard.owner.name}` : guard.helmRelease ? `helm/${guard.helmRelease.namespace}/${guard.helmRelease.name}` : ''
205
+ return [guard.level, owner, guard.ownershipError ?? '', ...guard.perWrite.map((w) => `${w.write.scope}:${(w.write.paths ?? []).join(',')}:${w.level}:${w.reason}`)].join('|')
206
+ }
207
+
208
+ export function canConfirmGitOpsWrite(guard: GitOpsWriteGuard | undefined, acked: boolean): boolean {
209
+ if (!guard || guard.pending) return false
210
+ return !guard.requiresAck || acked
211
+ }
212
+
213
+ export function gitOpsSyncPolicy(policy: GitOpsWritePolicyEvidence | null | undefined): GitOpsSyncPolicy | null {
214
+ if (!policy) return null
215
+ return {
216
+ tool: policy.tool,
217
+ auto: policy.auto,
218
+ selfHeal: policy.selfHeal,
219
+ prune: policy.prune,
220
+ suspended: policy.suspended,
221
+ ...(policy.interval ? { interval: policy.interval } : {}),
222
+ }
223
+ }
224
+
225
+ /** The field paths to request evidence for. */
226
+ export function gitOpsWriteEvidencePaths(writes: GitOpsWrite[]): string[] {
227
+ const paths = new Set<string>()
228
+ for (const write of writes) {
229
+ if (write.scope === 'metadata' || write.scope === 'spec') {
230
+ for (const path of write.paths ?? []) paths.add(path)
231
+ }
232
+ }
233
+ return [...paths]
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Classification
238
+ // ---------------------------------------------------------------------------
239
+
240
+ type Verdict = { level: GitOpsWriteLevel; reason: string }
241
+
242
+ interface Context {
243
+ input: GitOpsWriteGuardInput
244
+ owner: GitOpsOwnerRef
245
+ policy: GitOpsWritePolicyEvidence | null
246
+ policyUnknown: string
247
+ }
248
+
249
+ function nextSync(ctx: Context): string {
250
+ if (ctx.owner.tool === 'argocd') return 'the next sync'
251
+ const every = ctx.policy?.interval ? ` (every ${ctx.policy.interval})` : ''
252
+ return `the next reconcile${every}`
253
+ }
254
+
255
+ // What happens to a field the source sets, under the owner's policy.
256
+ function declaredOutcome(ctx: Context): Verdict {
257
+ const { owner, policy } = ctx
258
+ if (!policy) {
259
+ return {
260
+ level: 'may-revert',
261
+ reason: `Radar couldn't read ${ownerKindRef(owner)}'s sync policy (${ctx.policyUnknown}); the next sync will overwrite it.`,
262
+ }
263
+ }
264
+ if (policy.suspended) {
265
+ return { level: 'may-revert', reason: `${capitalize(ownerKindRef(owner))} is suspended; resuming it will overwrite this change.` }
266
+ }
267
+ if (policy.selfHeal) {
268
+ return { level: 'will-revert', reason: `It will be overwritten at ${nextSync(ctx)}.` }
269
+ }
270
+ if (owner.kind === 'helmreleases') {
271
+ return {
272
+ level: 'may-revert',
273
+ reason: `Drift detection is ${policy.driftDetection || 'disabled'}, so it stays until the next Helm upgrade by Flux overwrites it.`,
274
+ }
275
+ }
276
+ if (owner.tool === 'argocd' && policy.auto) {
277
+ return { level: 'may-revert', reason: 'Self-heal is off; the next sync will overwrite it.' }
278
+ }
279
+ return { level: 'may-revert', reason: 'Auto-sync is off; the next sync will overwrite it.' }
280
+ }
281
+
282
+ function evidenceNote(ev: GitOpsPathEvidence, owner: GitOpsOwnerRef): string {
283
+ const manager = ev.ownedBy.find((o) => o.tool === owner.tool && !o.subresource)?.manager
284
+ if (ev.ownedByGitOps && manager) return `${manager} ${ev.approximate ? 'manages' : 'owns'} this field`
285
+ return 'the last applied configuration sets this field'
286
+ }
287
+
288
+ function classifyPath(ctx: Context, path: string | null): Verdict {
289
+ const { owner, policy } = ctx
290
+ const tool = gitOpsToolLabel(owner.tool)
291
+ const ev = path ? ctx.input.evidence?.paths.find((p) => p.path === path) : undefined
292
+ if (ev?.empty) return { level: 'none', reason: '' }
293
+
294
+ // Helm's drift exemptions only stop drift correction: the next upgrade's
295
+ // three-way merge still resets a field the chart renders.
296
+ const helm = owner.kind === 'helmreleases'
297
+ const helmExemption = helm
298
+ ? policy?.objectReconcile === 'ignore'
299
+ ? 'the object opts out of drift detection'
300
+ : ev?.ignored === 'effective'
301
+ ? `drift detection ignores this field (${ev.ignoredBy ?? 'ignore rule'})`
302
+ : null
303
+ : null
304
+ // A change whose fields are unknown may be the one that removes the opt-out.
305
+ if (!helm && path && policy?.objectReconcile === 'ignore') {
306
+ return { level: 'info', reason: `This object opts out of ${tool} reconciliation, so a sync is not expected to overwrite it.` }
307
+ }
308
+ // Flux reads IfNotPresent from the desired manifest; the live annotation
309
+ // may have been added by hand or outlived a change in the source.
310
+ if (policy?.objectReconcile === 'if-not-present') {
311
+ return {
312
+ level: 'may-revert',
313
+ reason: `This object carries ${tool}'s IfNotPresent annotation, but Radar can't confirm the GitOps source declares it; if it doesn't, the next reconcile overwrites this change.`,
314
+ }
315
+ }
316
+ if (!helm && ev?.ignored === 'effective') {
317
+ return {
318
+ level: 'info',
319
+ reason: `${capitalize(ownerKindRef(owner))} ignores this field (${ev.ignoredBy ?? 'ignore rule'}), so a sync is not expected to overwrite it.`,
320
+ }
321
+ }
322
+
323
+ const declared = Boolean(ev && !ev.error && (ev.lastApplied === 'present' || ev.ownedByGitOps))
324
+ if (declared || policy?.replace) {
325
+ if (ev?.ignored === 'unevaluated') {
326
+ return {
327
+ level: 'may-revert',
328
+ reason: `Radar can't confirm ${ownerKindRef(owner)}'s ignore rule (${ev.ignoredBy ?? 'spec.ignoreDifferences'}) covers this field, so the next sync may overwrite it.`,
329
+ }
330
+ }
331
+ if (ev?.ignored === 'comparison-only') {
332
+ return {
333
+ level: 'may-revert',
334
+ reason: `${capitalize(ownerKindRef(owner))} excludes this field from comparison (${ev.ignoredBy ?? 'ignoreDifferences'}) without RespectIgnoreDifferences, so self-heal won't react, but the next sync will overwrite it.`,
335
+ }
336
+ }
337
+ const outcome = helmExemption
338
+ ? { level: 'may-revert' as const, reason: `Flux won't correct it as drift because ${helmExemption}, but the next Helm upgrade overwrites it.` }
339
+ : declaredOutcome(ctx)
340
+ // A wildcard the source only partly declares: which entries revert
341
+ // depends on which one the user changes.
342
+ if (ev?.partial && !policy?.replace && outcome.level === 'will-revert') {
343
+ return {
344
+ level: 'may-revert',
345
+ reason: `The GitOps source sets this for some containers but not others; the ones it sets will be overwritten at ${nextSync(ctx)}.`,
346
+ }
347
+ }
348
+ const why =
349
+ declared && ev
350
+ ? `The GitOps source sets this field (${evidenceNote(ev, owner)}).`
351
+ : `${capitalize(ownerKindRef(owner))} syncs with Replace=true, which replaces the whole object.`
352
+ return { level: outcome.level, reason: `${why} ${outcome.reason}` }
353
+ }
354
+
355
+ const unknown = 'It may be overwritten on sync if the GitOps source declares this field.'
356
+ if (!path) {
357
+ return { level: 'may-revert', reason: `Radar doesn't know which fields this change touches. ${unknown}` }
358
+ }
359
+ if (!ev || ev.error) {
360
+ const cause = ev?.error || ctx.input.evidenceError
361
+ return {
362
+ level: 'may-revert',
363
+ reason: `Radar can't see which fields the GitOps source declares${cause ? ` (${cause})` : ''}. ${unknown}`,
364
+ }
365
+ }
366
+ return {
367
+ level: 'may-revert',
368
+ reason: `Radar found no record of the GitOps source setting this field, but that doesn't rule it out. ${unknown}`,
369
+ }
370
+ }
371
+
372
+ function classifyFieldWrite(ctx: Context, write: GitOpsWrite): Verdict {
373
+ const paths: Array<string | null> = write.paths?.length ? write.paths : [null]
374
+ let worst = classifyPath(ctx, paths[0])
375
+ for (const path of paths.slice(1)) {
376
+ const verdict = classifyPath(ctx, path)
377
+ if (LEVEL_RANK[verdict.level] > LEVEL_RANK[worst.level]) worst = verdict
378
+ }
379
+ return worst
380
+ }
381
+
382
+ function classifyDelete(ctx: Context): Verdict {
383
+ const { owner, policy } = ctx
384
+ const tool = gitOpsToolLabel(owner.tool)
385
+ if (!policy) {
386
+ return {
387
+ level: 'may-revert',
388
+ reason: `Radar couldn't read ${ownerKindRef(owner)}'s sync policy (${ctx.policyUnknown}); the next sync will recreate it.`,
389
+ }
390
+ }
391
+ if (policy.objectReconcile === 'ignore') {
392
+ if (owner.kind === 'helmreleases') {
393
+ return { level: 'may-revert', reason: "Flux drift detection skips this object, but the next Helm upgrade recreates it." }
394
+ }
395
+ return { level: 'info', reason: `This object opts out of ${tool} reconciliation, so it is not expected to be recreated.` }
396
+ }
397
+ if (policy.suspended) {
398
+ return { level: 'may-revert', reason: `${capitalize(ownerKindRef(owner))} is suspended; resuming it will recreate it.` }
399
+ }
400
+ if (policy.selfHeal) {
401
+ return { level: 'will-revert', reason: `${tool} will recreate it at ${nextSync(ctx)}.` }
402
+ }
403
+ return { level: 'may-revert', reason: 'The next sync will recreate it.' }
404
+ }
405
+
406
+ function operatorOwnerOf(input: GitOpsWriteGuardInput): GitOpsWriteOperatorOwner | null {
407
+ if (input.operatorOwner !== undefined) return input.operatorOwner
408
+ const controller = input.evidence?.controllerOwner
409
+ return controller ? { kind: controller.kind, name: controller.name } : null
410
+ }
411
+
412
+ function classifyWrite(input: GitOpsWriteGuardInput, owner: GitOpsOwnerRef | null, write: GitOpsWrite): Verdict {
413
+ if (write.scope === 'status') {
414
+ return { level: 'none', reason: 'This writes controller status; ordinary GitOps sync does not manage these fields.' }
415
+ }
416
+ if (write.scope === 'delete') {
417
+ const operator = operatorOwnerOf(input)
418
+ if (operator) {
419
+ return { level: 'info', reason: `${operator.kind} ${operator.name} will recreate this ${input.target.kind}.` }
420
+ }
421
+ }
422
+ if (owner) {
423
+ const tool = gitOpsToolLabel(owner.tool)
424
+ if (write.scope === 'create-child') {
425
+ return { level: 'info', reason: `A new object isn't in the GitOps source, so ${tool} is not expected to revert it.` }
426
+ }
427
+ const ctx: Context = {
428
+ input,
429
+ owner,
430
+ policy: input.evidence?.policy ?? null,
431
+ policyUnknown: input.evidence?.policyError || input.evidenceError || 'unavailable',
432
+ }
433
+ return write.scope === 'delete' ? classifyDelete(ctx) : classifyFieldWrite(ctx, write)
434
+ }
435
+ if (input.helmRelease) {
436
+ if (write.scope === 'create-child') {
437
+ return { level: 'info', reason: "A new object isn't part of the release, so a Helm upgrade is not expected to revert it." }
438
+ }
439
+ if (write.scope === 'delete') {
440
+ return { level: 'may-revert', reason: 'The next Helm upgrade or rollback will recreate it.' }
441
+ }
442
+ const helmOwned = (write.paths ?? []).some((path) =>
443
+ input.evidence?.paths.find((p) => p.path === path)?.ownedBy.some((o) => o.tool === 'helm'),
444
+ )
445
+ return {
446
+ level: 'may-revert',
447
+ reason: helmOwned
448
+ ? 'Helm set this field; the next Helm upgrade or rollback will overwrite it.'
449
+ : 'If the chart sets this field, the next Helm upgrade or rollback will overwrite it.',
450
+ }
451
+ }
452
+ if (input.ownershipError && write.scope !== 'create-child') {
453
+ return {
454
+ level: 'may-revert',
455
+ reason: input.ownershipError,
456
+ }
457
+ }
458
+ return { level: 'none', reason: '' }
459
+ }
460
+
461
+ function headline(guard: Pick<GitOpsWriteGuard, 'owner' | 'helmRelease'>, level: GitOpsWriteLevel): string {
462
+ const who = guard.owner
463
+ ? describeGitOpsOwner(guard.owner)
464
+ : guard.helmRelease
465
+ ? `Helm release ${guard.helmRelease.namespace}/${guard.helmRelease.name}`
466
+ : 'A GitOps tool'
467
+ if (level === 'will-revert') return `${who} will revert this change.`
468
+ if (level === 'may-revert') return `${who} may revert this change.`
469
+ if (level === 'info') return guard.owner || guard.helmRelease ? `Managed by ${who}.` : ''
470
+ return ''
471
+ }
472
+
473
+ export function gitOpsWriteHeadline(guard: GitOpsWriteGuard): string {
474
+ return headline(guard, guard.level)
475
+ }
476
+
477
+ export function evaluateGitOpsWriteGuard(input: GitOpsWriteGuardInput): GitOpsWriteGuard {
478
+ const owner = input.owner ?? null
479
+ const helmRelease = owner ? null : (input.helmRelease ?? null)
480
+ const ownershipError = owner || helmRelease ? null : (input.ownershipError ?? null)
481
+ const normalized: GitOpsWriteGuardInput = { ...input, owner, helmRelease, ownershipError }
482
+
483
+ const perWrite = input.writes.map((write) => {
484
+ const verdict = classifyWrite(normalized, owner, write)
485
+ if (owner && input.ownerMatchedByName && verdict.level === 'info' && write.scope !== 'create-child') {
486
+ return {
487
+ write,
488
+ level: 'may-revert' as const,
489
+ reason: `${verdict.reason} Radar matched ${ownerKindRef(owner)} by name only, so it can't confirm this.`,
490
+ }
491
+ }
492
+ return { write, ...verdict }
493
+ })
494
+ const level = perWrite.reduce<GitOpsWriteLevel>((max, entry) => maxLevel(max, entry.level), 'none')
495
+ const top = perWrite.find((entry) => entry.level === level)
496
+ const summary = level === 'none' ? '' : [headline({ owner, helmRelease }, level), top?.reason ?? ''].filter(Boolean).join(' ')
497
+
498
+ return {
499
+ owner,
500
+ helmRelease,
501
+ pending: Boolean(input.ownerPending),
502
+ level,
503
+ perWrite,
504
+ summary,
505
+ syncPolicy: owner ? gitOpsSyncPolicy(input.evidence?.policy) : null,
506
+ requiresAck: level === 'will-revert' || level === 'may-revert',
507
+ ownershipError,
508
+ }
509
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { formatGrant } from './grant'
3
+
4
+ describe('formatGrant', () => {
5
+ it('words a namespaced grant like the server', () => {
6
+ expect(formatGrant({ verb: 'patch', group: 'postgresql.cnpg.io', resource: 'clusters', subresource: 'status', namespace: 'pg' })).toBe(
7
+ 'patch clusters/status (postgresql.cnpg.io) in namespace pg',
8
+ )
9
+ expect(formatGrant({ verb: 'get', resource: 'pods', subresource: 'proxy', namespace: 'pg' })).toBe('get pods/proxy in namespace pg')
10
+ })
11
+ it('words a cluster-wide grant with the group as resource.group', () => {
12
+ expect(formatGrant({ verb: 'patch', resource: 'clusters', group: 'example.io', subresource: 'status' })).toBe('patch clusters.example.io/status cluster-wide')
13
+ expect(formatGrant({ verb: 'get', resource: 'nodes' })).toBe('get nodes cluster-wide')
14
+ expect(formatGrant({ verb: 'list', group: 'postgresql.cnpg.io', resource: 'clusters' })).toBe('list clusters.postgresql.cnpg.io cluster-wide')
15
+ })
16
+ it('is undefined without a grant', () => {
17
+ expect(formatGrant(undefined)).toBeUndefined()
18
+ })
19
+ })
@@ -0,0 +1,35 @@
1
+ /**
2
+ * One RBAC permission a read or action needs, as the server sends it. An
3
+ * empty namespace means cluster-wide (a cluster-scoped resource, or a list
4
+ * across every namespace).
5
+ */
6
+ export interface Grant {
7
+ verb: string
8
+ group?: string
9
+ resource: string
10
+ subresource?: string
11
+ namespace?: string
12
+ }
13
+
14
+ function grantResource(g: Grant): string {
15
+ return g.subresource ? `${g.resource}/${g.subresource}` : g.resource
16
+ }
17
+
18
+ /**
19
+ * The grant's two halves for display: what ("patch clusters/status
20
+ * (postgresql.cnpg.io)") and where (" in namespace pg", " cluster-wide").
21
+ */
22
+ export function grantParts(g: Grant): { what: string; scope: string } {
23
+ if (g.namespace) {
24
+ return { what: `${g.verb} ${grantResource(g)}${g.group ? ` (${g.group})` : ''}`, scope: ` in namespace ${g.namespace}` }
25
+ }
26
+ return { what: `${g.verb} ${g.resource}${g.group ? `.${g.group}` : ''}${g.subresource ? `/${g.subresource}` : ''}`, scope: ' cluster-wide' }
27
+ }
28
+
29
+ export function formatGrant(g: Grant): string
30
+ export function formatGrant(g: Grant | undefined): string | undefined
31
+ export function formatGrant(g: Grant | undefined): string | undefined {
32
+ if (!g) return undefined
33
+ const { what, scope } = grantParts(g)
34
+ return what + scope
35
+ }
@@ -17,6 +17,7 @@ export * from './api-resources'
17
17
  export * from './skeleton-yaml'
18
18
  export * from './k8s-errors'
19
19
  export * from './parse-go-time'
20
+ export * from './gitops-write-guard'
20
21
  export * from './view-transition'
21
22
  export * from './validators'
22
23
  export * from './gitops-owner'
@@ -31,3 +32,8 @@ export * from './bulk-workload-actions'
31
32
  export * from './inClusterConsent'
32
33
  export * from './workflow-execution'
33
34
  export * from './network-policy'
35
+ export * from './cnpg-backup'
36
+ export * from './url-path'
37
+ export * from './resource-refs'
38
+
39
+ export * from './grant'
@@ -0,0 +1,22 @@
1
+ import type { ResourceRef } from '../types'
2
+
3
+ interface IngressClassParametersReference {
4
+ name: string
5
+ kind: string
6
+ apiGroup?: string
7
+ scope?: 'Cluster' | 'Namespace'
8
+ namespace?: string
9
+ }
10
+
11
+ /** IngressClass parameters default to cluster scope and the core API group.
12
+ * A namespaced reference requires its own namespace; the class has none.
13
+ */
14
+ export function ingressClassParametersResourceRef(ref: IngressClassParametersReference): ResourceRef | null {
15
+ if (ref.scope === 'Namespace' && !ref.namespace) return null
16
+ return {
17
+ kind: ref.kind,
18
+ group: ref.apiGroup ?? '',
19
+ namespace: ref.scope === 'Namespace' ? ref.namespace! : '',
20
+ name: ref.name,
21
+ }
22
+ }
@@ -6,7 +6,7 @@ interface FriendlyError {
6
6
  suggestion: string
7
7
  }
8
8
 
9
- const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string) => FriendlyError }> = [
9
+ const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string, allowApply: boolean) => FriendlyError }> = [
10
10
  // Admission webhook timeout
11
11
  {
12
12
  test: (msg) => /failed calling webhook.*context deadline exceeded/i.test(msg),
@@ -75,9 +75,9 @@ const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string)
75
75
  // Already exists (create mode)
76
76
  {
77
77
  test: (msg) => /already exists/i.test(msg),
78
- friendly: () => ({
78
+ friendly: (_msg, allowApply) => ({
79
79
  summary: 'Resource already exists',
80
- suggestion: 'Switch to Apply mode to update the existing resource, or change the name to create a new one.',
80
+ suggestion: allowApply ? 'Switch to Apply mode to update the existing resource, or change the name to create a new one.' : 'Change the name to create a new resource, or remove this document if it was already created.',
81
81
  }),
82
82
  },
83
83
  // Quota exceeded
@@ -122,11 +122,11 @@ function stripPrefix(msg: string): string {
122
122
  return msg.replace(/^failed to (apply|create) resource:\s*/i, '')
123
123
  }
124
124
 
125
- export function getFriendlyError(rawError: string): { summary: string; suggestion: string; raw: string } | null {
125
+ export function getFriendlyError(rawError: string, allowApply = true): { summary: string; suggestion: string; raw: string } | null {
126
126
  const cleaned = stripPrefix(rawError)
127
127
  for (const pattern of patterns) {
128
128
  if (pattern.test(cleaned)) {
129
- const { summary, suggestion } = pattern.friendly(cleaned)
129
+ const { summary, suggestion } = pattern.friendly(cleaned, allowApply)
130
130
  return { summary, suggestion, raw: cleaned }
131
131
  }
132
132
  }
@@ -134,8 +134,8 @@ export function getFriendlyError(rawError: string): { summary: string; suggestio
134
134
  }
135
135
 
136
136
  // Always returns something displayable — friendly if matched, raw if not
137
- export function formatApplyError(rawError: string): { summary: string; suggestion?: string; raw: string } {
138
- const friendly = getFriendlyError(rawError)
137
+ export function formatApplyError(rawError: string, { allowApply = true }: { allowApply?: boolean } = {}): { summary: string; suggestion?: string; raw: string } {
138
+ const friendly = getFriendlyError(rawError, allowApply)
139
139
  if (friendly) return friendly
140
140
  const cleaned = stripPrefix(rawError)
141
141
  return { summary: cleaned, raw: rawError }