@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.
- package/package.json +2 -2
- package/src/components/audit/AuditAlerts.tsx +88 -0
- package/src/components/audit/AuditCard.tsx +130 -0
- package/src/components/audit/AuditFindingsTable.tsx +526 -0
- package/src/components/audit/index.ts +3 -0
- package/src/components/dock/DockContext.tsx +5 -2
- package/src/components/dock/LocalTerminalTab.tsx +11 -0
- package/src/components/resources/ResourcesView.tsx +126 -49
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
- package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +445 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +160 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +17 -57
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/PodRenderer.tsx +22 -74
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
- package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
- package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
- package/src/components/resources/resource-utils-cnpg.ts +47 -0
- package/src/components/resources/resource-utils-karpenter.ts +6 -0
- package/src/components/resources/resource-utils-prometheus.ts +50 -6
- package/src/components/resources/resource-utils.ts +131 -33
- package/src/components/shared/CreateResourceDialog.tsx +236 -0
- package/src/components/shared/EditableYamlView.tsx +16 -1
- package/src/components/shared/ResourceActionsBar.tsx +9 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -6
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +11 -4
- package/src/components/topology/K8sResourceNode.tsx +15 -0
- package/src/components/topology/TopologyControls.tsx +21 -1
- package/src/components/topology/TopologyGraph.tsx +1 -1
- package/src/components/ui/Badge.tsx +8 -0
- package/src/components/ui/drawer-components.tsx +68 -5
- package/src/components/workload/WorkloadView.tsx +55 -29
- package/src/index.ts +3 -0
- package/src/theme/variables.css +1 -1
- package/src/types/core.ts +2 -1
- package/src/types/gitops.ts +8 -0
- package/src/utils/badge-colors.ts +7 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/k8s-errors.ts +142 -0
- package/src/utils/navigation.test.ts +29 -2
- package/src/utils/navigation.ts +45 -24
- package/src/utils/resource-icons.ts +3 -0
- 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({
|
package/src/utils/navigation.ts
CHANGED
|
@@ -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
|
-
//
|
|
10
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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 (
|
|
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
|
+
}
|