Duibonduil commited on
Commit
6bb09f6
·
verified ·
1 Parent(s): 6b584d6

Upload 4 files

Browse files
aworld/sandbox/env_client/kubernetes/client.py ADDED
@@ -0,0 +1,676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ General Kubernetes API Client Example
6
+ Can be used to perform various Kubernetes API operations
7
+ """
8
+ import logging
9
+ import os
10
+
11
+ import yaml
12
+ from dotenv import load_dotenv
13
+ from kubernetes import client, config
14
+ from kubernetes.client import V1DeleteOptions
15
+ from kubernetes.client.rest import ApiException
16
+
17
+
18
+ class KubernetesApiClient:
19
+ """Kubernetes API Client Wrapper Class"""
20
+
21
+ def __init__(self, kubeconfig_path=None, context=None, in_cluster=False):
22
+ """
23
+ Initialize Kubernetes API Client
24
+
25
+ Args:
26
+ kubeconfig_path: kubeconfig file path, defaults to None which uses ~/.kube/config
27
+ context: kubeconfig context name to use
28
+ in_cluster: whether running inside a cluster, if True use service account configuration
29
+ """
30
+ try:
31
+ # Use absolute path relative to the script file for KUBECONFIG_PATH
32
+ load_dotenv()
33
+ script_dir = os.path.dirname(os.path.abspath(__file__))
34
+ script_path = os.path.join(script_dir, "kubeconfig")
35
+ kubeconfig_path = kubeconfig_path or os.getenv("KUBECONFIG_PATH") or script_path
36
+ if in_cluster:
37
+ config.load_incluster_config()
38
+ else:
39
+ config.load_kube_config(
40
+ config_file=kubeconfig_path,
41
+ context=context
42
+ )
43
+
44
+ # Initialize various API clients
45
+ self.core_v1 = client.CoreV1Api()
46
+ self.apps_v1 = client.AppsV1Api()
47
+ self.batch_v1 = client.BatchV1Api()
48
+ self.networking_v1 = client.NetworkingV1Api()
49
+ self.rbac_v1 = client.RbacAuthorizationV1Api()
50
+ self.custom_objects = client.CustomObjectsApi()
51
+
52
+ logging.info("Kubernetes API client initialized successfully")
53
+ except Exception as e:
54
+ logging.info(f"Failed to initialize Kubernetes API client: {e}")
55
+ raise
56
+
57
+ # ===================== Pod Operations =====================
58
+
59
+ def get_pod(self, name, namespace="default"):
60
+ """
61
+ Get a specific Pod in the given namespace
62
+ Equivalent to: GET /api/v1/namespaces/{namespace}/pods/{name}
63
+ """
64
+ try:
65
+ return self.core_v1.read_namespaced_pod(
66
+ name=name,
67
+ namespace=namespace
68
+ )
69
+ except ApiException as e:
70
+ logging.warning(f"Failed to get Pod {namespace}/{name}: {e}")
71
+ return None
72
+
73
+ def list_pods(self, namespace="default", label_selector=None, field_selector=None):
74
+ """
75
+ List all Pods in the given namespace
76
+ Equivalent to: GET /api/v1/namespaces/{namespace}/pods
77
+ """
78
+ try:
79
+ return self.core_v1.list_namespaced_pod(
80
+ namespace=namespace,
81
+ label_selector=label_selector,
82
+ field_selector=field_selector
83
+ )
84
+ except ApiException as e:
85
+ logging.warning(f"Failed to list Pods in namespace {namespace}: {e}")
86
+ return None
87
+
88
+ def list_pods_all_namespaces(self, label_selector=None, field_selector=None):
89
+ """
90
+ List Pods across all namespaces
91
+ Equivalent to: GET /api/v1/pods
92
+ """
93
+ try:
94
+ return self.core_v1.list_pod_for_all_namespaces(
95
+ label_selector=label_selector,
96
+ field_selector=field_selector
97
+ )
98
+ except ApiException as e:
99
+ logging.warning(f"Failed to list Pods across all namespaces: {e}")
100
+ return None
101
+
102
+ def create_pod(self, pod_manifest, namespace="default"):
103
+ """
104
+ Create a Pod
105
+ Equivalent to: POST /api/v1/namespaces/{namespace}/pods
106
+
107
+ Args:
108
+ pod_manifest: Pod resource definition, can be dict or V1Pod object
109
+ namespace: Namespace where the Pod will be created
110
+
111
+ Returns:
112
+ V1Pod: The created Pod object on success
113
+ None: On failure
114
+ """
115
+ try:
116
+ # If input is a dictionary, use it directly
117
+ if isinstance(pod_manifest, dict):
118
+ # Using dictionary definition
119
+ return self.core_v1.create_namespaced_pod(
120
+ namespace=namespace,
121
+ body=pod_manifest
122
+ )
123
+ else:
124
+ # Using V1Pod object directly
125
+ return self.core_v1.create_namespaced_pod(
126
+ namespace=namespace,
127
+ body=pod_manifest
128
+ )
129
+ except ApiException as e:
130
+ logging.warning(f"Failed to create Pod: {e}")
131
+ return None
132
+
133
+ def create_pod_from_yaml(self, yaml_file=None, namespace=None, pod_name=None):
134
+ """
135
+ Create a Pod from YAML file
136
+
137
+ Args:
138
+ yaml_file: YAML file path
139
+ namespace: Namespace where the Pod will be created
140
+ pod_name: Override the Pod name in the YAML
141
+
142
+ Returns:
143
+ V1Pod: The created Pod object on success
144
+ None: On failure
145
+ """
146
+ try:
147
+ load_dotenv()
148
+ script_dir = os.path.dirname(os.path.abspath(__file__))
149
+ pod_path = os.path.join(script_dir, "pod.yaml")
150
+ yaml_file = yaml_file or os.getenv("POD_YAML_PATH") or pod_path
151
+ namespace = namespace or os.getenv("POD_NAMESPACE") or "default"
152
+ with open(yaml_file, 'r') as f:
153
+ pod_manifest = yaml.safe_load(f)
154
+ # Update Pod name if provided
155
+ if pod_name:
156
+ if 'metadata' in pod_manifest:
157
+ pod_manifest['metadata']['name'] = pod_name
158
+ if 'labels' in pod_manifest['metadata']:
159
+ pod_manifest['metadata']['labels']['name'] = pod_name
160
+ if 'spec' in pod_manifest and 'containers' in pod_manifest['spec'] and pod_manifest['spec'][
161
+ 'containers']:
162
+ pod_manifest['spec']['containers'][0]['name'] = pod_name
163
+
164
+ return self.create_pod(pod_manifest, namespace)
165
+ except Exception as e:
166
+ logging.info(f"Failed to create Pod from YAML file: {e}")
167
+ return None
168
+
169
+ def delete_pod(self, name, namespace="default", grace_period_seconds=30):
170
+ """
171
+ Delete a Pod
172
+ Equivalent to: DELETE /api/v1/namespaces/{namespace}/pods/{name}
173
+
174
+ Args:
175
+ name: Pod name
176
+ namespace: Namespace where the Pod is located
177
+ grace_period_seconds: Grace period in seconds
178
+
179
+ Returns:
180
+ V1Status: Status object on successful deletion
181
+ None: On failure
182
+ """
183
+ try:
184
+ return self.core_v1.delete_namespaced_pod(
185
+ name=name,
186
+ namespace=namespace,
187
+ body=V1DeleteOptions(
188
+ grace_period_seconds=grace_period_seconds,
189
+ propagation_policy="Background"
190
+ )
191
+ )
192
+ except ApiException as e:
193
+ logging.warning(f"Failed to delete Pod {namespace}/{name}: {e}")
194
+ return None
195
+
196
+ def update_pod(self, name, pod_manifest, namespace="default"):
197
+ """
198
+ Update a Pod
199
+ Equivalent to: PUT /api/v1/namespaces/{namespace}/pods/{name}
200
+
201
+ Args:
202
+ name: Pod name
203
+ pod_manifest: Pod resource definition, can be dict or V1Pod object
204
+ namespace: Namespace where the Pod is located
205
+
206
+ Returns:
207
+ V1Pod: The updated Pod object on success
208
+ None: On failure
209
+ """
210
+ try:
211
+ # If input is a dictionary, ensure name and namespace fields
212
+ if isinstance(pod_manifest, dict):
213
+ if 'metadata' not in pod_manifest:
214
+ pod_manifest['metadata'] = {}
215
+ pod_manifest['metadata']['name'] = name
216
+ pod_manifest['metadata']['namespace'] = namespace
217
+
218
+ return self.core_v1.replace_namespaced_pod(
219
+ name=name,
220
+ namespace=namespace,
221
+ body=pod_manifest
222
+ )
223
+ else:
224
+ # Ensure V1Pod object has correct name and namespace
225
+ pod_manifest.metadata.name = name
226
+ pod_manifest.metadata.namespace = namespace
227
+
228
+ return self.core_v1.replace_namespaced_pod(
229
+ name=name,
230
+ namespace=namespace,
231
+ body=pod_manifest
232
+ )
233
+ except ApiException as e:
234
+ logging.warning(f"Failed to update Pod {namespace}/{name}: {e}")
235
+ return None
236
+
237
+ def patch_pod(self, name, patch_data, namespace="default"):
238
+ """
239
+ Partially update a Pod
240
+ Equivalent to: PATCH /api/v1/namespaces/{namespace}/pods/{name}
241
+
242
+ Args:
243
+ name: Pod name
244
+ patch_data: Data to update, in dictionary format
245
+ namespace: Namespace where the Pod is located
246
+
247
+ Returns:
248
+ V1Pod: The updated Pod object on success
249
+ None: On failure
250
+ """
251
+ try:
252
+ return self.core_v1.patch_namespaced_pod(
253
+ name=name,
254
+ namespace=namespace,
255
+ body=patch_data
256
+ )
257
+ except ApiException as e:
258
+ logging.warning(f"Failed to patch Pod {namespace}/{name}: {e}")
259
+ return None
260
+
261
+ def get_pod_info(self, name, namespace="default"):
262
+ """
263
+ Get basic information about a Pod
264
+
265
+ Args:
266
+ name: Pod name
267
+ namespace: Namespace where the Pod is located
268
+
269
+ Returns:
270
+ dict: Dictionary containing basic Pod information including status, IP, start time, etc.
271
+ None: On failure
272
+ """
273
+ try:
274
+ pod = self.get_pod(name, namespace)
275
+ if not pod:
276
+ return None
277
+
278
+ # Format start time for readability
279
+ start_time = None
280
+ if pod.status.start_time:
281
+ # Convert time to readable format (ISO format: YYYY-MM-DD HH:MM:SS)
282
+ start_time_obj = pod.status.start_time.replace(tzinfo=None)
283
+ start_time = start_time_obj.strftime('%Y-%m-%d %H:%M:%S')
284
+
285
+ pod_info = {
286
+ "pod_name": pod.metadata.name,
287
+ "namespace": pod.metadata.namespace,
288
+ "status": pod.status.phase, # Pending, Running, Succeeded, Failed, Unknown
289
+ "pod_ip": pod.status.pod_ip,
290
+ "host_ip": pod.status.host_ip,
291
+ "start_time": start_time,
292
+ "node_name": pod.spec.node_name if hasattr(pod.spec, "node_name") else None
293
+ }
294
+
295
+ return pod_info
296
+ except Exception as e:
297
+ logging.warning(f"Failed to get Pod information for {namespace}/{name}: {e}")
298
+ return None
299
+
300
+ # ===================== Deployment Operations =====================
301
+
302
+ def get_deployment(self, name, namespace="default"):
303
+ """
304
+ Get a specific Deployment
305
+ Equivalent to: GET /apis/apps/v1/namespaces/{namespace}/deployments/{name}
306
+ """
307
+ try:
308
+ return self.apps_v1.read_namespaced_deployment(
309
+ name=name,
310
+ namespace=namespace
311
+ )
312
+ except ApiException as e:
313
+ logging.warning(f"Failed to get Deployment {namespace}/{name}: {e}")
314
+ return None
315
+
316
+ def list_deployments(self, namespace="default", label_selector=None):
317
+ """
318
+ List all Deployments in the given namespace
319
+ Equivalent to: GET /apis/apps/v1/namespaces/{namespace}/deployments
320
+ """
321
+ try:
322
+ return self.apps_v1.list_namespaced_deployment(
323
+ namespace=namespace,
324
+ label_selector=label_selector
325
+ )
326
+ except ApiException as e:
327
+ logging.warning(f"Failed to list Deployments in namespace {namespace}: {e}")
328
+ return None
329
+
330
+ # ===================== Service Operations =====================
331
+
332
+ def get_service(self, name, namespace="default"):
333
+ """
334
+ Get a specific Service
335
+ Equivalent to: GET /api/v1/namespaces/{namespace}/services/{name}
336
+ """
337
+ try:
338
+ return self.core_v1.read_namespaced_service(
339
+ name=name,
340
+ namespace=namespace
341
+ )
342
+ except ApiException as e:
343
+ logging.warning(f"Failed to get Service {namespace}/{name}: {e}")
344
+ return None
345
+
346
+ def list_services(self, namespace="default", label_selector=None):
347
+ """
348
+ List all Services in the given namespace
349
+ Equivalent to: GET /api/v1/namespaces/{namespace}/services
350
+ """
351
+ try:
352
+ return self.core_v1.list_namespaced_service(
353
+ namespace=namespace,
354
+ label_selector=label_selector
355
+ )
356
+ except ApiException as e:
357
+ logging.warning(f"Failed to list Services in namespace {namespace}: {e}")
358
+ return None
359
+
360
+ def create_service(self, service_manifest, namespace="default"):
361
+ """
362
+ Create a Service
363
+ Equivalent to: POST /api/v1/namespaces/{namespace}/services
364
+
365
+ Args:
366
+ service_manifest: Service resource definition, can be dict or V1Service object
367
+ namespace: Namespace where the Service will be created
368
+
369
+ Returns:
370
+ V1Service: The created Service object on success
371
+ None: On failure
372
+ """
373
+ try:
374
+ # If input is a dictionary, use it directly
375
+ if isinstance(service_manifest, dict):
376
+ # Using dictionary definition
377
+ return self.core_v1.create_namespaced_service(
378
+ namespace=namespace,
379
+ body=service_manifest
380
+ )
381
+ else:
382
+ # Using V1Service object directly
383
+ return self.core_v1.create_namespaced_service(
384
+ namespace=namespace,
385
+ body=service_manifest
386
+ )
387
+ except ApiException as e:
388
+ logging.warning(f"Failed to create Service: {e}")
389
+ return None
390
+
391
+ def create_service_from_yaml(self, yaml_file=None, namespace=None, service_name=None, selector_name=None):
392
+ """
393
+ Create a Service from YAML file
394
+
395
+ Args:
396
+ yaml_file: YAML file path
397
+ namespace: Namespace where the Service will be created
398
+ service_name: Override the Service name in the YAML
399
+ selector_name: Override the selector name for pod targeting
400
+
401
+ Returns:
402
+ V1Service: The created Service object on success
403
+ None: On failure
404
+ """
405
+ try:
406
+ load_dotenv()
407
+ script_dir = os.path.dirname(os.path.abspath(__file__))
408
+ service_path = os.path.join(script_dir, "service.yaml")
409
+ yaml_file = yaml_file or os.getenv("SERVICE_YAML_PATH") or service_path
410
+ namespace = namespace or os.getenv("SERVICE_NAMESPACE") or "default"
411
+ with open(yaml_file, 'r') as f:
412
+ service_manifest = yaml.safe_load(f)
413
+
414
+ # Update Service name if provided
415
+ if service_name:
416
+ if 'metadata' in service_manifest:
417
+ service_manifest['metadata']['name'] = service_name
418
+ # Update app label if present
419
+ if 'labels' in service_manifest['metadata']:
420
+ service_manifest['metadata']['labels']['app'] = service_name
421
+
422
+ # Update selector if provided
423
+ if selector_name:
424
+ if 'spec' in service_manifest and 'selector' in service_manifest['spec']:
425
+ service_manifest['spec']['selector']['name'] = selector_name
426
+
427
+ return self.create_service(service_manifest, namespace)
428
+ except Exception as e:
429
+ logging.info(f"Failed to create Service from YAML file: {e}")
430
+ return None
431
+
432
+ def update_service(self, name, service_manifest, namespace="default"):
433
+ """
434
+ Update a Service
435
+ Equivalent to: PUT /api/v1/namespaces/{namespace}/services/{name}
436
+
437
+ Args:
438
+ name: Service name
439
+ service_manifest: Service resource definition, can be dict or V1Service object
440
+ namespace: Namespace where the Service is located
441
+
442
+ Returns:
443
+ V1Service: The updated Service object on success
444
+ None: On failure
445
+ """
446
+ try:
447
+ # If input is a dictionary, ensure name and namespace fields
448
+ if isinstance(service_manifest, dict):
449
+ if 'metadata' not in service_manifest:
450
+ service_manifest['metadata'] = {}
451
+ service_manifest['metadata']['name'] = name
452
+ service_manifest['metadata']['namespace'] = namespace
453
+
454
+ return self.core_v1.replace_namespaced_service(
455
+ name=name,
456
+ namespace=namespace,
457
+ body=service_manifest
458
+ )
459
+ else:
460
+ # Ensure V1Service object has correct name and namespace
461
+ service_manifest.metadata.name = name
462
+ service_manifest.metadata.namespace = namespace
463
+
464
+ return self.core_v1.replace_namespaced_service(
465
+ name=name,
466
+ namespace=namespace,
467
+ body=service_manifest
468
+ )
469
+ except ApiException as e:
470
+ logging.warning(f"Failed to update Service {namespace}/{name}: {e}")
471
+ return None
472
+
473
+ def patch_service(self, name, patch_data, namespace="default"):
474
+ """
475
+ Partially update a Service
476
+ Equivalent to: PATCH /api/v1/namespaces/{namespace}/services/{name}
477
+
478
+ Args:
479
+ name: Service name
480
+ patch_data: Data to update, in dictionary format
481
+ namespace: Namespace where the Service is located
482
+
483
+ Returns:
484
+ V1Service: The updated Service object on success
485
+ None: On failure
486
+ """
487
+ try:
488
+ return self.core_v1.patch_namespaced_service(
489
+ name=name,
490
+ namespace=namespace,
491
+ body=patch_data
492
+ )
493
+ except ApiException as e:
494
+ logging.warning(f"Failed to patch Service {namespace}/{name}: {e}")
495
+ return None
496
+
497
+ def delete_service(self, name, namespace="default"):
498
+ """
499
+ Delete a Service
500
+ Equivalent to: DELETE /api/v1/namespaces/{namespace}/services/{name}
501
+
502
+ Args:
503
+ name: Service name
504
+ namespace: Namespace where the Service is located
505
+
506
+ Returns:
507
+ V1Status: Status object on successful deletion
508
+ None: On failure
509
+ """
510
+ try:
511
+ return self.core_v1.delete_namespaced_service(
512
+ name=name,
513
+ namespace=namespace
514
+ )
515
+ except ApiException as e:
516
+ logging.warning(f"Failed to delete Service {namespace}/{name}: {e}")
517
+ return None
518
+
519
+ def get_service_info(self, name, namespace="default"):
520
+ """
521
+ Get basic information about a Service
522
+
523
+ Args:
524
+ name: Service name
525
+ namespace: Namespace where the Service is located
526
+
527
+ Returns:
528
+ dict: Dictionary containing basic Service information including type, IP, ports, etc.
529
+ None: On failure
530
+ """
531
+ try:
532
+ service = self.get_service(name, namespace)
533
+ if not service:
534
+ return None
535
+
536
+ # Format creation time for readability
537
+ creation_time = None
538
+ if service.metadata.creation_timestamp:
539
+ creation_time_obj = service.metadata.creation_timestamp.replace(tzinfo=None)
540
+ creation_time = creation_time_obj.strftime('%Y-%m-%d %H:%M:%S')
541
+
542
+ # Simplify port information
543
+ ports_info = []
544
+ if service.spec.ports:
545
+ for port in service.spec.ports:
546
+ port_info = {
547
+ "port": port.port,
548
+ "target_port": port.target_port,
549
+ "protocol": port.protocol
550
+ }
551
+
552
+ # Add node port if present for NodePort type
553
+ if hasattr(port, "node_port") and port.node_port:
554
+ port_info["node_port"] = port.node_port
555
+
556
+ ports_info.append(port_info)
557
+
558
+ service_info = {
559
+ "service_name": service.metadata.name,
560
+ "namespace": service.metadata.namespace,
561
+ "type": service.spec.type, # ClusterIP, NodePort, LoadBalancer, ExternalName
562
+ "cluster_ip": service.spec.cluster_ip,
563
+ "creation_time": creation_time,
564
+ "ports": ports_info,
565
+ "selector": service.spec.selector,
566
+ "host": '' # Default to use cluster_ip as host
567
+ }
568
+
569
+ # Add external IP information for LoadBalancer type
570
+ if service.spec.type == "LoadBalancer" and hasattr(service.status,
571
+ "load_balancer") and service.status.load_balancer:
572
+ external_ips = []
573
+ if hasattr(service.status.load_balancer, "ingress") and service.status.load_balancer.ingress:
574
+ for ingress in service.status.load_balancer.ingress:
575
+ if hasattr(ingress, "ip") and ingress.ip:
576
+ external_ips.append(ingress.ip)
577
+ elif hasattr(ingress, "hostname") and ingress.hostname:
578
+ external_ips.append(ingress.hostname)
579
+
580
+ # If there are external IPs or hostnames, use the first one as the host field
581
+ if external_ips:
582
+ service_info["host"] = external_ips[0]
583
+
584
+ service_info["external_ips"] = external_ips
585
+
586
+ # Add external name for ExternalName type
587
+ if service.spec.type == "ExternalName" and hasattr(service.spec, "external_name"):
588
+ service_info["external_name"] = service.spec.external_name
589
+ service_info["host"] = service.spec.external_name # For ExternalName type, use external_name as host
590
+
591
+ return service_info
592
+ except Exception as e:
593
+ print(f"Failed to get Service information for {namespace}/{name}: {e}")
594
+ return None
595
+
596
+ # ===================== Namespace Operations =====================
597
+
598
+ def get_namespace(self, name):
599
+ """
600
+ Get a specific namespace
601
+ Equivalent to: GET /api/v1/namespaces/{name}
602
+ """
603
+ try:
604
+ return self.core_v1.read_namespace(name=name)
605
+ except ApiException as e:
606
+ logging.warning(f"Failed to get namespace {name}: {e}")
607
+ return None
608
+
609
+ def list_namespaces(self, label_selector=None):
610
+ """
611
+ List all namespaces
612
+ Equivalent to: GET /api/v1/namespaces
613
+ """
614
+ try:
615
+ return self.core_v1.list_namespace(label_selector=label_selector)
616
+ except ApiException as e:
617
+ logging.warning(f"Failed to list namespaces: {e}")
618
+ return None
619
+
620
+ # ===================== Custom Resource (CRD) Operations =====================
621
+
622
+ def get_custom_resource(self, group, version, plural, name, namespace=None):
623
+ """
624
+ Get a custom resource
625
+
626
+ For namespaced resources:
627
+ GET /apis/{group}/{version}/namespaces/{namespace}/{plural}/{name}
628
+
629
+ For cluster-scoped resources:
630
+ GET /apis/{group}/{version}/{plural}/{name}
631
+ """
632
+ try:
633
+ if namespace:
634
+ return self.custom_objects.get_namespaced_custom_object(
635
+ group=group,
636
+ version=version,
637
+ namespace=namespace,
638
+ plural=plural,
639
+ name=name
640
+ )
641
+ else:
642
+ return self.custom_objects.get_cluster_custom_object(
643
+ group=group,
644
+ version=version,
645
+ plural=plural,
646
+ name=name
647
+ )
648
+ except ApiException as e:
649
+ resource_path = f"{namespace}/{name}" if namespace else name
650
+ logging.warning(f"Failed to get custom resource {group}/{version}/{plural}/{resource_path}: {e}")
651
+ return None
652
+
653
+
654
+ if __name__ == "__main__":
655
+ client = KubernetesApiClient("./kubeconfig")
656
+ # result = client.get_pod("mcp-openapi-node-2", namespace="default")
657
+ # print(result)
658
+ #result = client.create_pod_from_yaml("./pod.yaml")
659
+
660
+ # result = client.get_pod_info("mcp-openapi-node-5", namespace="default")
661
+ # print(result)
662
+ result = client.get_service_info("mcp-openapi-service-1", namespace="default")
663
+ print(result)
664
+
665
+ # result = client.create_pod_from_yaml("./pod.yaml")
666
+ # print(result)
667
+
668
+ # result = client.create_service_from_yaml("./service.yaml")
669
+ # print(result)
670
+
671
+ # result = client.delete_pod("mcp-openapi-node-1")
672
+ # print(result)
673
+
674
+ # result = client.delete_service("mcp-openapi-service-2")
675
+ # print(result)
676
+
aworld/sandbox/env_client/kubernetes/kubeconfig ADDED
File without changes
aworld/sandbox/env_client/kubernetes/pod.yaml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: Pod
3
+ metadata:
4
+ labels:
5
+ name: mcp-openapi-node-5
6
+ name: mcp-openapi-node-5
7
+ spec:
8
+ # serviceAccountName: user1 # specify specific sevice account for pod creation
9
+ # automountServiceAccountToken: true # mount token for api access inside pod/container
10
+ imagePullSecrets: #Comment out to enable specific image pull secret
11
+ - name: mcp-openapi # repleace it to specific registry key
12
+ containers:
13
+ - image: crpi-5emlza767l7em5xz-vpc.ap-southeast-1.personal.cr.aliyuncs.com/aworld_x/mcp-openapi:0.1.5
14
+ imagePullPolicy: IfNotPresent
15
+ name: mcp-openapi-node-5
16
+ ports:
17
+ - containerPort: 9090
18
+ protocol: TCP
19
+ resources: {}
20
+ securityContext:
21
+ capabilities: {}
22
+ privileged: false
23
+ terminationMessagePath: /dev/termination-log
24
+ dnsPolicy: ClusterFirst
25
+ restartPolicy: Always
26
+ # nodeSelector:
27
+ # env: test-team
aworld/sandbox/env_client/kubernetes/service.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: mcp-openapi-service-3 #TODO: to specify your service name
5
+ labels:
6
+ app: mcp-openapi-service-3
7
+ spec:
8
+ selector:
9
+ name: mcp-openapi
10
+ #name: mcp-openapi-node-1 #TODO: change label selector to match your backend pod
11
+ ports:
12
+ - protocol: TCP
13
+ name: http
14
+ port: 80 #TODO: choose an unique port on each node to avoid port conflict
15
+ targetPort: 9090
16
+ type: LoadBalancer
17
+ #type: ClusterIP
18
+ # type: LoadBalancer