@skyhook-io/k8s-ui 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/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 +14 -0
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +228 -0
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +174 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +10 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
- package/src/components/resources/renderers/index.ts +3 -0
- 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 +9 -2
- package/src/components/shared/index.ts +1 -0
- 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 +20 -4
- package/src/components/workload/WorkloadView.tsx +55 -29
- package/src/index.ts +3 -0
- package/src/types/core.ts +2 -1
- 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/resource-icons.ts +3 -0
- package/src/utils/skeleton-yaml.ts +302 -0
|
@@ -121,6 +121,12 @@ interface WorkloadViewProps {
|
|
|
121
121
|
renderMetricsTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
122
122
|
/** Whether metrics are available for this resource kind */
|
|
123
123
|
isMetricsAvailable?: (kind: string, resource: any) => boolean
|
|
124
|
+
/** Render extra content at the bottom of the overview tab (e.g. audit findings) */
|
|
125
|
+
renderOverviewExtra?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
126
|
+
|
|
127
|
+
// ── Duplicate ────────────────────────────────────────────────────────────
|
|
128
|
+
/** Duplicate handler — opens create dialog with this resource's YAML */
|
|
129
|
+
onDuplicate?: (params: { kind: string; namespace: string; name: string; yaml: string }) => void
|
|
124
130
|
|
|
125
131
|
// ── ResourceActionsBar props (passed through) ────────────────────────────
|
|
126
132
|
/** All props for the actions bar (forwarded as-is) */
|
|
@@ -166,6 +172,9 @@ export function WorkloadView({
|
|
|
166
172
|
renderLogsTab,
|
|
167
173
|
renderMetricsTab,
|
|
168
174
|
isMetricsAvailable,
|
|
175
|
+
// Duplicate
|
|
176
|
+
onDuplicate,
|
|
177
|
+
renderOverviewExtra,
|
|
169
178
|
// Actions bar
|
|
170
179
|
actionsBarProps,
|
|
171
180
|
// Renderer overrides
|
|
@@ -430,22 +439,30 @@ export function WorkloadView({
|
|
|
430
439
|
onSave={onUpdateResource}
|
|
431
440
|
isSaving={isUpdatingResource}
|
|
432
441
|
saveError={updateResourceError}
|
|
442
|
+
onDuplicate={onDuplicate}
|
|
433
443
|
/>
|
|
434
444
|
) : (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
445
|
+
<>
|
|
446
|
+
<ResourceRendererDispatch
|
|
447
|
+
resource={selectedResource}
|
|
448
|
+
data={resource}
|
|
449
|
+
relationships={relationships}
|
|
450
|
+
certificateInfo={certificateInfo}
|
|
451
|
+
onCopy={copyToClipboard}
|
|
452
|
+
copied={copied}
|
|
453
|
+
onNavigate={onNavigateToResource ? (ref) => onNavigateToResource(refToSelectedResource(ref)) : undefined}
|
|
454
|
+
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
455
|
+
isSavingSecret={isUpdatingResource}
|
|
456
|
+
rendererOverrides={rendererOverrides}
|
|
457
|
+
resolvedEnvFrom={resolvedEnvFrom}
|
|
458
|
+
renderMetrics={renderMetricsTab}
|
|
459
|
+
/>
|
|
460
|
+
{renderOverviewExtra && (
|
|
461
|
+
<div className="px-4 pb-4">
|
|
462
|
+
{renderOverviewExtra({ kind, namespace, name })}
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
</>
|
|
449
466
|
)}
|
|
450
467
|
</div>
|
|
451
468
|
</div>
|
|
@@ -571,21 +588,22 @@ export function WorkloadView({
|
|
|
571
588
|
{/* Tab Content */}
|
|
572
589
|
<div className="flex-1 overflow-hidden relative">
|
|
573
590
|
{activeTab === 'overview' && (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
591
|
+
<InfoTab
|
|
592
|
+
resource={resource}
|
|
593
|
+
selectedResource={selectedResource}
|
|
594
|
+
relationships={relationships}
|
|
595
|
+
isLoading={resourceLoading}
|
|
596
|
+
onNavigate={onNavigateToResource}
|
|
597
|
+
onCopy={copyToClipboard}
|
|
598
|
+
copied={copied}
|
|
599
|
+
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
600
|
+
isSavingSecret={isUpdatingResource}
|
|
601
|
+
onOpenLogs={handleOpenLogs}
|
|
602
|
+
onSwitchToTimeline={() => handleSetTab('timeline')}
|
|
603
|
+
rendererOverrides={rendererOverrides}
|
|
604
|
+
resolvedEnvFrom={resolvedEnvFrom}
|
|
605
|
+
extraContent={renderOverviewExtra && renderOverviewExtra({ kind, namespace, name })}
|
|
606
|
+
/>
|
|
589
607
|
)}
|
|
590
608
|
{activeTab === 'timeline' && (
|
|
591
609
|
<EventsTab
|
|
@@ -635,6 +653,7 @@ export function WorkloadView({
|
|
|
635
653
|
onSave={onUpdateResource}
|
|
636
654
|
isSaving={isUpdatingResource}
|
|
637
655
|
saveError={updateResourceError}
|
|
656
|
+
onDuplicate={onDuplicate}
|
|
638
657
|
/>
|
|
639
658
|
)}
|
|
640
659
|
</div>
|
|
@@ -1068,6 +1087,7 @@ function InfoTab({
|
|
|
1068
1087
|
onSwitchToTimeline,
|
|
1069
1088
|
rendererOverrides,
|
|
1070
1089
|
resolvedEnvFrom,
|
|
1090
|
+
extraContent,
|
|
1071
1091
|
}: {
|
|
1072
1092
|
resource: any
|
|
1073
1093
|
selectedResource: SelectedResource
|
|
@@ -1082,6 +1102,7 @@ function InfoTab({
|
|
|
1082
1102
|
onSwitchToTimeline?: () => void
|
|
1083
1103
|
rendererOverrides?: RendererOverrides
|
|
1084
1104
|
resolvedEnvFrom?: ResolvedEnvFrom
|
|
1105
|
+
extraContent?: ReactNode
|
|
1085
1106
|
}) {
|
|
1086
1107
|
if (isLoading) {
|
|
1087
1108
|
return (
|
|
@@ -1132,6 +1153,11 @@ function InfoTab({
|
|
|
1132
1153
|
</div>
|
|
1133
1154
|
)}
|
|
1134
1155
|
/>
|
|
1156
|
+
{extraContent && (
|
|
1157
|
+
<div className="px-4 pb-4">
|
|
1158
|
+
{extraContent}
|
|
1159
|
+
</div>
|
|
1160
|
+
)}
|
|
1135
1161
|
</div>
|
|
1136
1162
|
)
|
|
1137
1163
|
}
|
package/src/index.ts
CHANGED
|
@@ -33,3 +33,6 @@ export * from './components/dock'
|
|
|
33
33
|
|
|
34
34
|
// Topology (TopologyGraph, TopologySearch, TopologyFilterSidebar, layout utilities)
|
|
35
35
|
export * from './components/topology'
|
|
36
|
+
|
|
37
|
+
// Cluster audit (AuditCard, AuditAlerts, AuditFindingsTable)
|
|
38
|
+
export * from './components/audit'
|
package/src/types/core.ts
CHANGED
|
@@ -145,6 +145,7 @@ export interface TopologyEdge {
|
|
|
145
145
|
type: EdgeType
|
|
146
146
|
label?: string
|
|
147
147
|
skipIfKindVisible?: string // Hide this edge if this kind is visible (for shortcut edges)
|
|
148
|
+
policyEffect?: 'allowed' | 'blocked' | 'unprotected'
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
export interface Topology {
|
|
@@ -846,7 +847,7 @@ export interface TrafficFilters {
|
|
|
846
847
|
}
|
|
847
848
|
|
|
848
849
|
// Main view type now includes 'traffic' and 'cost'
|
|
849
|
-
export type ExtendedMainView = MainView | 'traffic' | 'cost'
|
|
850
|
+
export type ExtendedMainView = MainView | 'traffic' | 'cost' | 'audit'
|
|
850
851
|
|
|
851
852
|
// ============================================================================
|
|
852
853
|
// Image Filesystem Types
|
|
@@ -52,6 +52,13 @@ export const HELM_STATUS_COLORS: Record<string, string> = {
|
|
|
52
52
|
uninstalled: BADGE_SEVERITY_COLORS.neutral,
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Best practices category colors
|
|
56
|
+
export const BP_CATEGORY_BADGE: Record<string, string> = {
|
|
57
|
+
Security: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/40 dark:text-purple-400 dark:border-purple-800/40',
|
|
58
|
+
Reliability: 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950/40 dark:text-sky-400 dark:border-sky-800/40',
|
|
59
|
+
Efficiency: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-400 dark:border-emerald-800/40',
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
// Default fallback color
|
|
56
63
|
export const DEFAULT_BADGE_COLOR = 'bg-theme-elevated text-theme-text-secondary'
|
|
57
64
|
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Human-friendly summaries for common Kubernetes API error patterns.
|
|
2
|
+
// Falls back to null when no known pattern matches — caller shows raw error.
|
|
3
|
+
|
|
4
|
+
interface FriendlyError {
|
|
5
|
+
summary: string
|
|
6
|
+
suggestion: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string) => FriendlyError }> = [
|
|
10
|
+
// Admission webhook timeout
|
|
11
|
+
{
|
|
12
|
+
test: (msg) => /failed calling webhook.*context deadline exceeded/i.test(msg),
|
|
13
|
+
friendly: (msg) => {
|
|
14
|
+
const webhook = msg.match(/webhook "([^"]+)"/)?.[1] ?? 'unknown'
|
|
15
|
+
const service = webhook.split('.')[0] || webhook
|
|
16
|
+
return {
|
|
17
|
+
summary: `Admission webhook timed out (${service})`,
|
|
18
|
+
suggestion: 'A cluster validation webhook failed to respond. This is usually a temporary cluster issue, not a problem with your YAML. Try again or contact your cluster admin.',
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
// Admission webhook rejected
|
|
23
|
+
{
|
|
24
|
+
test: (msg) => /admission webhook.*denied the request/i.test(msg),
|
|
25
|
+
friendly: (msg) => {
|
|
26
|
+
const webhook = msg.match(/webhook "([^"]+)"/)?.[1] ?? 'unknown'
|
|
27
|
+
return {
|
|
28
|
+
summary: `Rejected by admission policy (${webhook.split('.')[0]})`,
|
|
29
|
+
suggestion: 'A cluster admission webhook rejected this resource. Check that your YAML meets the cluster\'s policies, or contact your cluster admin.',
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
// Validation error — invalid field value
|
|
34
|
+
{
|
|
35
|
+
test: (msg) => /is invalid:.*Invalid value/i.test(msg),
|
|
36
|
+
friendly: (msg) => {
|
|
37
|
+
const fields = [...msg.matchAll(/(\S+): Invalid value/g)].map(m => m[1])
|
|
38
|
+
const fieldList = fields.length > 0 ? fields.join(', ') : 'unknown field'
|
|
39
|
+
return {
|
|
40
|
+
summary: `Validation failed: invalid value for ${fieldList}`,
|
|
41
|
+
suggestion: 'One or more field values don\'t match what Kubernetes expects. Check the highlighted fields in the error details.',
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
// Validation error — selector doesn't match labels
|
|
46
|
+
{
|
|
47
|
+
test: (msg) => /`selector` does not match template `labels`/i.test(msg),
|
|
48
|
+
friendly: () => ({
|
|
49
|
+
summary: 'Selector doesn\'t match template labels',
|
|
50
|
+
suggestion: 'The spec.selector.matchLabels must match spec.template.metadata.labels exactly.',
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
// Validation error — required field
|
|
54
|
+
{
|
|
55
|
+
test: (msg) => /Required value|is required/i.test(msg),
|
|
56
|
+
friendly: (msg) => {
|
|
57
|
+
const field = msg.match(/(\S+): Required value/)?.[1] ?? msg.match(/(\S+) is required/)?.[1] ?? ''
|
|
58
|
+
return {
|
|
59
|
+
summary: `Missing required field${field ? `: ${field}` : ''}`,
|
|
60
|
+
suggestion: 'Add the missing required field to your YAML.',
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
// RBAC forbidden — use "is forbidden:" to avoid matching "Forbidden: field is immutable" or "forbidden: exceeded quota"
|
|
65
|
+
{
|
|
66
|
+
test: (msg) => /is forbidden:|cannot .* in the namespace/i.test(msg),
|
|
67
|
+
friendly: (msg) => {
|
|
68
|
+
const verb = msg.match(/cannot (\w+)/)?.[1] ?? 'perform this action on'
|
|
69
|
+
return {
|
|
70
|
+
summary: `Permission denied`,
|
|
71
|
+
suggestion: `Your cluster role doesn't allow you to ${verb} this resource. Contact your cluster admin for access.`,
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
// Already exists (create mode)
|
|
76
|
+
{
|
|
77
|
+
test: (msg) => /already exists/i.test(msg),
|
|
78
|
+
friendly: () => ({
|
|
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.',
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
// Quota exceeded
|
|
84
|
+
{
|
|
85
|
+
test: (msg) => /exceeded quota|quota.*exceeded|forbidden.*quota/i.test(msg),
|
|
86
|
+
friendly: () => ({
|
|
87
|
+
summary: 'Resource quota exceeded',
|
|
88
|
+
suggestion: 'The namespace has reached its resource quota limit. Request a quota increase or reduce resource requests.',
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
// Namespace not found
|
|
92
|
+
{
|
|
93
|
+
test: (msg) => /namespace.*not found|namespaces.*not found/i.test(msg),
|
|
94
|
+
friendly: (msg) => {
|
|
95
|
+
const ns = msg.match(/namespace[s]? "([^"]+)"/)?.[1] ?? ''
|
|
96
|
+
return {
|
|
97
|
+
summary: `Namespace "${ns}" not found`,
|
|
98
|
+
suggestion: 'Create the namespace first or change the namespace in your YAML.',
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
// Immutable field
|
|
103
|
+
{
|
|
104
|
+
test: (msg) => /field is immutable/i.test(msg),
|
|
105
|
+
friendly: () => ({
|
|
106
|
+
summary: 'Cannot change immutable field',
|
|
107
|
+
suggestion: 'Some fields can\'t be modified after creation. Delete and recreate the resource, or remove the immutable field change.',
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
// Connection refused / cluster unreachable
|
|
111
|
+
{
|
|
112
|
+
test: (msg) => /connection refused|no such host|cluster.*unreachable/i.test(msg),
|
|
113
|
+
friendly: () => ({
|
|
114
|
+
summary: 'Cannot reach the cluster',
|
|
115
|
+
suggestion: 'The Kubernetes API server is unreachable. Check your cluster connection.',
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
// Strip the "failed to apply resource: " / "failed to create resource: " prefix our backend adds
|
|
121
|
+
function stripPrefix(msg: string): string {
|
|
122
|
+
return msg.replace(/^failed to (apply|create) resource:\s*/i, '')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getFriendlyError(rawError: string): { summary: string; suggestion: string; raw: string } | null {
|
|
126
|
+
const cleaned = stripPrefix(rawError)
|
|
127
|
+
for (const pattern of patterns) {
|
|
128
|
+
if (pattern.test(cleaned)) {
|
|
129
|
+
const { summary, suggestion } = pattern.friendly(cleaned)
|
|
130
|
+
return { summary, suggestion, raw: cleaned }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
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)
|
|
139
|
+
if (friendly) return friendly
|
|
140
|
+
const cleaned = stripPrefix(rawError)
|
|
141
|
+
return { summary: cleaned, raw: rawError }
|
|
142
|
+
}
|
|
@@ -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
|
+
}
|