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.
@@ -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)