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/__init__.py +87 -0
- k8s_helper/cli.py +526 -0
- k8s_helper/config.py +204 -0
- k8s_helper/core.py +511 -0
- k8s_helper/utils.py +301 -0
- k8s_helper_cli-0.1.0.dist-info/METADATA +491 -0
- k8s_helper_cli-0.1.0.dist-info/RECORD +11 -0
- k8s_helper_cli-0.1.0.dist-info/WHEEL +5 -0
- k8s_helper_cli-0.1.0.dist-info/entry_points.txt +2 -0
- k8s_helper_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- k8s_helper_cli-0.1.0.dist-info/top_level.txt +1 -0
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
|