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/core.py
ADDED
@@ -0,0 +1,511 @@
|
|
1
|
+
from kubernetes import client, config
|
2
|
+
from kubernetes.client.rest import ApiException
|
3
|
+
from typing import Dict, List, Optional, Any
|
4
|
+
import yaml
|
5
|
+
|
6
|
+
|
7
|
+
class K8sClient:
|
8
|
+
def __init__(self, namespace="default"):
|
9
|
+
try:
|
10
|
+
config.load_kube_config() # Loads from ~/.kube/config
|
11
|
+
except:
|
12
|
+
config.load_incluster_config() # For running inside a cluster
|
13
|
+
|
14
|
+
self.namespace = namespace
|
15
|
+
self.apps_v1 = client.AppsV1Api()
|
16
|
+
self.core_v1 = client.CoreV1Api()
|
17
|
+
|
18
|
+
# ======================
|
19
|
+
# DEPLOYMENT OPERATIONS
|
20
|
+
# ======================
|
21
|
+
def create_deployment(self, name: str, image: str, replicas: int = 1,
|
22
|
+
container_port: int = 80, env_vars: Optional[Dict[str, str]] = None,
|
23
|
+
labels: Optional[Dict[str, str]] = None) -> Optional[Any]:
|
24
|
+
"""Create a Kubernetes deployment"""
|
25
|
+
if labels is None:
|
26
|
+
labels = {"app": name}
|
27
|
+
|
28
|
+
# Environment variables
|
29
|
+
env = []
|
30
|
+
if env_vars:
|
31
|
+
env = [client.V1EnvVar(name=k, value=v) for k, v in env_vars.items()]
|
32
|
+
|
33
|
+
container = client.V1Container(
|
34
|
+
name=name,
|
35
|
+
image=image,
|
36
|
+
ports=[client.V1ContainerPort(container_port=container_port)],
|
37
|
+
env=env if env else None
|
38
|
+
)
|
39
|
+
|
40
|
+
template = client.V1PodTemplateSpec(
|
41
|
+
metadata=client.V1ObjectMeta(labels=labels),
|
42
|
+
spec=client.V1PodSpec(containers=[container])
|
43
|
+
)
|
44
|
+
|
45
|
+
spec = client.V1DeploymentSpec(
|
46
|
+
replicas=replicas,
|
47
|
+
template=template,
|
48
|
+
selector=client.V1LabelSelector(match_labels=labels)
|
49
|
+
)
|
50
|
+
|
51
|
+
deployment = client.V1Deployment(
|
52
|
+
metadata=client.V1ObjectMeta(name=name, labels=labels),
|
53
|
+
spec=spec
|
54
|
+
)
|
55
|
+
|
56
|
+
try:
|
57
|
+
resp = self.apps_v1.create_namespaced_deployment(
|
58
|
+
body=deployment,
|
59
|
+
namespace=self.namespace
|
60
|
+
)
|
61
|
+
print(f"✅ Deployment '{name}' created successfully")
|
62
|
+
return resp
|
63
|
+
except ApiException as e:
|
64
|
+
print(f"❌ Error creating deployment '{name}': {e}")
|
65
|
+
return None
|
66
|
+
|
67
|
+
def delete_deployment(self, name: str) -> bool:
|
68
|
+
"""Delete a Kubernetes deployment"""
|
69
|
+
try:
|
70
|
+
self.apps_v1.delete_namespaced_deployment(
|
71
|
+
name=name,
|
72
|
+
namespace=self.namespace
|
73
|
+
)
|
74
|
+
print(f"✅ Deployment '{name}' deleted successfully")
|
75
|
+
return True
|
76
|
+
except ApiException as e:
|
77
|
+
print(f"❌ Error deleting deployment '{name}': {e}")
|
78
|
+
return False
|
79
|
+
|
80
|
+
def scale_deployment(self, name: str, replicas: int) -> bool:
|
81
|
+
"""Scale a deployment to the specified number of replicas"""
|
82
|
+
try:
|
83
|
+
# Get current deployment
|
84
|
+
deployment = self.apps_v1.read_namespaced_deployment(
|
85
|
+
name=name,
|
86
|
+
namespace=self.namespace
|
87
|
+
)
|
88
|
+
|
89
|
+
# Update replicas
|
90
|
+
deployment.spec.replicas = replicas
|
91
|
+
|
92
|
+
# Apply the update
|
93
|
+
self.apps_v1.patch_namespaced_deployment(
|
94
|
+
name=name,
|
95
|
+
namespace=self.namespace,
|
96
|
+
body=deployment
|
97
|
+
)
|
98
|
+
print(f"✅ Deployment '{name}' scaled to {replicas} replicas")
|
99
|
+
return True
|
100
|
+
except ApiException as e:
|
101
|
+
print(f"❌ Error scaling deployment '{name}': {e}")
|
102
|
+
return False
|
103
|
+
|
104
|
+
def list_deployments(self) -> List[Dict[str, Any]]:
|
105
|
+
"""List all deployments in the namespace"""
|
106
|
+
try:
|
107
|
+
deployments = self.apps_v1.list_namespaced_deployment(namespace=self.namespace)
|
108
|
+
result = []
|
109
|
+
for deployment in deployments.items:
|
110
|
+
result.append({
|
111
|
+
'name': deployment.metadata.name,
|
112
|
+
'replicas': deployment.spec.replicas,
|
113
|
+
'ready_replicas': deployment.status.ready_replicas or 0,
|
114
|
+
'available_replicas': deployment.status.available_replicas or 0,
|
115
|
+
'created': deployment.metadata.creation_timestamp
|
116
|
+
})
|
117
|
+
return result
|
118
|
+
except ApiException as e:
|
119
|
+
print(f"❌ Error listing deployments: {e}")
|
120
|
+
return []
|
121
|
+
|
122
|
+
# ======================
|
123
|
+
# POD OPERATIONS
|
124
|
+
# ======================
|
125
|
+
def create_pod(self, name: str, image: str, container_port: int = 80,
|
126
|
+
env_vars: Optional[Dict[str, str]] = None,
|
127
|
+
labels: Optional[Dict[str, str]] = None) -> Optional[Any]:
|
128
|
+
"""Create a simple pod"""
|
129
|
+
if labels is None:
|
130
|
+
labels = {"app": name}
|
131
|
+
|
132
|
+
# Environment variables
|
133
|
+
env = []
|
134
|
+
if env_vars:
|
135
|
+
env = [client.V1EnvVar(name=k, value=v) for k, v in env_vars.items()]
|
136
|
+
|
137
|
+
container = client.V1Container(
|
138
|
+
name=name,
|
139
|
+
image=image,
|
140
|
+
ports=[client.V1ContainerPort(container_port=container_port)],
|
141
|
+
env=env if env else None
|
142
|
+
)
|
143
|
+
|
144
|
+
pod = client.V1Pod(
|
145
|
+
metadata=client.V1ObjectMeta(name=name, labels=labels),
|
146
|
+
spec=client.V1PodSpec(containers=[container])
|
147
|
+
)
|
148
|
+
|
149
|
+
try:
|
150
|
+
resp = self.core_v1.create_namespaced_pod(
|
151
|
+
body=pod,
|
152
|
+
namespace=self.namespace
|
153
|
+
)
|
154
|
+
print(f"✅ Pod '{name}' created successfully")
|
155
|
+
return resp
|
156
|
+
except ApiException as e:
|
157
|
+
print(f"❌ Error creating pod '{name}': {e}")
|
158
|
+
return None
|
159
|
+
|
160
|
+
def delete_pod(self, name: str) -> bool:
|
161
|
+
"""Delete a pod"""
|
162
|
+
try:
|
163
|
+
self.core_v1.delete_namespaced_pod(
|
164
|
+
name=name,
|
165
|
+
namespace=self.namespace
|
166
|
+
)
|
167
|
+
print(f"✅ Pod '{name}' deleted successfully")
|
168
|
+
return True
|
169
|
+
except ApiException as e:
|
170
|
+
print(f"❌ Error deleting pod '{name}': {e}")
|
171
|
+
return False
|
172
|
+
|
173
|
+
def list_pods(self) -> List[Dict[str, Any]]:
|
174
|
+
"""List all pods in the namespace"""
|
175
|
+
try:
|
176
|
+
pods = self.core_v1.list_namespaced_pod(namespace=self.namespace)
|
177
|
+
result = []
|
178
|
+
for pod in pods.items:
|
179
|
+
result.append({
|
180
|
+
'name': pod.metadata.name,
|
181
|
+
'phase': pod.status.phase,
|
182
|
+
'ready': self._is_pod_ready(pod),
|
183
|
+
'restarts': self._get_pod_restarts(pod),
|
184
|
+
'age': pod.metadata.creation_timestamp,
|
185
|
+
'node': pod.spec.node_name
|
186
|
+
})
|
187
|
+
return result
|
188
|
+
except ApiException as e:
|
189
|
+
print(f"❌ Error listing pods: {e}")
|
190
|
+
return []
|
191
|
+
|
192
|
+
def _is_pod_ready(self, pod) -> bool:
|
193
|
+
"""Check if a pod is ready"""
|
194
|
+
if pod.status.conditions:
|
195
|
+
for condition in pod.status.conditions:
|
196
|
+
if condition.type == "Ready":
|
197
|
+
return condition.status == "True"
|
198
|
+
return False
|
199
|
+
|
200
|
+
def _get_pod_restarts(self, pod) -> int:
|
201
|
+
"""Get the number of restarts for a pod"""
|
202
|
+
if pod.status.container_statuses:
|
203
|
+
return sum(container.restart_count for container in pod.status.container_statuses)
|
204
|
+
return 0
|
205
|
+
|
206
|
+
def get_logs(self, pod_name: str, container_name: Optional[str] = None,
|
207
|
+
tail_lines: Optional[int] = None) -> Optional[str]:
|
208
|
+
"""Get logs from a pod"""
|
209
|
+
try:
|
210
|
+
kwargs = {
|
211
|
+
'name': pod_name,
|
212
|
+
'namespace': self.namespace
|
213
|
+
}
|
214
|
+
if container_name:
|
215
|
+
kwargs['container'] = container_name
|
216
|
+
if tail_lines:
|
217
|
+
kwargs['tail_lines'] = tail_lines
|
218
|
+
|
219
|
+
logs = self.core_v1.read_namespaced_pod_log(**kwargs)
|
220
|
+
print(f"📄 Logs from pod '{pod_name}':")
|
221
|
+
print(logs)
|
222
|
+
return logs
|
223
|
+
except ApiException as e:
|
224
|
+
print(f"❌ Error fetching logs from pod '{pod_name}': {e}")
|
225
|
+
return None
|
226
|
+
|
227
|
+
# ======================
|
228
|
+
# SERVICE OPERATIONS
|
229
|
+
# ======================
|
230
|
+
def create_service(self, name: str, port: int, target_port: int,
|
231
|
+
service_type: str = "ClusterIP",
|
232
|
+
selector: Optional[Dict[str, str]] = None) -> Optional[Any]:
|
233
|
+
"""Create a Kubernetes service"""
|
234
|
+
if selector is None:
|
235
|
+
selector = {"app": name}
|
236
|
+
|
237
|
+
service = client.V1Service(
|
238
|
+
metadata=client.V1ObjectMeta(name=name),
|
239
|
+
spec=client.V1ServiceSpec(
|
240
|
+
selector=selector,
|
241
|
+
ports=[client.V1ServicePort(
|
242
|
+
port=port,
|
243
|
+
target_port=target_port
|
244
|
+
)],
|
245
|
+
type=service_type
|
246
|
+
)
|
247
|
+
)
|
248
|
+
|
249
|
+
try:
|
250
|
+
resp = self.core_v1.create_namespaced_service(
|
251
|
+
body=service,
|
252
|
+
namespace=self.namespace
|
253
|
+
)
|
254
|
+
print(f"✅ Service '{name}' created successfully")
|
255
|
+
return resp
|
256
|
+
except ApiException as e:
|
257
|
+
print(f"❌ Error creating service '{name}': {e}")
|
258
|
+
return None
|
259
|
+
|
260
|
+
def delete_service(self, name: str) -> bool:
|
261
|
+
"""Delete a service"""
|
262
|
+
try:
|
263
|
+
self.core_v1.delete_namespaced_service(
|
264
|
+
name=name,
|
265
|
+
namespace=self.namespace
|
266
|
+
)
|
267
|
+
print(f"✅ Service '{name}' deleted successfully")
|
268
|
+
return True
|
269
|
+
except ApiException as e:
|
270
|
+
print(f"❌ Error deleting service '{name}': {e}")
|
271
|
+
return False
|
272
|
+
|
273
|
+
def list_services(self) -> List[Dict[str, Any]]:
|
274
|
+
"""List all services in the namespace"""
|
275
|
+
try:
|
276
|
+
services = self.core_v1.list_namespaced_service(namespace=self.namespace)
|
277
|
+
result = []
|
278
|
+
for service in services.items:
|
279
|
+
result.append({
|
280
|
+
'name': service.metadata.name,
|
281
|
+
'type': service.spec.type,
|
282
|
+
'cluster_ip': service.spec.cluster_ip,
|
283
|
+
'external_ip': service.status.load_balancer.ingress[0].ip if (
|
284
|
+
service.status.load_balancer and
|
285
|
+
service.status.load_balancer.ingress
|
286
|
+
) else None,
|
287
|
+
'ports': [{'port': port.port, 'target_port': port.target_port}
|
288
|
+
for port in service.spec.ports],
|
289
|
+
'created': service.metadata.creation_timestamp
|
290
|
+
})
|
291
|
+
return result
|
292
|
+
except ApiException as e:
|
293
|
+
print(f"❌ Error listing services: {e}")
|
294
|
+
return []
|
295
|
+
|
296
|
+
# ======================
|
297
|
+
# EVENTS AND MONITORING
|
298
|
+
# ======================
|
299
|
+
def get_events(self, resource_name: Optional[str] = None) -> List[Dict[str, Any]]:
|
300
|
+
"""Get events from the namespace, optionally filtered by resource name"""
|
301
|
+
try:
|
302
|
+
events = self.core_v1.list_namespaced_event(namespace=self.namespace)
|
303
|
+
result = []
|
304
|
+
|
305
|
+
for event in events.items:
|
306
|
+
if resource_name and event.involved_object.name != resource_name:
|
307
|
+
continue
|
308
|
+
|
309
|
+
result.append({
|
310
|
+
'name': event.metadata.name,
|
311
|
+
'type': event.type,
|
312
|
+
'reason': event.reason,
|
313
|
+
'message': event.message,
|
314
|
+
'resource': f"{event.involved_object.kind}/{event.involved_object.name}",
|
315
|
+
'first_timestamp': event.first_timestamp,
|
316
|
+
'last_timestamp': event.last_timestamp,
|
317
|
+
'count': event.count
|
318
|
+
})
|
319
|
+
|
320
|
+
return sorted(result, key=lambda x: x['last_timestamp'] or x['first_timestamp'], reverse=True)
|
321
|
+
except ApiException as e:
|
322
|
+
print(f"❌ Error fetching events: {e}")
|
323
|
+
return []
|
324
|
+
|
325
|
+
# ======================
|
326
|
+
# RESOURCE DESCRIPTION
|
327
|
+
# ======================
|
328
|
+
def describe_pod(self, name: str) -> Optional[Dict[str, Any]]:
|
329
|
+
"""Get detailed information about a pod"""
|
330
|
+
try:
|
331
|
+
pod = self.core_v1.read_namespaced_pod(name=name, namespace=self.namespace)
|
332
|
+
|
333
|
+
return {
|
334
|
+
'metadata': {
|
335
|
+
'name': pod.metadata.name,
|
336
|
+
'namespace': pod.metadata.namespace,
|
337
|
+
'labels': pod.metadata.labels,
|
338
|
+
'annotations': pod.metadata.annotations,
|
339
|
+
'creation_timestamp': pod.metadata.creation_timestamp
|
340
|
+
},
|
341
|
+
'spec': {
|
342
|
+
'containers': [
|
343
|
+
{
|
344
|
+
'name': container.name,
|
345
|
+
'image': container.image,
|
346
|
+
'ports': [{'container_port': port.container_port} for port in container.ports] if container.ports else [],
|
347
|
+
'env': [{'name': env.name, 'value': env.value} for env in container.env] if container.env else []
|
348
|
+
}
|
349
|
+
for container in pod.spec.containers
|
350
|
+
],
|
351
|
+
'restart_policy': pod.spec.restart_policy,
|
352
|
+
'node_name': pod.spec.node_name
|
353
|
+
},
|
354
|
+
'status': {
|
355
|
+
'phase': pod.status.phase,
|
356
|
+
'conditions': [
|
357
|
+
{
|
358
|
+
'type': condition.type,
|
359
|
+
'status': condition.status,
|
360
|
+
'reason': condition.reason,
|
361
|
+
'message': condition.message
|
362
|
+
}
|
363
|
+
for condition in pod.status.conditions
|
364
|
+
] if pod.status.conditions else [],
|
365
|
+
'container_statuses': [
|
366
|
+
{
|
367
|
+
'name': status.name,
|
368
|
+
'ready': status.ready,
|
369
|
+
'restart_count': status.restart_count,
|
370
|
+
'state': str(status.state)
|
371
|
+
}
|
372
|
+
for status in pod.status.container_statuses
|
373
|
+
] if pod.status.container_statuses else []
|
374
|
+
}
|
375
|
+
}
|
376
|
+
except ApiException as e:
|
377
|
+
print(f"❌ Error describing pod '{name}': {e}")
|
378
|
+
return None
|
379
|
+
|
380
|
+
def describe_deployment(self, name: str) -> Optional[Dict[str, Any]]:
|
381
|
+
"""Get detailed information about a deployment"""
|
382
|
+
try:
|
383
|
+
deployment = self.apps_v1.read_namespaced_deployment(name=name, namespace=self.namespace)
|
384
|
+
|
385
|
+
return {
|
386
|
+
'metadata': {
|
387
|
+
'name': deployment.metadata.name,
|
388
|
+
'namespace': deployment.metadata.namespace,
|
389
|
+
'labels': deployment.metadata.labels,
|
390
|
+
'annotations': deployment.metadata.annotations,
|
391
|
+
'creation_timestamp': deployment.metadata.creation_timestamp
|
392
|
+
},
|
393
|
+
'spec': {
|
394
|
+
'replicas': deployment.spec.replicas,
|
395
|
+
'selector': deployment.spec.selector.match_labels,
|
396
|
+
'template': {
|
397
|
+
'metadata': {
|
398
|
+
'labels': deployment.spec.template.metadata.labels
|
399
|
+
},
|
400
|
+
'spec': {
|
401
|
+
'containers': [
|
402
|
+
{
|
403
|
+
'name': container.name,
|
404
|
+
'image': container.image,
|
405
|
+
'ports': [{'container_port': port.container_port} for port in container.ports] if container.ports else []
|
406
|
+
}
|
407
|
+
for container in deployment.spec.template.spec.containers
|
408
|
+
]
|
409
|
+
}
|
410
|
+
}
|
411
|
+
},
|
412
|
+
'status': {
|
413
|
+
'replicas': deployment.status.replicas,
|
414
|
+
'ready_replicas': deployment.status.ready_replicas,
|
415
|
+
'available_replicas': deployment.status.available_replicas,
|
416
|
+
'unavailable_replicas': deployment.status.unavailable_replicas,
|
417
|
+
'conditions': [
|
418
|
+
{
|
419
|
+
'type': condition.type,
|
420
|
+
'status': condition.status,
|
421
|
+
'reason': condition.reason,
|
422
|
+
'message': condition.message
|
423
|
+
}
|
424
|
+
for condition in deployment.status.conditions
|
425
|
+
] if deployment.status.conditions else []
|
426
|
+
}
|
427
|
+
}
|
428
|
+
except ApiException as e:
|
429
|
+
print(f"❌ Error describing deployment '{name}': {e}")
|
430
|
+
return None
|
431
|
+
|
432
|
+
def describe_service(self, name: str) -> Optional[Dict[str, Any]]:
|
433
|
+
"""Get detailed information about a service"""
|
434
|
+
try:
|
435
|
+
service = self.core_v1.read_namespaced_service(name=name, namespace=self.namespace)
|
436
|
+
|
437
|
+
return {
|
438
|
+
'metadata': {
|
439
|
+
'name': service.metadata.name,
|
440
|
+
'namespace': service.metadata.namespace,
|
441
|
+
'labels': service.metadata.labels,
|
442
|
+
'annotations': service.metadata.annotations,
|
443
|
+
'creation_timestamp': service.metadata.creation_timestamp
|
444
|
+
},
|
445
|
+
'spec': {
|
446
|
+
'type': service.spec.type,
|
447
|
+
'selector': service.spec.selector,
|
448
|
+
'ports': [
|
449
|
+
{
|
450
|
+
'port': port.port,
|
451
|
+
'target_port': port.target_port,
|
452
|
+
'protocol': port.protocol
|
453
|
+
}
|
454
|
+
for port in service.spec.ports
|
455
|
+
],
|
456
|
+
'cluster_ip': service.spec.cluster_ip
|
457
|
+
},
|
458
|
+
'status': {
|
459
|
+
'load_balancer': {
|
460
|
+
'ingress': [
|
461
|
+
{'ip': ingress.ip, 'hostname': ingress.hostname}
|
462
|
+
for ingress in service.status.load_balancer.ingress
|
463
|
+
] if service.status.load_balancer and service.status.load_balancer.ingress else []
|
464
|
+
}
|
465
|
+
}
|
466
|
+
}
|
467
|
+
except ApiException as e:
|
468
|
+
print(f"❌ Error describing service '{name}': {e}")
|
469
|
+
return None
|
470
|
+
|
471
|
+
# ======================
|
472
|
+
# UTILITY METHODS
|
473
|
+
# ======================
|
474
|
+
def get_namespace_resources(self) -> Dict[str, int]:
|
475
|
+
"""Get a summary of resources in the namespace"""
|
476
|
+
try:
|
477
|
+
pods = len(self.core_v1.list_namespaced_pod(namespace=self.namespace).items)
|
478
|
+
deployments = len(self.apps_v1.list_namespaced_deployment(namespace=self.namespace).items)
|
479
|
+
services = len(self.core_v1.list_namespaced_service(namespace=self.namespace).items)
|
480
|
+
|
481
|
+
return {
|
482
|
+
'pods': pods,
|
483
|
+
'deployments': deployments,
|
484
|
+
'services': services
|
485
|
+
}
|
486
|
+
except ApiException as e:
|
487
|
+
print(f"❌ Error getting namespace resources: {e}")
|
488
|
+
return {}
|
489
|
+
|
490
|
+
def wait_for_deployment_ready(self, name: str, timeout: int = 300) -> bool:
|
491
|
+
"""Wait for a deployment to be ready"""
|
492
|
+
import time
|
493
|
+
start_time = time.time()
|
494
|
+
|
495
|
+
while time.time() - start_time < timeout:
|
496
|
+
try:
|
497
|
+
deployment = self.apps_v1.read_namespaced_deployment(name=name, namespace=self.namespace)
|
498
|
+
if (deployment.status.ready_replicas == deployment.spec.replicas and
|
499
|
+
deployment.status.ready_replicas > 0):
|
500
|
+
print(f"✅ Deployment '{name}' is ready")
|
501
|
+
return True
|
502
|
+
|
503
|
+
print(f"⏳ Waiting for deployment '{name}' to be ready... ({deployment.status.ready_replicas or 0}/{deployment.spec.replicas})")
|
504
|
+
time.sleep(5)
|
505
|
+
|
506
|
+
except ApiException as e:
|
507
|
+
print(f"❌ Error checking deployment status: {e}")
|
508
|
+
return False
|
509
|
+
|
510
|
+
print(f"❌ Timeout waiting for deployment '{name}' to be ready")
|
511
|
+
return False
|