@skyhook-io/k8s-ui 1.3.2 → 1.4.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 (78) hide show
  1. package/package.json +2 -2
  2. package/src/components/audit/AuditAlerts.tsx +88 -0
  3. package/src/components/audit/AuditCard.tsx +130 -0
  4. package/src/components/audit/AuditFindingsTable.tsx +526 -0
  5. package/src/components/audit/index.ts +3 -0
  6. package/src/components/dock/DockContext.tsx +5 -2
  7. package/src/components/dock/LocalTerminalTab.tsx +11 -0
  8. package/src/components/resources/ResourcesView.tsx +126 -49
  9. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
  10. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
  11. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
  12. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
  13. package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
  14. package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
  15. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +445 -0
  16. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
  17. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
  18. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
  19. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +160 -0
  20. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
  21. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
  22. package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
  23. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
  24. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
  25. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
  26. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
  27. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
  28. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
  29. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
  30. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
  31. package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
  32. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
  33. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +17 -57
  34. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
  35. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
  36. package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
  37. package/src/components/resources/renderers/PodRenderer.tsx +22 -74
  38. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
  39. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
  41. package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
  42. package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
  43. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
  44. package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
  45. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
  46. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
  47. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
  48. package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
  49. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
  50. package/src/components/resources/renderers/index.ts +3 -0
  51. package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
  52. package/src/components/resources/resource-utils-cnpg.ts +47 -0
  53. package/src/components/resources/resource-utils-karpenter.ts +6 -0
  54. package/src/components/resources/resource-utils-prometheus.ts +50 -6
  55. package/src/components/resources/resource-utils.ts +131 -33
  56. package/src/components/shared/CreateResourceDialog.tsx +236 -0
  57. package/src/components/shared/EditableYamlView.tsx +16 -1
  58. package/src/components/shared/ResourceActionsBar.tsx +9 -7
  59. package/src/components/shared/ResourceRendererDispatch.tsx +13 -6
  60. package/src/components/shared/index.ts +1 -0
  61. package/src/components/timeline/TimelineList.tsx +11 -4
  62. package/src/components/topology/K8sResourceNode.tsx +15 -0
  63. package/src/components/topology/TopologyControls.tsx +21 -1
  64. package/src/components/topology/TopologyGraph.tsx +1 -1
  65. package/src/components/ui/Badge.tsx +8 -0
  66. package/src/components/ui/drawer-components.tsx +68 -5
  67. package/src/components/workload/WorkloadView.tsx +55 -29
  68. package/src/index.ts +3 -0
  69. package/src/theme/variables.css +1 -1
  70. package/src/types/core.ts +2 -1
  71. package/src/types/gitops.ts +8 -0
  72. package/src/utils/badge-colors.ts +7 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/k8s-errors.ts +142 -0
  75. package/src/utils/navigation.test.ts +29 -2
  76. package/src/utils/navigation.ts +45 -24
  77. package/src/utils/resource-icons.ts +3 -0
  78. package/src/utils/skeleton-yaml.ts +302 -0
@@ -1,5 +1,9 @@
1
- import { describe, test, expect } from 'vitest'
2
- import { kindToPlural, pluralToKind, refToSelectedResource } from './navigation'
1
+ import { describe, test, expect, afterEach } from 'vitest'
2
+ import { kindToPlural, pluralToKind, refToSelectedResource, initNavigationMap, resetNavigationMap } from './navigation'
3
+
4
+ afterEach(() => {
5
+ resetNavigationMap()
6
+ })
3
7
 
4
8
  describe('kindToPlural', () => {
5
9
  test('singular PascalCase to plural lowercase', () => {
@@ -110,6 +114,29 @@ describe('pluralToKind', () => {
110
114
  })
111
115
  })
112
116
 
117
+ describe('initNavigationMap', () => {
118
+ test('discovered API resources override heuristic pluralization', () => {
119
+ // Before init, an unknown CRD would hit the heuristic fallback
120
+ // After init, it uses the discovered plural name
121
+ initNavigationMap([
122
+ { group: 'external-secrets.io', version: 'v1', kind: 'SecretStore', name: 'secretstores', namespaced: true, isCrd: true, verbs: ['get'] },
123
+ { group: 'external-secrets.io', version: 'v1', kind: 'ClusterSecretStore', name: 'clustersecretstores', namespaced: false, isCrd: true, verbs: ['get'] },
124
+ ])
125
+ expect(kindToPlural('SecretStore')).toBe('secretstores')
126
+ expect(kindToPlural('ClusterSecretStore')).toBe('clustersecretstores')
127
+ expect(pluralToKind('secretstores')).toBe('SecretStore')
128
+ expect(pluralToKind('clustersecretstores')).toBe('ClusterSecretStore')
129
+ })
130
+
131
+ test('prevents double-pluralization of discovered plurals', () => {
132
+ initNavigationMap([
133
+ { group: 'external-secrets.io', version: 'v1', kind: 'SecretStore', name: 'secretstores', namespaced: true, isCrd: true, verbs: ['get'] },
134
+ ])
135
+ // Passing an already-plural kind should be idempotent
136
+ expect(kindToPlural('secretstores')).toBe('secretstores')
137
+ })
138
+ })
139
+
113
140
  describe('refToSelectedResource', () => {
114
141
  test('converts singular kind to plural for navigation', () => {
115
142
  const result = refToSelectedResource({
@@ -1,4 +1,4 @@
1
- import type { SelectedResource, ResourceRef } from '../types/core'
1
+ import type { SelectedResource, ResourceRef, APIResource } from '../types/core'
2
2
 
3
3
  /**
4
4
  * Canonical callback type for navigating to a resource.
@@ -6,9 +6,8 @@ import type { SelectedResource, ResourceRef } from '../types/core'
6
6
  */
7
7
  export type NavigateToResource = (resource: SelectedResource) => void
8
8
 
9
- // Known plural API resource names singular PascalCase kind.
10
- // Shared between kindToPlural (idempotency guard) and pluralToKind (reverse lookup).
11
- const PLURAL_TO_KIND: Record<string, string> = {
9
+ // Fallback map for core K8s resources used before API discovery completes.
10
+ const BUILTIN_PLURAL_TO_KIND: Record<string, string> = {
12
11
  pods: 'Pod',
13
12
  services: 'Service',
14
13
  deployments: 'Deployment',
@@ -16,11 +15,6 @@ const PLURAL_TO_KIND: Record<string, string> = {
16
15
  statefulsets: 'StatefulSet',
17
16
  replicasets: 'ReplicaSet',
18
17
  ingresses: 'Ingress',
19
- gateways: 'Gateway',
20
- httproutes: 'HTTPRoute',
21
- grpcroutes: 'GRPCRoute',
22
- tcproutes: 'TCPRoute',
23
- tlsroutes: 'TLSRoute',
24
18
  configmaps: 'ConfigMap',
25
19
  secrets: 'Secret',
26
20
  namespaces: 'Namespace',
@@ -33,24 +27,44 @@ const PLURAL_TO_KIND: Record<string, string> = {
33
27
  persistentvolumes: 'PersistentVolume',
34
28
  storageclasses: 'StorageClass',
35
29
  poddisruptionbudgets: 'PodDisruptionBudget',
36
- rollouts: 'Rollout',
37
- applications: 'Application',
38
- kustomizations: 'Kustomization',
39
- helmreleases: 'HelmRelease',
40
- gitrepositories: 'GitRepository',
41
- certificates: 'Certificate',
42
30
  roles: 'Role',
43
31
  clusterroles: 'ClusterRole',
44
32
  rolebindings: 'RoleBinding',
45
33
  clusterrolebindings: 'ClusterRoleBinding',
46
34
  serviceaccounts: 'ServiceAccount',
47
35
  networkpolicies: 'NetworkPolicy',
48
- verticalpodautoscalers: 'VerticalPodAutoscaler',
49
- virtualservices: 'VirtualService',
50
- destinationrules: 'DestinationRule',
51
- serviceentries: 'ServiceEntry',
52
- peerauthentications: 'PeerAuthentication',
53
- authorizationpolicies: 'AuthorizationPolicy',
36
+ }
37
+
38
+ // Dynamic map built from API discovery — populated by initNavigationMap().
39
+ // Once populated, this is the source of truth for all kind↔plural lookups.
40
+ let discoveredPluralToKind: Record<string, string> | null = null
41
+ let discoveredKindToPlural: Record<string, string> | null = null
42
+
43
+ /**
44
+ * Initialize navigation maps from discovered API resources.
45
+ * Call once when API resources are fetched. After this, kindToPlural/pluralToKind
46
+ * use the real cluster data instead of heuristics.
47
+ */
48
+ export function initNavigationMap(resources: APIResource[]) {
49
+ const p2k: Record<string, string> = { ...BUILTIN_PLURAL_TO_KIND }
50
+ const k2p: Record<string, string> = {}
51
+ for (const r of resources) {
52
+ const plural = r.name.toLowerCase()
53
+ p2k[plural] = r.kind
54
+ k2p[r.kind.toLowerCase()] = plural
55
+ }
56
+ discoveredPluralToKind = p2k
57
+ discoveredKindToPlural = k2p
58
+ }
59
+
60
+ /** Reset navigation maps to builtin-only state. For testing. */
61
+ export function resetNavigationMap() {
62
+ discoveredPluralToKind = null
63
+ discoveredKindToPlural = null
64
+ }
65
+
66
+ function getPluralToKind(): Record<string, string> {
67
+ return discoveredPluralToKind || BUILTIN_PLURAL_TO_KIND
54
68
  }
55
69
 
56
70
  /**
@@ -61,9 +75,15 @@ const PLURAL_TO_KIND: Record<string, string> = {
61
75
  */
62
76
  export function kindToPlural(kind: string): string {
63
77
  const kindLower = kind.toLowerCase()
78
+ const pluralToKindMap = getPluralToKind()
64
79
 
65
80
  // Already a known plural — return as-is to prevent double-pluralization
66
- if (kindLower in PLURAL_TO_KIND) return kindLower
81
+ if (kindLower in pluralToKindMap) return kindLower
82
+
83
+ // Lookup from discovered API resources (singular kind → plural name)
84
+ if (discoveredKindToPlural && kindLower in discoveredKindToPlural) {
85
+ return discoveredKindToPlural[kindLower]
86
+ }
67
87
 
68
88
  // Aliases: abbreviations or mappings to a different resource name
69
89
  const aliases: Record<string, string> = {
@@ -73,7 +93,7 @@ export function kindToPlural(kind: string): string {
73
93
  }
74
94
  if (aliases[kindLower]) return aliases[kindLower]
75
95
 
76
- // English pluralization rules (covers *Class→*classes, *Policy→*policies, *Repository→*repositories, etc.)
96
+ // Fallback: English pluralization rules (covers *Class→*classes, *Policy→*policies, *Repository→*repositories, etc.)
77
97
  if (kindLower.endsWith('s') || kindLower.endsWith('x') || kindLower.endsWith('ch') || kindLower.endsWith('sh')) {
78
98
  return kindLower + 'es'
79
99
  }
@@ -90,8 +110,9 @@ export function kindToPlural(kind: string): string {
90
110
  */
91
111
  export function pluralToKind(plural: string): string {
92
112
  const lower = plural.toLowerCase()
113
+ const pluralToKindMap = getPluralToKind()
93
114
 
94
- if (PLURAL_TO_KIND[lower]) return PLURAL_TO_KIND[lower]
115
+ if (pluralToKindMap[lower]) return pluralToKindMap[lower]
95
116
 
96
117
  // If it already looks like a singular PascalCase kind (starts with uppercase), return as-is
97
118
  if (plural[0] === plural[0].toUpperCase() && plural[0] !== plural[0].toLowerCase()) {
@@ -80,6 +80,9 @@ const KIND_ICON_MAP: Record<string, LucideIcon> = {
80
80
  service: Plug,
81
81
  ingress: DoorOpen,
82
82
  networkpolicy: ShieldCheck,
83
+ ciliumnetworkpolicy: ShieldCheck,
84
+ ciliumclusterwidenetworkpolicy: ShieldCheck,
85
+ clusternetworkpolicy: ShieldCheck,
83
86
  endpoints: Radio,
84
87
  endpointslice: Radio,
85
88
  gateway: DoorOpen,
@@ -0,0 +1,302 @@
1
+ // Skeleton YAML templates for common Kubernetes resource kinds.
2
+ // Used by the Create Resource dialog to pre-fill the editor.
3
+
4
+ const skeletons: Record<string, string> = {
5
+ Pod: `apiVersion: v1
6
+ kind: Pod
7
+ metadata:
8
+ name: my-pod
9
+ namespace: default
10
+ spec:
11
+ containers:
12
+ - name: app
13
+ image: nginx:latest
14
+ ports:
15
+ - containerPort: 80`,
16
+
17
+ Deployment: `apiVersion: apps/v1
18
+ kind: Deployment
19
+ metadata:
20
+ name: my-deployment
21
+ namespace: default
22
+ spec:
23
+ replicas: 1
24
+ selector:
25
+ matchLabels:
26
+ app: my-deployment
27
+ template:
28
+ metadata:
29
+ labels:
30
+ app: my-deployment
31
+ spec:
32
+ containers:
33
+ - name: app
34
+ image: nginx:latest
35
+ ports:
36
+ - containerPort: 80`,
37
+
38
+ Service: `apiVersion: v1
39
+ kind: Service
40
+ metadata:
41
+ name: my-service
42
+ namespace: default
43
+ spec:
44
+ selector:
45
+ app: my-app
46
+ ports:
47
+ - port: 80
48
+ targetPort: 80
49
+ type: ClusterIP`,
50
+
51
+ ConfigMap: `apiVersion: v1
52
+ kind: ConfigMap
53
+ metadata:
54
+ name: my-configmap
55
+ namespace: default
56
+ data:
57
+ key: value`,
58
+
59
+ Secret: `apiVersion: v1
60
+ kind: Secret
61
+ metadata:
62
+ name: my-secret
63
+ namespace: default
64
+ type: Opaque
65
+ stringData:
66
+ key: value`,
67
+
68
+ Namespace: `apiVersion: v1
69
+ kind: Namespace
70
+ metadata:
71
+ name: my-namespace`,
72
+
73
+ Job: `apiVersion: batch/v1
74
+ kind: Job
75
+ metadata:
76
+ name: my-job
77
+ namespace: default
78
+ spec:
79
+ template:
80
+ spec:
81
+ containers:
82
+ - name: job
83
+ image: busybox:latest
84
+ command: ["echo", "hello"]
85
+ restartPolicy: Never
86
+ backoffLimit: 3`,
87
+
88
+ CronJob: `apiVersion: batch/v1
89
+ kind: CronJob
90
+ metadata:
91
+ name: my-cronjob
92
+ namespace: default
93
+ spec:
94
+ schedule: "*/5 * * * *"
95
+ jobTemplate:
96
+ spec:
97
+ template:
98
+ spec:
99
+ containers:
100
+ - name: job
101
+ image: busybox:latest
102
+ command: ["echo", "hello"]
103
+ restartPolicy: Never`,
104
+
105
+ Ingress: `apiVersion: networking.k8s.io/v1
106
+ kind: Ingress
107
+ metadata:
108
+ name: my-ingress
109
+ namespace: default
110
+ spec:
111
+ rules:
112
+ - host: example.com
113
+ http:
114
+ paths:
115
+ - path: /
116
+ pathType: Prefix
117
+ backend:
118
+ service:
119
+ name: my-service
120
+ port:
121
+ number: 80`,
122
+
123
+ PersistentVolumeClaim: `apiVersion: v1
124
+ kind: PersistentVolumeClaim
125
+ metadata:
126
+ name: my-pvc
127
+ namespace: default
128
+ spec:
129
+ accessModes:
130
+ - ReadWriteOnce
131
+ resources:
132
+ requests:
133
+ storage: 1Gi`,
134
+
135
+ ServiceAccount: `apiVersion: v1
136
+ kind: ServiceAccount
137
+ metadata:
138
+ name: my-service-account
139
+ namespace: default`,
140
+
141
+ StatefulSet: `apiVersion: apps/v1
142
+ kind: StatefulSet
143
+ metadata:
144
+ name: my-statefulset
145
+ namespace: default
146
+ spec:
147
+ replicas: 1
148
+ selector:
149
+ matchLabels:
150
+ app: my-statefulset
151
+ serviceName: my-statefulset
152
+ template:
153
+ metadata:
154
+ labels:
155
+ app: my-statefulset
156
+ spec:
157
+ containers:
158
+ - name: app
159
+ image: nginx:latest
160
+ ports:
161
+ - containerPort: 80`,
162
+
163
+ DaemonSet: `apiVersion: apps/v1
164
+ kind: DaemonSet
165
+ metadata:
166
+ name: my-daemonset
167
+ namespace: default
168
+ spec:
169
+ selector:
170
+ matchLabels:
171
+ app: my-daemonset
172
+ template:
173
+ metadata:
174
+ labels:
175
+ app: my-daemonset
176
+ spec:
177
+ containers:
178
+ - name: app
179
+ image: nginx:latest`,
180
+ }
181
+
182
+ // Returns skeleton YAML for a known kind, or a generic template for unknown kinds.
183
+ export function getSkeletonYaml(kind: string, group?: string): string {
184
+ const skeleton = skeletons[kind]
185
+ if (skeleton) return skeleton
186
+
187
+ // For unknown/CRD kinds, provide a minimal template
188
+ let apiVersion = 'v1'
189
+ if (group) {
190
+ apiVersion = `${group}/v1`
191
+ }
192
+
193
+ return `apiVersion: ${apiVersion}
194
+ kind: ${kind}
195
+ metadata:
196
+ name: my-${kind.toLowerCase()}
197
+ namespace: default`
198
+ }
199
+
200
+ // Strips cluster-managed metadata from a resource YAML for duplication.
201
+ export function cleanYamlForDuplicate(yamlContent: string): string {
202
+ const lines = yamlContent.split('\n')
203
+ const result: string[] = []
204
+ let skipUntilDedent = false
205
+ let skipIndent = 0
206
+
207
+ const stripTopLevelKeys = new Set([
208
+ 'status:',
209
+ ])
210
+
211
+ const stripMetadataKeys = new Set([
212
+ 'resourceVersion:',
213
+ 'uid:',
214
+ 'creationTimestamp:',
215
+ 'generation:',
216
+ 'managedFields:',
217
+ 'selfLink:',
218
+ ])
219
+
220
+ const stripAnnotationKeys = [
221
+ 'kubectl.kubernetes.io/last-applied-configuration:',
222
+ ]
223
+
224
+ let inMetadata = false
225
+ let inAnnotations = false
226
+ let metadataIndent = 0
227
+ let annotationsIndent = 0
228
+
229
+ for (const line of lines) {
230
+ const trimmed = line.trimStart()
231
+ const indent = line.length - trimmed.length
232
+
233
+ // Handle skip mode (for multi-line values like managedFields or status)
234
+ if (skipUntilDedent) {
235
+ if (indent > skipIndent || trimmed === '' || trimmed.startsWith('-')) {
236
+ continue
237
+ }
238
+ skipUntilDedent = false
239
+ }
240
+
241
+ // Track metadata section
242
+ if (trimmed === 'metadata:') {
243
+ inMetadata = true
244
+ metadataIndent = indent
245
+ result.push(line)
246
+ continue
247
+ }
248
+
249
+ if (inMetadata && indent <= metadataIndent && trimmed !== '' && trimmed !== 'metadata:') {
250
+ inMetadata = false
251
+ inAnnotations = false
252
+ }
253
+
254
+ // Track annotations within metadata
255
+ if (inMetadata && trimmed === 'annotations:') {
256
+ inAnnotations = true
257
+ annotationsIndent = indent
258
+ result.push(line)
259
+ continue
260
+ }
261
+
262
+ if (inAnnotations && indent <= annotationsIndent && trimmed !== '' && trimmed !== 'annotations:') {
263
+ inAnnotations = false
264
+ }
265
+
266
+ // Strip top-level keys (status)
267
+ if (indent === 0 && stripTopLevelKeys.has(trimmed.split(' ')[0])) {
268
+ skipUntilDedent = true
269
+ skipIndent = indent
270
+ continue
271
+ }
272
+
273
+ // Strip metadata keys
274
+ if (inMetadata && !inAnnotations) {
275
+ const key = trimmed.split(' ')[0]
276
+ if (stripMetadataKeys.has(key)) {
277
+ if (key === 'managedFields:') {
278
+ skipUntilDedent = true
279
+ skipIndent = indent
280
+ }
281
+ continue
282
+ }
283
+ // Clear the name value (user needs to provide a new one)
284
+ if (trimmed.startsWith('name:')) {
285
+ result.push(line.replace(/name:.*/, 'name: '))
286
+ continue
287
+ }
288
+ }
289
+
290
+ // Strip specific annotations
291
+ if (inAnnotations) {
292
+ const shouldStrip = stripAnnotationKeys.some(key => trimmed.startsWith(key))
293
+ if (shouldStrip) {
294
+ continue
295
+ }
296
+ }
297
+
298
+ result.push(line)
299
+ }
300
+
301
+ return result.join('\n')
302
+ }