k8s-helper-cli 0.1.0__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.
k8s_helper/utils.py ADDED
@@ -0,0 +1,301 @@
1
+ """
2
+ Utility functions for k8s-helper
3
+ """
4
+
5
+ from typing import Dict, List, Any, Optional
6
+ import yaml
7
+ import json
8
+ from datetime import datetime, timezone
9
+ import re
10
+
11
+
12
+ def format_age(timestamp) -> str:
13
+ """Format a timestamp to show age (e.g., '2d', '3h', '45m')"""
14
+ if not timestamp:
15
+ return "Unknown"
16
+
17
+ now = datetime.now(timezone.utc)
18
+ if hasattr(timestamp, 'replace'):
19
+ # Handle timezone-aware datetime
20
+ if timestamp.tzinfo is None:
21
+ timestamp = timestamp.replace(tzinfo=timezone.utc)
22
+
23
+ diff = now - timestamp
24
+
25
+ days = diff.days
26
+ hours, remainder = divmod(diff.seconds, 3600)
27
+ minutes, _ = divmod(remainder, 60)
28
+
29
+ if days > 0:
30
+ return f"{days}d"
31
+ elif hours > 0:
32
+ return f"{hours}h"
33
+ elif minutes > 0:
34
+ return f"{minutes}m"
35
+ else:
36
+ return "Just now"
37
+
38
+
39
+ def format_resource_table(resources: List[Dict[str, Any]], headers: List[str]) -> str:
40
+ """Format a list of resources as a table"""
41
+ if not resources:
42
+ return "No resources found"
43
+
44
+ # Calculate column widths
45
+ col_widths = {}
46
+ for header in headers:
47
+ col_widths[header] = len(header)
48
+
49
+ for resource in resources:
50
+ for header in headers:
51
+ value = str(resource.get(header, 'N/A'))
52
+ col_widths[header] = max(col_widths[header], len(value))
53
+
54
+ # Build the table
55
+ header_line = " | ".join(header.ljust(col_widths[header]) for header in headers)
56
+ separator = "-" * len(header_line)
57
+
58
+ lines = [header_line, separator]
59
+
60
+ for resource in resources:
61
+ row = " | ".join(str(resource.get(header, 'N/A')).ljust(col_widths[header]) for header in headers)
62
+ lines.append(row)
63
+
64
+ return "\n".join(lines)
65
+
66
+
67
+ def format_pod_list(pods: List[Dict[str, Any]]) -> str:
68
+ """Format pod list for display"""
69
+ if not pods:
70
+ return "No pods found"
71
+
72
+ formatted_pods = []
73
+ for pod in pods:
74
+ formatted_pod = {
75
+ 'NAME': pod['name'],
76
+ 'READY': '1/1' if pod['ready'] else '0/1',
77
+ 'STATUS': pod['phase'],
78
+ 'RESTARTS': pod['restarts'],
79
+ 'AGE': format_age(pod['age']),
80
+ 'NODE': pod.get('node', 'N/A')
81
+ }
82
+ formatted_pods.append(formatted_pod)
83
+
84
+ return format_resource_table(formatted_pods, ['NAME', 'READY', 'STATUS', 'RESTARTS', 'AGE', 'NODE'])
85
+
86
+
87
+ def format_deployment_list(deployments: List[Dict[str, Any]]) -> str:
88
+ """Format deployment list for display"""
89
+ if not deployments:
90
+ return "No deployments found"
91
+
92
+ formatted_deployments = []
93
+ for deployment in deployments:
94
+ formatted_deployment = {
95
+ 'NAME': deployment['name'],
96
+ 'READY': f"{deployment['ready_replicas']}/{deployment['replicas']}",
97
+ 'UP-TO-DATE': deployment['available_replicas'],
98
+ 'AVAILABLE': deployment['available_replicas'],
99
+ 'AGE': format_age(deployment['created'])
100
+ }
101
+ formatted_deployments.append(formatted_deployment)
102
+
103
+ return format_resource_table(formatted_deployments, ['NAME', 'READY', 'UP-TO-DATE', 'AVAILABLE', 'AGE'])
104
+
105
+
106
+ def format_service_list(services: List[Dict[str, Any]]) -> str:
107
+ """Format service list for display"""
108
+ if not services:
109
+ return "No services found"
110
+
111
+ formatted_services = []
112
+ for service in services:
113
+ ports_str = ','.join([f"{port['port']}/{port.get('protocol', 'TCP')}" for port in service['ports']])
114
+
115
+ formatted_service = {
116
+ 'NAME': service['name'],
117
+ 'TYPE': service['type'],
118
+ 'CLUSTER-IP': service['cluster_ip'],
119
+ 'EXTERNAL-IP': service['external_ip'] or '<none>',
120
+ 'PORTS': ports_str,
121
+ 'AGE': format_age(service['created'])
122
+ }
123
+ formatted_services.append(formatted_service)
124
+
125
+ return format_resource_table(formatted_services, ['NAME', 'TYPE', 'CLUSTER-IP', 'EXTERNAL-IP', 'PORTS', 'AGE'])
126
+
127
+
128
+ def format_events(events: List[Dict[str, Any]]) -> str:
129
+ """Format events for display"""
130
+ if not events:
131
+ return "No events found"
132
+
133
+ formatted_events = []
134
+ for event in events:
135
+ formatted_event = {
136
+ 'LAST SEEN': format_age(event['last_timestamp'] or event['first_timestamp']),
137
+ 'TYPE': event['type'],
138
+ 'REASON': event['reason'],
139
+ 'OBJECT': event['resource'],
140
+ 'MESSAGE': event['message'][:60] + '...' if len(event['message']) > 60 else event['message']
141
+ }
142
+ formatted_events.append(formatted_event)
143
+
144
+ return format_resource_table(formatted_events, ['LAST SEEN', 'TYPE', 'REASON', 'OBJECT', 'MESSAGE'])
145
+
146
+
147
+ def validate_name(name: str) -> bool:
148
+ """Validate Kubernetes resource name"""
149
+ # K8s names must be lowercase alphanumeric with hyphens, max 63 chars
150
+ pattern = r'^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$'
151
+ return bool(re.match(pattern, name)) and len(name) <= 63
152
+
153
+
154
+ def validate_namespace(namespace: str) -> bool:
155
+ """Validate Kubernetes namespace name"""
156
+ return validate_name(namespace)
157
+
158
+
159
+ def validate_image(image: str) -> bool:
160
+ """Basic validation for Docker image names"""
161
+ # Very basic validation - just check it's not empty and has reasonable format
162
+ return bool(image) and len(image) > 0 and ' ' not in image
163
+
164
+
165
+ def parse_env_vars(env_string: str) -> Dict[str, str]:
166
+ """Parse environment variables from a string like 'KEY1=value1,KEY2=value2'"""
167
+ env_vars = {}
168
+ if not env_string:
169
+ return env_vars
170
+
171
+ pairs = env_string.split(',')
172
+ for pair in pairs:
173
+ if '=' in pair:
174
+ key, value = pair.split('=', 1)
175
+ env_vars[key.strip()] = value.strip()
176
+
177
+ return env_vars
178
+
179
+
180
+ def parse_labels(labels_string: str) -> Dict[str, str]:
181
+ """Parse labels from a string like 'key1=value1,key2=value2'"""
182
+ return parse_env_vars(labels_string) # Same format
183
+
184
+
185
+ def safe_get(dictionary: Dict, key: str, default: Any = None) -> Any:
186
+ """Safely get a value from a dictionary with nested key support"""
187
+ try:
188
+ value = dictionary
189
+ for k in key.split('.'):
190
+ value = value[k]
191
+ return value
192
+ except (KeyError, TypeError):
193
+ return default
194
+
195
+
196
+ def format_yaml_output(data: Any) -> str:
197
+ """Format data as YAML string"""
198
+ try:
199
+ return yaml.dump(data, default_flow_style=False, indent=2)
200
+ except Exception:
201
+ return str(data)
202
+
203
+
204
+ def format_json_output(data: Any) -> str:
205
+ """Format data as JSON string"""
206
+ try:
207
+ return json.dumps(data, indent=2, default=str)
208
+ except Exception:
209
+ return str(data)
210
+
211
+
212
+ def print_status(message: str, status: str = "info"):
213
+ """Print a status message with appropriate emoji"""
214
+ emojis = {
215
+ "success": "✅",
216
+ "error": "❌",
217
+ "warning": "⚠️",
218
+ "info": "ℹ️",
219
+ "loading": "⏳"
220
+ }
221
+
222
+ emoji = emojis.get(status, "ℹ️")
223
+ print(f"{emoji} {message}")
224
+
225
+
226
+ def create_deployment_manifest(name: str, image: str, replicas: int = 1,
227
+ port: int = 80, env_vars: Optional[Dict[str, str]] = None,
228
+ labels: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
229
+ """Create a deployment manifest dictionary"""
230
+ if labels is None:
231
+ labels = {"app": name}
232
+
233
+ env_list = []
234
+ if env_vars:
235
+ env_list = [{"name": k, "value": v} for k, v in env_vars.items()]
236
+
237
+ manifest = {
238
+ "apiVersion": "apps/v1",
239
+ "kind": "Deployment",
240
+ "metadata": {
241
+ "name": name,
242
+ "labels": labels
243
+ },
244
+ "spec": {
245
+ "replicas": replicas,
246
+ "selector": {
247
+ "matchLabels": labels
248
+ },
249
+ "template": {
250
+ "metadata": {
251
+ "labels": labels
252
+ },
253
+ "spec": {
254
+ "containers": [
255
+ {
256
+ "name": name,
257
+ "image": image,
258
+ "ports": [
259
+ {
260
+ "containerPort": port
261
+ }
262
+ ]
263
+ }
264
+ ]
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ if env_list:
271
+ manifest["spec"]["template"]["spec"]["containers"][0]["env"] = env_list
272
+
273
+ return manifest
274
+
275
+
276
+ def create_service_manifest(name: str, port: int, target_port: int,
277
+ service_type: str = "ClusterIP",
278
+ selector: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
279
+ """Create a service manifest dictionary"""
280
+ if selector is None:
281
+ selector = {"app": name}
282
+
283
+ manifest = {
284
+ "apiVersion": "v1",
285
+ "kind": "Service",
286
+ "metadata": {
287
+ "name": name
288
+ },
289
+ "spec": {
290
+ "selector": selector,
291
+ "ports": [
292
+ {
293
+ "port": port,
294
+ "targetPort": target_port
295
+ }
296
+ ],
297
+ "type": service_type
298
+ }
299
+ }
300
+
301
+ return manifest