fixdoc 0.0.1__py3-none-any.whl
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.
- fixdoc/__init__.py +8 -0
- fixdoc/cli.py +26 -0
- fixdoc/commands/__init__.py +11 -0
- fixdoc/commands/analyze.py +313 -0
- fixdoc/commands/capture.py +109 -0
- fixdoc/commands/capture_handlers.py +298 -0
- fixdoc/commands/delete.py +72 -0
- fixdoc/commands/edit.py +118 -0
- fixdoc/commands/manage.py +67 -0
- fixdoc/commands/search.py +65 -0
- fixdoc/commands/sync.py +268 -0
- fixdoc/config.py +113 -0
- fixdoc/fix.py +19 -0
- fixdoc/formatter.py +62 -0
- fixdoc/git.py +263 -0
- fixdoc/markdown_parser.py +106 -0
- fixdoc/models.py +83 -0
- fixdoc/parsers/__init__.py +24 -0
- fixdoc/parsers/base.py +131 -0
- fixdoc/parsers/kubernetes.py +584 -0
- fixdoc/parsers/router.py +160 -0
- fixdoc/parsers/terraform.py +409 -0
- fixdoc/storage.py +146 -0
- fixdoc/sync_engine.py +330 -0
- fixdoc/terraform_parser.py +135 -0
- fixdoc-0.0.1.dist-info/METADATA +261 -0
- fixdoc-0.0.1.dist-info/RECORD +30 -0
- fixdoc-0.0.1.dist-info/WHEEL +5 -0
- fixdoc-0.0.1.dist-info/entry_points.txt +2 -0
- fixdoc-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kubernetes error parser for kubectl and Helm.
|
|
3
|
+
|
|
4
|
+
Supports parsing errors from:
|
|
5
|
+
- kubectl apply/create/delete
|
|
6
|
+
- kubectl describe (events)
|
|
7
|
+
- kubectl logs
|
|
8
|
+
- Helm install/upgrade/rollback
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from .base import ParsedError, ErrorParser, CloudProvider, ErrorSeverity
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KubernetesErrorType(Enum):
|
|
20
|
+
"""Types of Kubernetes errors."""
|
|
21
|
+
KUBECTL_APPLY = "kubectl_apply"
|
|
22
|
+
KUBECTL_CREATE = "kubectl_create"
|
|
23
|
+
POD_STATUS = "pod_status"
|
|
24
|
+
POD_EVENT = "pod_event"
|
|
25
|
+
HELM_INSTALL = "helm_install"
|
|
26
|
+
HELM_UPGRADE = "helm_upgrade"
|
|
27
|
+
HELM_TEMPLATE = "helm_template"
|
|
28
|
+
RESOURCE_QUOTA = "resource_quota"
|
|
29
|
+
SCHEDULING = "scheduling"
|
|
30
|
+
UNKNOWN = "unknown"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Common Kubernetes error patterns
|
|
34
|
+
K8S_STATUS_ERRORS = {
|
|
35
|
+
'ImagePullBackOff': {
|
|
36
|
+
'severity': ErrorSeverity.ERROR,
|
|
37
|
+
'suggestions': [
|
|
38
|
+
"Verify the image name and tag exist in the registry",
|
|
39
|
+
"Check imagePullSecrets are configured correctly",
|
|
40
|
+
"Ensure the registry is accessible from the cluster",
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
'ErrImagePull': {
|
|
44
|
+
'severity': ErrorSeverity.ERROR,
|
|
45
|
+
'suggestions': [
|
|
46
|
+
"Check if the image exists in the registry",
|
|
47
|
+
"Verify registry credentials in imagePullSecrets",
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
'CrashLoopBackOff': {
|
|
51
|
+
'severity': ErrorSeverity.CRITICAL,
|
|
52
|
+
'suggestions': [
|
|
53
|
+
"Check container logs: kubectl logs <pod-name>",
|
|
54
|
+
"Verify environment variables and config",
|
|
55
|
+
"Check if the application has proper health checks",
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
'OOMKilled': {
|
|
59
|
+
'severity': ErrorSeverity.CRITICAL,
|
|
60
|
+
'suggestions': [
|
|
61
|
+
"Increase memory limits in the pod spec",
|
|
62
|
+
"Check for memory leaks in the application",
|
|
63
|
+
"Review memory requests vs actual usage",
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
'CreateContainerConfigError': {
|
|
67
|
+
'severity': ErrorSeverity.ERROR,
|
|
68
|
+
'suggestions': [
|
|
69
|
+
"Check if referenced ConfigMaps exist",
|
|
70
|
+
"Check if referenced Secrets exist",
|
|
71
|
+
"Verify volume mount configurations",
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
'Pending': {
|
|
75
|
+
'severity': ErrorSeverity.WARNING,
|
|
76
|
+
'suggestions': [
|
|
77
|
+
"Check node resources and scheduling constraints",
|
|
78
|
+
"Verify PersistentVolumeClaims are bound",
|
|
79
|
+
"Check node affinity and taints/tolerations",
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
'FailedScheduling': {
|
|
83
|
+
'severity': ErrorSeverity.ERROR,
|
|
84
|
+
'suggestions': [
|
|
85
|
+
"Check node resources (CPU, memory)",
|
|
86
|
+
"Verify node selectors and affinity rules",
|
|
87
|
+
"Check for taints on nodes",
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
'Unhealthy': {
|
|
91
|
+
'severity': ErrorSeverity.WARNING,
|
|
92
|
+
'suggestions': [
|
|
93
|
+
"Check readiness/liveness probe configuration",
|
|
94
|
+
"Verify the application endpoint is responding",
|
|
95
|
+
"Review probe timeout and threshold settings",
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Helm-specific error patterns
|
|
101
|
+
HELM_ERROR_PATTERNS = {
|
|
102
|
+
'cannot re-use a name': {
|
|
103
|
+
'error_code': 'ReleaseExists',
|
|
104
|
+
'suggestions': [
|
|
105
|
+
"Use 'helm upgrade' instead of 'helm install'",
|
|
106
|
+
"Or use '--replace' flag to replace the existing release",
|
|
107
|
+
"Or delete the existing release first: helm uninstall <release>",
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
'chart.*not found': {
|
|
111
|
+
'error_code': 'ChartNotFound',
|
|
112
|
+
'suggestions': [
|
|
113
|
+
"Run 'helm repo update' to refresh chart repositories",
|
|
114
|
+
"Verify the chart name and repository",
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
'timed out waiting': {
|
|
118
|
+
'error_code': 'Timeout',
|
|
119
|
+
'suggestions': [
|
|
120
|
+
"Increase timeout with --timeout flag",
|
|
121
|
+
"Check pod status for scheduling or image issues",
|
|
122
|
+
"Review pod events for failure reasons",
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
'UPGRADE FAILED.*has no deployed releases': {
|
|
126
|
+
'error_code': 'NoDeployedReleases',
|
|
127
|
+
'suggestions': [
|
|
128
|
+
"Use 'helm install' for new releases",
|
|
129
|
+
"Or use 'helm upgrade --install' to install if not present",
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
'failed pre-install|failed post-install': {
|
|
133
|
+
'error_code': 'HookFailed',
|
|
134
|
+
'suggestions': [
|
|
135
|
+
"Check hook job logs for failure details",
|
|
136
|
+
"Verify hook resources have correct permissions",
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
'exceeded quota': {
|
|
140
|
+
'error_code': 'QuotaExceeded',
|
|
141
|
+
'suggestions': [
|
|
142
|
+
"Request increased quotas from cluster admin",
|
|
143
|
+
"Reduce resource requests in values",
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
'RBAC|forbidden|cannot create': {
|
|
147
|
+
'error_code': 'RBACDenied',
|
|
148
|
+
'suggestions': [
|
|
149
|
+
"Check RBAC permissions for the service account",
|
|
150
|
+
"Ensure ClusterRole/Role bindings are correct",
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class KubernetesError(ParsedError):
|
|
158
|
+
"""Kubernetes-specific error with additional context."""
|
|
159
|
+
|
|
160
|
+
k8s_error_type: KubernetesErrorType = KubernetesErrorType.UNKNOWN
|
|
161
|
+
pod_name: Optional[str] = None
|
|
162
|
+
container_name: Optional[str] = None
|
|
163
|
+
exit_code: Optional[int] = None
|
|
164
|
+
restart_count: Optional[int] = None
|
|
165
|
+
helm_release: Optional[str] = None
|
|
166
|
+
helm_chart: Optional[str] = None
|
|
167
|
+
|
|
168
|
+
def __post_init__(self):
|
|
169
|
+
self.error_type = "kubernetes"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class KubernetesParser(ErrorParser):
|
|
173
|
+
"""Parser for Kubernetes (kubectl/Helm) errors."""
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def name(self) -> str:
|
|
177
|
+
return "kubernetes"
|
|
178
|
+
|
|
179
|
+
def can_parse(self, text: str) -> bool:
|
|
180
|
+
"""Check if text looks like Kubernetes/Helm output."""
|
|
181
|
+
indicators = [
|
|
182
|
+
r'kubectl\s+(apply|create|delete|get|describe)',
|
|
183
|
+
r'helm\s+(install|upgrade|rollback|template)',
|
|
184
|
+
r'Error from server',
|
|
185
|
+
r'error when creating',
|
|
186
|
+
r'INSTALLATION FAILED',
|
|
187
|
+
r'UPGRADE FAILED',
|
|
188
|
+
r'ImagePullBackOff',
|
|
189
|
+
r'CrashLoopBackOff',
|
|
190
|
+
r'OOMKilled',
|
|
191
|
+
r'CreateContainerConfigError',
|
|
192
|
+
r'FailedScheduling',
|
|
193
|
+
r'pod/[a-z0-9-]+',
|
|
194
|
+
r'deployment\.apps/',
|
|
195
|
+
r'service/',
|
|
196
|
+
r'namespace:.*\s+\w+',
|
|
197
|
+
r'kubectl.*-n\s+\w+',
|
|
198
|
+
r'\.yaml.*error',
|
|
199
|
+
]
|
|
200
|
+
return any(re.search(pattern, text, re.IGNORECASE) for pattern in indicators)
|
|
201
|
+
|
|
202
|
+
def parse(self, text: str) -> list[KubernetesError]:
|
|
203
|
+
"""Parse Kubernetes output for all errors."""
|
|
204
|
+
errors = []
|
|
205
|
+
|
|
206
|
+
# Try to parse as Helm error first
|
|
207
|
+
if self._is_helm_output(text):
|
|
208
|
+
helm_errors = self._parse_helm_output(text)
|
|
209
|
+
errors.extend(helm_errors)
|
|
210
|
+
|
|
211
|
+
# Parse kubectl errors
|
|
212
|
+
kubectl_errors = self._parse_kubectl_output(text)
|
|
213
|
+
errors.extend(kubectl_errors)
|
|
214
|
+
|
|
215
|
+
# Parse pod status errors
|
|
216
|
+
status_errors = self._parse_pod_status(text)
|
|
217
|
+
errors.extend(status_errors)
|
|
218
|
+
|
|
219
|
+
# Deduplicate
|
|
220
|
+
seen = set()
|
|
221
|
+
unique = []
|
|
222
|
+
for e in errors:
|
|
223
|
+
key = f"{e.error_code}:{e.resource_name}:{e.error_message[:50]}"
|
|
224
|
+
if key not in seen:
|
|
225
|
+
seen.add(key)
|
|
226
|
+
unique.append(e)
|
|
227
|
+
|
|
228
|
+
return unique if unique else [self.parse_single(text)] if self.parse_single(text) else []
|
|
229
|
+
|
|
230
|
+
def parse_single(self, text: str) -> Optional[KubernetesError]:
|
|
231
|
+
"""Parse a single error from the text."""
|
|
232
|
+
# Try Helm first
|
|
233
|
+
if self._is_helm_output(text):
|
|
234
|
+
helm_errors = self._parse_helm_output(text)
|
|
235
|
+
if helm_errors:
|
|
236
|
+
return helm_errors[0]
|
|
237
|
+
|
|
238
|
+
# Try kubectl apply errors
|
|
239
|
+
kubectl_errors = self._parse_kubectl_output(text)
|
|
240
|
+
if kubectl_errors:
|
|
241
|
+
return kubectl_errors[0]
|
|
242
|
+
|
|
243
|
+
# Try pod status
|
|
244
|
+
status_errors = self._parse_pod_status(text)
|
|
245
|
+
if status_errors:
|
|
246
|
+
return status_errors[0]
|
|
247
|
+
|
|
248
|
+
# Generic fallback
|
|
249
|
+
return self._parse_generic_k8s_error(text)
|
|
250
|
+
|
|
251
|
+
def _is_helm_output(self, text: str) -> bool:
|
|
252
|
+
"""Check if text is Helm output."""
|
|
253
|
+
helm_patterns = [
|
|
254
|
+
r'helm\s+(install|upgrade|rollback)',
|
|
255
|
+
r'INSTALLATION FAILED',
|
|
256
|
+
r'UPGRADE FAILED',
|
|
257
|
+
r'ROLLBACK FAILED',
|
|
258
|
+
r'Error:\s+chart\s+',
|
|
259
|
+
r'release\s+"[^"]+"\s+',
|
|
260
|
+
]
|
|
261
|
+
return any(re.search(p, text, re.IGNORECASE) for p in helm_patterns)
|
|
262
|
+
|
|
263
|
+
def _parse_helm_output(self, text: str) -> list[KubernetesError]:
|
|
264
|
+
"""Parse Helm-specific errors."""
|
|
265
|
+
errors = []
|
|
266
|
+
|
|
267
|
+
# Extract release name
|
|
268
|
+
release_match = re.search(r'release\s+"([^"]+)"', text, re.IGNORECASE)
|
|
269
|
+
release_name = release_match.group(1) if release_match else None
|
|
270
|
+
|
|
271
|
+
# Extract chart name
|
|
272
|
+
chart_match = re.search(r'chart\s+"([^"]+)"', text, re.IGNORECASE)
|
|
273
|
+
chart_name = chart_match.group(1) if chart_match else None
|
|
274
|
+
|
|
275
|
+
# Determine error type
|
|
276
|
+
if 'INSTALLATION FAILED' in text:
|
|
277
|
+
k8s_type = KubernetesErrorType.HELM_INSTALL
|
|
278
|
+
elif 'UPGRADE FAILED' in text:
|
|
279
|
+
k8s_type = KubernetesErrorType.HELM_UPGRADE
|
|
280
|
+
else:
|
|
281
|
+
k8s_type = KubernetesErrorType.UNKNOWN
|
|
282
|
+
|
|
283
|
+
# Extract main error message
|
|
284
|
+
error_match = re.search(
|
|
285
|
+
r'Error:\s*(.+?)(?=\n\n|\n[A-Z]|\Z)',
|
|
286
|
+
text,
|
|
287
|
+
re.DOTALL
|
|
288
|
+
)
|
|
289
|
+
error_message = error_match.group(1).strip() if error_match else "Helm operation failed"
|
|
290
|
+
error_message = re.sub(r'\s+', ' ', error_message)[:500]
|
|
291
|
+
|
|
292
|
+
# Determine error code and suggestions
|
|
293
|
+
error_code = None
|
|
294
|
+
suggestions = []
|
|
295
|
+
for pattern, info in HELM_ERROR_PATTERNS.items():
|
|
296
|
+
if re.search(pattern, text, re.IGNORECASE):
|
|
297
|
+
error_code = info['error_code']
|
|
298
|
+
suggestions = info['suggestions']
|
|
299
|
+
break
|
|
300
|
+
|
|
301
|
+
# Check for underlying Kubernetes errors
|
|
302
|
+
underlying_match = re.search(
|
|
303
|
+
r'(pods?|deployments?|services?|configmaps?|secrets?)\s+"?([^":\s]+)"?\s+is\s+(\w+)',
|
|
304
|
+
text,
|
|
305
|
+
re.IGNORECASE
|
|
306
|
+
)
|
|
307
|
+
resource_type = None
|
|
308
|
+
resource_name = None
|
|
309
|
+
if underlying_match:
|
|
310
|
+
resource_type = underlying_match.group(1).lower()
|
|
311
|
+
resource_name = underlying_match.group(2)
|
|
312
|
+
|
|
313
|
+
# Extract namespace
|
|
314
|
+
namespace_match = re.search(r'-n\s+(\S+)|namespace[=:\s]+(\S+)', text, re.IGNORECASE)
|
|
315
|
+
namespace = namespace_match.group(1) or namespace_match.group(2) if namespace_match else None
|
|
316
|
+
|
|
317
|
+
errors.append(KubernetesError(
|
|
318
|
+
error_type="kubernetes",
|
|
319
|
+
error_message=error_message,
|
|
320
|
+
raw_output=text,
|
|
321
|
+
resource_type=resource_type or "helm_release",
|
|
322
|
+
resource_name=resource_name or release_name,
|
|
323
|
+
namespace=namespace,
|
|
324
|
+
error_code=error_code,
|
|
325
|
+
severity=ErrorSeverity.ERROR,
|
|
326
|
+
suggestions=suggestions,
|
|
327
|
+
tags=['kubernetes', 'helm', k8s_type.value],
|
|
328
|
+
k8s_error_type=k8s_type,
|
|
329
|
+
helm_release=release_name,
|
|
330
|
+
helm_chart=chart_name,
|
|
331
|
+
))
|
|
332
|
+
|
|
333
|
+
return errors
|
|
334
|
+
|
|
335
|
+
def _parse_kubectl_output(self, text: str) -> list[KubernetesError]:
|
|
336
|
+
"""Parse kubectl apply/create errors."""
|
|
337
|
+
errors = []
|
|
338
|
+
|
|
339
|
+
# Pattern for "Error from server" style errors
|
|
340
|
+
server_errors = re.findall(
|
|
341
|
+
r'Error from server\s*\((\w+)\):\s*(.+?)(?=Error from server|\Z)',
|
|
342
|
+
text,
|
|
343
|
+
re.DOTALL | re.IGNORECASE
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
for error_type, message in server_errors:
|
|
347
|
+
# Extract resource info
|
|
348
|
+
resource_match = re.search(
|
|
349
|
+
r'(pods?|deployments?|services?|configmaps?|secrets?|statefulsets?)\s+"?([^":\s]+)"?',
|
|
350
|
+
message,
|
|
351
|
+
re.IGNORECASE
|
|
352
|
+
)
|
|
353
|
+
resource_type = resource_match.group(1) if resource_match else None
|
|
354
|
+
resource_name = resource_match.group(2) if resource_match else None
|
|
355
|
+
|
|
356
|
+
# Extract namespace
|
|
357
|
+
ns_match = re.search(r'namespace\s+"?([^":\s]+)"?', message, re.IGNORECASE)
|
|
358
|
+
namespace = ns_match.group(1) if ns_match else None
|
|
359
|
+
|
|
360
|
+
# Get suggestions based on error type
|
|
361
|
+
suggestions = self._get_suggestions_for_error(error_type, message)
|
|
362
|
+
|
|
363
|
+
errors.append(KubernetesError(
|
|
364
|
+
error_type="kubernetes",
|
|
365
|
+
error_message=re.sub(r'\s+', ' ', message.strip())[:500],
|
|
366
|
+
raw_output=text,
|
|
367
|
+
resource_type=resource_type,
|
|
368
|
+
resource_name=resource_name,
|
|
369
|
+
namespace=namespace,
|
|
370
|
+
error_code=error_type,
|
|
371
|
+
severity=ErrorSeverity.ERROR,
|
|
372
|
+
suggestions=suggestions,
|
|
373
|
+
tags=['kubernetes', 'kubectl', error_type.lower()],
|
|
374
|
+
k8s_error_type=KubernetesErrorType.KUBECTL_APPLY,
|
|
375
|
+
))
|
|
376
|
+
|
|
377
|
+
# Pattern for "error when creating" style errors
|
|
378
|
+
create_errors = re.findall(
|
|
379
|
+
r'error when (\w+)\s+"([^"]+)":\s*(.+?)(?=error when|\Z)',
|
|
380
|
+
text,
|
|
381
|
+
re.DOTALL | re.IGNORECASE
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
for action, file_or_resource, message in create_errors:
|
|
385
|
+
# Extract resource info from message
|
|
386
|
+
resource_match = re.search(
|
|
387
|
+
r'(\w+)\.(\w+/\w+)\s+"([^"]+)"',
|
|
388
|
+
message,
|
|
389
|
+
re.IGNORECASE
|
|
390
|
+
)
|
|
391
|
+
if resource_match:
|
|
392
|
+
resource_type = resource_match.group(2).split('/')[0]
|
|
393
|
+
resource_name = resource_match.group(3)
|
|
394
|
+
else:
|
|
395
|
+
resource_type = None
|
|
396
|
+
resource_name = None
|
|
397
|
+
|
|
398
|
+
# Extract error reason
|
|
399
|
+
reason_match = re.search(r'is (\w+):', message, re.IGNORECASE)
|
|
400
|
+
error_code = reason_match.group(1) if reason_match else None
|
|
401
|
+
|
|
402
|
+
errors.append(KubernetesError(
|
|
403
|
+
error_type="kubernetes",
|
|
404
|
+
error_message=re.sub(r'\s+', ' ', message.strip())[:500],
|
|
405
|
+
raw_output=text,
|
|
406
|
+
resource_type=resource_type,
|
|
407
|
+
resource_name=resource_name,
|
|
408
|
+
file=file_or_resource if '.yaml' in file_or_resource or '.yml' in file_or_resource else None,
|
|
409
|
+
error_code=error_code,
|
|
410
|
+
severity=ErrorSeverity.ERROR,
|
|
411
|
+
tags=['kubernetes', 'kubectl', action.lower()],
|
|
412
|
+
k8s_error_type=KubernetesErrorType.KUBECTL_APPLY,
|
|
413
|
+
))
|
|
414
|
+
|
|
415
|
+
return errors
|
|
416
|
+
|
|
417
|
+
def _parse_pod_status(self, text: str) -> list[KubernetesError]:
|
|
418
|
+
"""Parse pod status errors from kubectl get/describe output."""
|
|
419
|
+
errors = []
|
|
420
|
+
|
|
421
|
+
# Check for known status errors
|
|
422
|
+
for status, info in K8S_STATUS_ERRORS.items():
|
|
423
|
+
if status in text:
|
|
424
|
+
# Try to extract pod name
|
|
425
|
+
pod_match = re.search(
|
|
426
|
+
rf'([a-z0-9][-a-z0-9]*)\s+\d+/\d+\s+{status}',
|
|
427
|
+
text,
|
|
428
|
+
re.IGNORECASE
|
|
429
|
+
)
|
|
430
|
+
pod_name = pod_match.group(1) if pod_match else None
|
|
431
|
+
|
|
432
|
+
# Alternative: from describe output
|
|
433
|
+
if not pod_name:
|
|
434
|
+
describe_match = re.search(r'Name:\s+(\S+)', text)
|
|
435
|
+
pod_name = describe_match.group(1) if describe_match else None
|
|
436
|
+
|
|
437
|
+
# Extract namespace
|
|
438
|
+
ns_match = re.search(r'Namespace:\s+(\S+)', text)
|
|
439
|
+
namespace = ns_match.group(1) if ns_match else None
|
|
440
|
+
|
|
441
|
+
# Extract container name if present
|
|
442
|
+
container_match = re.search(r'Container:\s+(\S+)|container\s+(\S+)', text, re.IGNORECASE)
|
|
443
|
+
container_name = (container_match.group(1) or container_match.group(2)) if container_match else None
|
|
444
|
+
|
|
445
|
+
# Extract restart count
|
|
446
|
+
restart_match = re.search(r'Restart Count:\s+(\d+)|RESTARTS\s+.*?(\d+)', text)
|
|
447
|
+
restart_count = int(restart_match.group(1) or restart_match.group(2)) if restart_match else None
|
|
448
|
+
|
|
449
|
+
# Extract exit code for OOMKilled/CrashLoop
|
|
450
|
+
exit_match = re.search(r'Exit Code:\s+(\d+)', text)
|
|
451
|
+
exit_code = int(exit_match.group(1)) if exit_match else None
|
|
452
|
+
|
|
453
|
+
# Extract additional context from events
|
|
454
|
+
event_message = ""
|
|
455
|
+
event_match = re.search(
|
|
456
|
+
rf'(Warning|Error)\s+\w+\s+.*?{status}.*?$',
|
|
457
|
+
text,
|
|
458
|
+
re.MULTILINE | re.IGNORECASE
|
|
459
|
+
)
|
|
460
|
+
if event_match:
|
|
461
|
+
event_message = event_match.group(0)
|
|
462
|
+
|
|
463
|
+
error_message = f"Pod status: {status}"
|
|
464
|
+
if event_message:
|
|
465
|
+
error_message += f" - {event_message}"
|
|
466
|
+
|
|
467
|
+
errors.append(KubernetesError(
|
|
468
|
+
error_type="kubernetes",
|
|
469
|
+
error_message=error_message[:500],
|
|
470
|
+
raw_output=text,
|
|
471
|
+
resource_type="Pod",
|
|
472
|
+
resource_name=pod_name,
|
|
473
|
+
namespace=namespace,
|
|
474
|
+
error_code=status,
|
|
475
|
+
severity=info['severity'],
|
|
476
|
+
suggestions=info['suggestions'],
|
|
477
|
+
tags=['kubernetes', 'pod', status.lower()],
|
|
478
|
+
k8s_error_type=KubernetesErrorType.POD_STATUS,
|
|
479
|
+
pod_name=pod_name,
|
|
480
|
+
container_name=container_name,
|
|
481
|
+
exit_code=exit_code,
|
|
482
|
+
restart_count=restart_count,
|
|
483
|
+
))
|
|
484
|
+
|
|
485
|
+
return errors
|
|
486
|
+
|
|
487
|
+
def _parse_generic_k8s_error(self, text: str) -> Optional[KubernetesError]:
|
|
488
|
+
"""Parse a generic Kubernetes error when specific patterns don't match."""
|
|
489
|
+
# Try to find any error message
|
|
490
|
+
error_match = re.search(
|
|
491
|
+
r'(?:Error|error|ERROR)[:\s]+(.+?)(?=\n\n|\Z)',
|
|
492
|
+
text,
|
|
493
|
+
re.DOTALL
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if not error_match:
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
error_message = re.sub(r'\s+', ' ', error_match.group(1).strip())[:500]
|
|
500
|
+
|
|
501
|
+
# Try to extract resource info
|
|
502
|
+
resource_match = re.search(
|
|
503
|
+
r'(pods?|deployments?|services?|statefulsets?|daemonsets?|jobs?|cronjobs?)/([^\s:]+)',
|
|
504
|
+
text,
|
|
505
|
+
re.IGNORECASE
|
|
506
|
+
)
|
|
507
|
+
resource_type = resource_match.group(1) if resource_match else None
|
|
508
|
+
resource_name = resource_match.group(2) if resource_match else None
|
|
509
|
+
|
|
510
|
+
# Extract namespace
|
|
511
|
+
ns_match = re.search(r'-n\s+(\S+)|namespace[=:\s]+(\S+)', text, re.IGNORECASE)
|
|
512
|
+
namespace = (ns_match.group(1) or ns_match.group(2)) if ns_match else None
|
|
513
|
+
|
|
514
|
+
return KubernetesError(
|
|
515
|
+
error_type="kubernetes",
|
|
516
|
+
error_message=error_message,
|
|
517
|
+
raw_output=text,
|
|
518
|
+
resource_type=resource_type,
|
|
519
|
+
resource_name=resource_name,
|
|
520
|
+
namespace=namespace,
|
|
521
|
+
severity=ErrorSeverity.ERROR,
|
|
522
|
+
tags=['kubernetes'],
|
|
523
|
+
k8s_error_type=KubernetesErrorType.UNKNOWN,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
def _get_suggestions_for_error(self, error_type: str, message: str) -> list[str]:
|
|
527
|
+
"""Get suggestions based on error type and message."""
|
|
528
|
+
suggestions = []
|
|
529
|
+
|
|
530
|
+
error_lower = error_type.lower()
|
|
531
|
+
message_lower = message.lower()
|
|
532
|
+
|
|
533
|
+
if error_lower == 'forbidden':
|
|
534
|
+
if 'quota' in message_lower:
|
|
535
|
+
suggestions = [
|
|
536
|
+
"Request increased quotas from cluster admin",
|
|
537
|
+
"Reduce resource requests in pod spec",
|
|
538
|
+
]
|
|
539
|
+
elif 'service account' in message_lower:
|
|
540
|
+
suggestions = [
|
|
541
|
+
"Create the service account",
|
|
542
|
+
"Check serviceAccountName in pod spec",
|
|
543
|
+
]
|
|
544
|
+
else:
|
|
545
|
+
suggestions = [
|
|
546
|
+
"Check RBAC permissions",
|
|
547
|
+
"Verify namespace exists and is accessible",
|
|
548
|
+
]
|
|
549
|
+
elif error_lower == 'invalid':
|
|
550
|
+
if 'resource' in message_lower:
|
|
551
|
+
suggestions = [
|
|
552
|
+
"Check resource requests/limits values",
|
|
553
|
+
"Ensure requests <= limits",
|
|
554
|
+
]
|
|
555
|
+
else:
|
|
556
|
+
suggestions = [
|
|
557
|
+
"Validate YAML syntax",
|
|
558
|
+
"Check Kubernetes API version compatibility",
|
|
559
|
+
]
|
|
560
|
+
elif error_lower == 'conflict':
|
|
561
|
+
suggestions = [
|
|
562
|
+
"Fetch the latest version and reapply",
|
|
563
|
+
"Use server-side apply: kubectl apply --server-side",
|
|
564
|
+
]
|
|
565
|
+
elif error_lower == 'notfound':
|
|
566
|
+
suggestions = [
|
|
567
|
+
"Check if the resource exists",
|
|
568
|
+
"Verify the namespace is correct",
|
|
569
|
+
]
|
|
570
|
+
|
|
571
|
+
return suggestions
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# Convenience functions
|
|
575
|
+
def parse_kubernetes_output(output: str) -> list[KubernetesError]:
|
|
576
|
+
"""Parse Kubernetes output for all errors."""
|
|
577
|
+
parser = KubernetesParser()
|
|
578
|
+
return parser.parse(output)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def is_kubernetes_output(text: str) -> bool:
|
|
582
|
+
"""Check if text looks like Kubernetes output."""
|
|
583
|
+
parser = KubernetesParser()
|
|
584
|
+
return parser.can_parse(text)
|