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/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