agentscope-runtime 0.1.5b1__py3-none-any.whl → 0.1.6__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.
- agentscope_runtime/engine/agents/agentscope_agent.py +447 -0
- agentscope_runtime/engine/agents/agno_agent.py +19 -18
- agentscope_runtime/engine/agents/autogen_agent.py +13 -8
- agentscope_runtime/engine/agents/utils.py +53 -0
- agentscope_runtime/engine/deployers/__init__.py +0 -13
- agentscope_runtime/engine/deployers/local_deployer.py +501 -356
- agentscope_runtime/engine/helpers/helper.py +60 -41
- agentscope_runtime/engine/runner.py +11 -36
- agentscope_runtime/engine/schemas/agent_schemas.py +2 -70
- agentscope_runtime/engine/services/sandbox_service.py +62 -70
- agentscope_runtime/engine/services/tablestore_memory_service.py +304 -0
- agentscope_runtime/engine/services/tablestore_rag_service.py +143 -0
- agentscope_runtime/engine/services/tablestore_session_history_service.py +293 -0
- agentscope_runtime/engine/services/utils/tablestore_service_utils.py +352 -0
- agentscope_runtime/sandbox/__init__.py +2 -0
- agentscope_runtime/sandbox/box/base/__init__.py +4 -0
- agentscope_runtime/sandbox/box/base/base_sandbox.py +4 -3
- agentscope_runtime/sandbox/box/browser/__init__.py +4 -0
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +8 -13
- agentscope_runtime/sandbox/box/dummy/__init__.py +4 -0
- agentscope_runtime/sandbox/box/filesystem/__init__.py +4 -0
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +8 -6
- agentscope_runtime/sandbox/box/gui/__init__.py +4 -0
- agentscope_runtime/sandbox/box/gui/gui_sandbox.py +80 -0
- agentscope_runtime/sandbox/box/sandbox.py +5 -2
- agentscope_runtime/sandbox/box/shared/routers/generic.py +20 -1
- agentscope_runtime/sandbox/box/training_box/__init__.py +4 -0
- agentscope_runtime/sandbox/box/training_box/training_box.py +10 -15
- agentscope_runtime/sandbox/build.py +143 -58
- agentscope_runtime/sandbox/client/http_client.py +43 -49
- agentscope_runtime/sandbox/client/training_client.py +0 -1
- agentscope_runtime/sandbox/constant.py +24 -1
- agentscope_runtime/sandbox/custom/custom_sandbox.py +5 -5
- agentscope_runtime/sandbox/custom/example.py +2 -2
- agentscope_runtime/sandbox/enums.py +1 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +11 -6
- agentscope_runtime/sandbox/manager/collections/redis_mapping.py +25 -9
- agentscope_runtime/sandbox/manager/container_clients/__init__.py +0 -10
- agentscope_runtime/sandbox/manager/container_clients/agentrun_client.py +1098 -0
- agentscope_runtime/sandbox/manager/container_clients/docker_client.py +33 -205
- agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +8 -555
- agentscope_runtime/sandbox/manager/sandbox_manager.py +187 -88
- agentscope_runtime/sandbox/manager/server/app.py +82 -14
- agentscope_runtime/sandbox/manager/server/config.py +50 -3
- agentscope_runtime/sandbox/model/container.py +6 -23
- agentscope_runtime/sandbox/model/manager_config.py +93 -5
- agentscope_runtime/sandbox/tools/gui/__init__.py +7 -0
- agentscope_runtime/sandbox/tools/gui/tool.py +77 -0
- agentscope_runtime/sandbox/tools/mcp_tool.py +6 -2
- agentscope_runtime/sandbox/utils.py +124 -0
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/METADATA +168 -77
- {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/RECORD +59 -78
- {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/entry_points.txt +0 -1
- agentscope_runtime/engine/agents/agentscope_agent/__init__.py +0 -6
- agentscope_runtime/engine/agents/agentscope_agent/agent.py +0 -401
- agentscope_runtime/engine/agents/agentscope_agent/hooks.py +0 -169
- agentscope_runtime/engine/agents/llm_agent.py +0 -51
- agentscope_runtime/engine/deployers/adapter/responses/response_api_adapter_utils.py +0 -2886
- agentscope_runtime/engine/deployers/adapter/responses/response_api_agent_adapter.py +0 -51
- agentscope_runtime/engine/deployers/adapter/responses/response_api_protocol_adapter.py +0 -314
- agentscope_runtime/engine/deployers/cli_fc_deploy.py +0 -143
- agentscope_runtime/engine/deployers/kubernetes_deployer.py +0 -265
- agentscope_runtime/engine/deployers/modelstudio_deployer.py +0 -626
- agentscope_runtime/engine/deployers/utils/deployment_modes.py +0 -14
- agentscope_runtime/engine/deployers/utils/docker_image_utils/__init__.py +0 -8
- agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +0 -429
- agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +0 -240
- agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +0 -297
- agentscope_runtime/engine/deployers/utils/package_project_utils.py +0 -932
- agentscope_runtime/engine/deployers/utils/service_utils/__init__.py +0 -9
- agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +0 -504
- agentscope_runtime/engine/deployers/utils/service_utils/fastapi_templates.py +0 -157
- agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +0 -268
- agentscope_runtime/engine/deployers/utils/service_utils/service_config.py +0 -75
- agentscope_runtime/engine/deployers/utils/service_utils/service_factory.py +0 -220
- agentscope_runtime/engine/deployers/utils/wheel_packager.py +0 -389
- agentscope_runtime/engine/helpers/agent_api_builder.py +0 -651
- agentscope_runtime/engine/llms/__init__.py +0 -3
- agentscope_runtime/engine/llms/base_llm.py +0 -60
- agentscope_runtime/engine/llms/qwen_llm.py +0 -47
- agentscope_runtime/engine/schemas/embedding.py +0 -37
- agentscope_runtime/engine/schemas/modelstudio_llm.py +0 -310
- agentscope_runtime/engine/schemas/oai_llm.py +0 -538
- agentscope_runtime/engine/schemas/realtime.py +0 -254
- /agentscope_runtime/engine/{deployers/adapter/responses → services/utils}/__init__.py +0 -0
- /agentscope_runtime/{engine/deployers/utils → sandbox/box/gui/box}/__init__.py +0 -0
- {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/WHEEL +0 -0
- {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# pylint: disable=too-many-branches
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import hashlib
|
|
6
|
-
import logging
|
|
2
|
+
# pylint: disable=too-many-branches
|
|
3
|
+
import os
|
|
7
4
|
import time
|
|
5
|
+
import hashlib
|
|
8
6
|
import traceback
|
|
9
|
-
|
|
7
|
+
import logging
|
|
10
8
|
|
|
11
9
|
from kubernetes import client
|
|
12
10
|
from kubernetes import config as k8s_config
|
|
@@ -16,19 +14,12 @@ from .base_client import BaseClient
|
|
|
16
14
|
|
|
17
15
|
logger = logging.getLogger(__name__)
|
|
18
16
|
|
|
19
|
-
DEFAULT_IMAGE_REGISTRY = "agentscope-registry.ap-southeast-1.cr.aliyuncs.com"
|
|
20
|
-
|
|
21
17
|
|
|
22
18
|
class KubernetesClient(BaseClient):
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
config=None,
|
|
26
|
-
image_registry: Optional[str] = DEFAULT_IMAGE_REGISTRY,
|
|
27
|
-
):
|
|
19
|
+
def __init__(self, config=None):
|
|
28
20
|
self.config = config
|
|
29
21
|
namespace = self.config.k8s_namespace
|
|
30
22
|
kubeconfig = self.config.kubeconfig_path
|
|
31
|
-
self.image_registry = image_registry
|
|
32
23
|
try:
|
|
33
24
|
if kubeconfig:
|
|
34
25
|
k8s_config.load_kube_config(config_file=kubeconfig)
|
|
@@ -40,7 +31,6 @@ class KubernetesClient(BaseClient):
|
|
|
40
31
|
except k8s_config.ConfigException:
|
|
41
32
|
k8s_config.load_kube_config()
|
|
42
33
|
self.v1 = client.CoreV1Api()
|
|
43
|
-
self.apps_v1 = client.AppsV1Api() # For Deployments
|
|
44
34
|
self.namespace = namespace
|
|
45
35
|
# Test connection
|
|
46
36
|
self.v1.list_namespace()
|
|
@@ -55,61 +45,6 @@ class KubernetesClient(BaseClient):
|
|
|
55
45
|
"• For in-cluster: ensure proper RBAC permissions",
|
|
56
46
|
) from e
|
|
57
47
|
|
|
58
|
-
def _is_local_cluster(self):
|
|
59
|
-
"""
|
|
60
|
-
Determine if we're connected to a local Kubernetes cluster.
|
|
61
|
-
|
|
62
|
-
Returns:
|
|
63
|
-
bool: True if connected to a local cluster, False otherwise
|
|
64
|
-
"""
|
|
65
|
-
try:
|
|
66
|
-
# Get the current context configuration
|
|
67
|
-
contexts, current_context = k8s_config.list_kube_config_contexts(
|
|
68
|
-
config_file=self.config.kubeconfig_path
|
|
69
|
-
if hasattr(self.config, "kubeconfig_path")
|
|
70
|
-
and self.config.kubeconfig_path
|
|
71
|
-
else None,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
if current_context and current_context.get("context"):
|
|
75
|
-
cluster_name = current_context["context"].get("cluster", "")
|
|
76
|
-
server = None
|
|
77
|
-
|
|
78
|
-
# Get cluster server URL
|
|
79
|
-
for cluster in contexts.get("clusters", []):
|
|
80
|
-
if cluster["name"] == cluster_name:
|
|
81
|
-
server = cluster.get("cluster", {}).get("server", "")
|
|
82
|
-
break
|
|
83
|
-
|
|
84
|
-
if server:
|
|
85
|
-
# Check for common local cluster patterns
|
|
86
|
-
local_patterns = [
|
|
87
|
-
"localhost",
|
|
88
|
-
"127.0.0.1",
|
|
89
|
-
"0.0.0.0",
|
|
90
|
-
"docker-desktop",
|
|
91
|
-
"kind-", # kind clusters
|
|
92
|
-
"minikube", # minikube
|
|
93
|
-
"k3d-", # k3d clusters
|
|
94
|
-
"colima", # colima
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
server_lower = server.lower()
|
|
98
|
-
cluster_lower = cluster_name.lower()
|
|
99
|
-
|
|
100
|
-
for pattern in local_patterns:
|
|
101
|
-
if pattern in server_lower or pattern in cluster_lower:
|
|
102
|
-
return True
|
|
103
|
-
|
|
104
|
-
return False
|
|
105
|
-
|
|
106
|
-
except Exception as e:
|
|
107
|
-
logger.debug(
|
|
108
|
-
f"Could not determine cluster type, assuming remote: {e}",
|
|
109
|
-
)
|
|
110
|
-
# If we can't determine, assume remote for safety
|
|
111
|
-
return False
|
|
112
|
-
|
|
113
48
|
def _parse_port_spec(self, port_spec):
|
|
114
49
|
"""
|
|
115
50
|
Parse port specification.
|
|
@@ -157,13 +92,6 @@ class KubernetesClient(BaseClient):
|
|
|
157
92
|
container_name = name or "main-container"
|
|
158
93
|
|
|
159
94
|
# Container specification
|
|
160
|
-
# TODO: use image from docker registry first
|
|
161
|
-
|
|
162
|
-
if not self.image_registry:
|
|
163
|
-
image = image
|
|
164
|
-
else:
|
|
165
|
-
image = f"{self.image_registry}/{image}"
|
|
166
|
-
|
|
167
95
|
container = client.V1Container(
|
|
168
96
|
name=container_name,
|
|
169
97
|
image=image,
|
|
@@ -634,8 +562,9 @@ class KubernetesClient(BaseClient):
|
|
|
634
562
|
def _get_pod_node_ip(self, pod_name):
|
|
635
563
|
"""Get the IP of the node where the pod is running"""
|
|
636
564
|
|
|
637
|
-
# Check if we are
|
|
638
|
-
|
|
565
|
+
# Check if we are running in Colima, where pod runs in VM
|
|
566
|
+
docker_host = os.getenv("DOCKER_HOST", "")
|
|
567
|
+
if "colima" in docker_host.lower():
|
|
639
568
|
return "localhost"
|
|
640
569
|
|
|
641
570
|
try:
|
|
@@ -672,479 +601,3 @@ class KubernetesClient(BaseClient):
|
|
|
672
601
|
except Exception as e:
|
|
673
602
|
logger.error(f"Failed to get pod node IP: {e}")
|
|
674
603
|
return None
|
|
675
|
-
|
|
676
|
-
def _create_deployment_spec(
|
|
677
|
-
self,
|
|
678
|
-
image,
|
|
679
|
-
name,
|
|
680
|
-
ports=None,
|
|
681
|
-
volumes=None,
|
|
682
|
-
environment=None,
|
|
683
|
-
runtime_config=None,
|
|
684
|
-
replicas=1,
|
|
685
|
-
):
|
|
686
|
-
"""Create a Kubernetes Deployment specification."""
|
|
687
|
-
if runtime_config is None:
|
|
688
|
-
runtime_config = {}
|
|
689
|
-
|
|
690
|
-
container_name = name or "main-container"
|
|
691
|
-
|
|
692
|
-
# Use image registry if configured
|
|
693
|
-
if not self.image_registry:
|
|
694
|
-
full_image = image
|
|
695
|
-
else:
|
|
696
|
-
full_image = f"{self.image_registry}/{image}"
|
|
697
|
-
|
|
698
|
-
# Create container spec (reuse the existing pod spec logic)
|
|
699
|
-
container = client.V1Container(
|
|
700
|
-
name=container_name,
|
|
701
|
-
image=full_image,
|
|
702
|
-
image_pull_policy=runtime_config.get(
|
|
703
|
-
"image_pull_policy",
|
|
704
|
-
"IfNotPresent",
|
|
705
|
-
),
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
# Configure ports
|
|
709
|
-
if ports:
|
|
710
|
-
container_ports = []
|
|
711
|
-
for port_spec in ports:
|
|
712
|
-
port_info = self._parse_port_spec(port_spec)
|
|
713
|
-
if port_info:
|
|
714
|
-
container_ports.append(
|
|
715
|
-
client.V1ContainerPort(
|
|
716
|
-
container_port=port_info["port"],
|
|
717
|
-
protocol=port_info["protocol"],
|
|
718
|
-
),
|
|
719
|
-
)
|
|
720
|
-
if container_ports:
|
|
721
|
-
container.ports = container_ports
|
|
722
|
-
|
|
723
|
-
# Configure environment variables
|
|
724
|
-
if environment:
|
|
725
|
-
env_vars = []
|
|
726
|
-
for key, value in environment.items():
|
|
727
|
-
env_vars.append(client.V1EnvVar(name=key, value=str(value)))
|
|
728
|
-
container.env = env_vars
|
|
729
|
-
|
|
730
|
-
# Configure volume mounts and volumes
|
|
731
|
-
volume_mounts = []
|
|
732
|
-
pod_volumes = []
|
|
733
|
-
if volumes:
|
|
734
|
-
for volume_idx, (host_path, mount_info) in enumerate(
|
|
735
|
-
volumes.items(),
|
|
736
|
-
):
|
|
737
|
-
if isinstance(mount_info, dict):
|
|
738
|
-
container_path = mount_info["bind"]
|
|
739
|
-
mode = mount_info.get("mode", "rw")
|
|
740
|
-
else:
|
|
741
|
-
container_path = mount_info
|
|
742
|
-
mode = "rw"
|
|
743
|
-
volume_name = f"vol-{volume_idx}"
|
|
744
|
-
|
|
745
|
-
# Create volume mount
|
|
746
|
-
volume_mounts.append(
|
|
747
|
-
client.V1VolumeMount(
|
|
748
|
-
name=volume_name,
|
|
749
|
-
mount_path=container_path,
|
|
750
|
-
read_only=(mode == "ro"),
|
|
751
|
-
),
|
|
752
|
-
)
|
|
753
|
-
# Create host path volume
|
|
754
|
-
pod_volumes.append(
|
|
755
|
-
client.V1Volume(
|
|
756
|
-
name=volume_name,
|
|
757
|
-
host_path=client.V1HostPathVolumeSource(
|
|
758
|
-
path=host_path,
|
|
759
|
-
),
|
|
760
|
-
),
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
if volume_mounts:
|
|
764
|
-
container.volume_mounts = volume_mounts
|
|
765
|
-
|
|
766
|
-
# Apply runtime config to container
|
|
767
|
-
if "resources" in runtime_config:
|
|
768
|
-
container.resources = client.V1ResourceRequirements(
|
|
769
|
-
**runtime_config["resources"],
|
|
770
|
-
)
|
|
771
|
-
|
|
772
|
-
if "security_context" in runtime_config:
|
|
773
|
-
container.security_context = client.V1SecurityContext(
|
|
774
|
-
**runtime_config["security_context"],
|
|
775
|
-
)
|
|
776
|
-
|
|
777
|
-
# Pod template specification
|
|
778
|
-
pod_spec = client.V1PodSpec(
|
|
779
|
-
containers=[container],
|
|
780
|
-
restart_policy=runtime_config.get(
|
|
781
|
-
"restart_policy",
|
|
782
|
-
"Always",
|
|
783
|
-
), # Deployments typically use Always
|
|
784
|
-
)
|
|
785
|
-
|
|
786
|
-
if pod_volumes:
|
|
787
|
-
pod_spec.volumes = pod_volumes
|
|
788
|
-
|
|
789
|
-
if "node_selector" in runtime_config:
|
|
790
|
-
pod_spec.node_selector = runtime_config["node_selector"]
|
|
791
|
-
|
|
792
|
-
if "tolerations" in runtime_config:
|
|
793
|
-
pod_spec.tolerations = runtime_config["tolerations"]
|
|
794
|
-
|
|
795
|
-
# Handle image pull secrets
|
|
796
|
-
image_pull_secrets = runtime_config.get("image_pull_secrets", [])
|
|
797
|
-
if image_pull_secrets:
|
|
798
|
-
secrets = []
|
|
799
|
-
for secret_name in image_pull_secrets:
|
|
800
|
-
secrets.append(client.V1LocalObjectReference(name=secret_name))
|
|
801
|
-
pod_spec.image_pull_secrets = secrets
|
|
802
|
-
|
|
803
|
-
# Pod template
|
|
804
|
-
pod_template = client.V1PodTemplateSpec(
|
|
805
|
-
metadata=client.V1ObjectMeta(
|
|
806
|
-
labels={
|
|
807
|
-
"app": name,
|
|
808
|
-
"created-by": "kubernetes-client",
|
|
809
|
-
},
|
|
810
|
-
),
|
|
811
|
-
spec=pod_spec,
|
|
812
|
-
)
|
|
813
|
-
|
|
814
|
-
# Deployment specification
|
|
815
|
-
deployment_spec = client.V1DeploymentSpec(
|
|
816
|
-
replicas=replicas,
|
|
817
|
-
selector=client.V1LabelSelector(
|
|
818
|
-
match_labels={"app": name},
|
|
819
|
-
),
|
|
820
|
-
template=pod_template,
|
|
821
|
-
)
|
|
822
|
-
|
|
823
|
-
return deployment_spec
|
|
824
|
-
|
|
825
|
-
def _create_loadbalancer_service(self, deployment_name, port_list):
|
|
826
|
-
"""Create a LoadBalancer service for the deployment."""
|
|
827
|
-
try:
|
|
828
|
-
service_name = f"{deployment_name}-lb-service"
|
|
829
|
-
selector = {"app": deployment_name}
|
|
830
|
-
|
|
831
|
-
# Construct multi-port configuration
|
|
832
|
-
service_ports = []
|
|
833
|
-
for port_info in port_list:
|
|
834
|
-
port = port_info["port"]
|
|
835
|
-
protocol = port_info["protocol"]
|
|
836
|
-
service_ports.append(
|
|
837
|
-
client.V1ServicePort(
|
|
838
|
-
name=f"port-{port}",
|
|
839
|
-
port=port,
|
|
840
|
-
target_port=port,
|
|
841
|
-
protocol=protocol,
|
|
842
|
-
),
|
|
843
|
-
)
|
|
844
|
-
|
|
845
|
-
service_spec = client.V1ServiceSpec(
|
|
846
|
-
selector=selector,
|
|
847
|
-
ports=service_ports,
|
|
848
|
-
type="LoadBalancer",
|
|
849
|
-
)
|
|
850
|
-
|
|
851
|
-
service = client.V1Service(
|
|
852
|
-
api_version="v1",
|
|
853
|
-
kind="Service",
|
|
854
|
-
metadata=client.V1ObjectMeta(
|
|
855
|
-
name=service_name,
|
|
856
|
-
namespace=self.namespace,
|
|
857
|
-
),
|
|
858
|
-
spec=service_spec,
|
|
859
|
-
)
|
|
860
|
-
|
|
861
|
-
# Create the service
|
|
862
|
-
self.v1.create_namespaced_service(
|
|
863
|
-
namespace=self.namespace,
|
|
864
|
-
body=service,
|
|
865
|
-
)
|
|
866
|
-
|
|
867
|
-
logger.debug(
|
|
868
|
-
f"LoadBalancer service '{service_name}' created successfully",
|
|
869
|
-
)
|
|
870
|
-
return True, service_name
|
|
871
|
-
except Exception as e:
|
|
872
|
-
logger.error(
|
|
873
|
-
f"Failed to create LoadBalancer service for deployment "
|
|
874
|
-
f"{deployment_name}: "
|
|
875
|
-
f"{e}, {traceback.format_exc()}",
|
|
876
|
-
)
|
|
877
|
-
return False, None
|
|
878
|
-
|
|
879
|
-
def create_deployment(
|
|
880
|
-
self,
|
|
881
|
-
image,
|
|
882
|
-
name=None,
|
|
883
|
-
ports=None,
|
|
884
|
-
volumes=None,
|
|
885
|
-
environment=None,
|
|
886
|
-
runtime_config=None,
|
|
887
|
-
replicas=1,
|
|
888
|
-
create_service=True,
|
|
889
|
-
) -> Tuple[str, list, str] | Tuple[None, None, None]:
|
|
890
|
-
"""
|
|
891
|
-
Create a new Kubernetes Deployment with LoadBalancer service.
|
|
892
|
-
|
|
893
|
-
Args:
|
|
894
|
-
image: Container image name
|
|
895
|
-
name: Deployment name (auto-generated if None)
|
|
896
|
-
ports: List of ports to expose
|
|
897
|
-
volumes: Volume mounts dictionary
|
|
898
|
-
environment: Environment variables dictionary
|
|
899
|
-
runtime_config: Runtime configuration dictionary
|
|
900
|
-
replicas: Number of replicas for the deployment
|
|
901
|
-
create_service: Whether to create LoadBalancer service
|
|
902
|
-
|
|
903
|
-
Returns:
|
|
904
|
-
Tuple of (deployment_name, ports, load_balancer_ip)
|
|
905
|
-
"""
|
|
906
|
-
if not name:
|
|
907
|
-
name = f"deploy-{hashlib.md5(image.encode()).hexdigest()[:8]}"
|
|
908
|
-
|
|
909
|
-
try:
|
|
910
|
-
# Create deployment specification
|
|
911
|
-
deployment_spec = self._create_deployment_spec(
|
|
912
|
-
image,
|
|
913
|
-
name,
|
|
914
|
-
ports,
|
|
915
|
-
volumes,
|
|
916
|
-
environment,
|
|
917
|
-
runtime_config,
|
|
918
|
-
replicas,
|
|
919
|
-
)
|
|
920
|
-
|
|
921
|
-
# Create deployment metadata
|
|
922
|
-
metadata = client.V1ObjectMeta(
|
|
923
|
-
name=name,
|
|
924
|
-
namespace=self.namespace,
|
|
925
|
-
labels={
|
|
926
|
-
"created-by": "kubernetes-client",
|
|
927
|
-
"app": name,
|
|
928
|
-
},
|
|
929
|
-
)
|
|
930
|
-
|
|
931
|
-
# Create deployment object
|
|
932
|
-
deployment = client.V1Deployment(
|
|
933
|
-
api_version="apps/v1",
|
|
934
|
-
kind="Deployment",
|
|
935
|
-
metadata=metadata,
|
|
936
|
-
spec=deployment_spec,
|
|
937
|
-
)
|
|
938
|
-
|
|
939
|
-
# Create the deployment
|
|
940
|
-
self.apps_v1.create_namespaced_deployment(
|
|
941
|
-
namespace=self.namespace,
|
|
942
|
-
body=deployment,
|
|
943
|
-
)
|
|
944
|
-
|
|
945
|
-
logger.debug(
|
|
946
|
-
f"Deployment '{name}' created successfully in namespace "
|
|
947
|
-
f"'{self.namespace}' with {replicas} replicas",
|
|
948
|
-
)
|
|
949
|
-
|
|
950
|
-
service_info = None
|
|
951
|
-
load_balancer_ip = None
|
|
952
|
-
|
|
953
|
-
# Create LoadBalancer service if requested and ports are specified
|
|
954
|
-
if create_service and ports:
|
|
955
|
-
parsed_ports = []
|
|
956
|
-
for port_spec in ports:
|
|
957
|
-
port_info = self._parse_port_spec(port_spec)
|
|
958
|
-
if port_info:
|
|
959
|
-
parsed_ports.append(port_info)
|
|
960
|
-
|
|
961
|
-
if parsed_ports:
|
|
962
|
-
(
|
|
963
|
-
service_created,
|
|
964
|
-
service_name,
|
|
965
|
-
) = self._create_loadbalancer_service(
|
|
966
|
-
name,
|
|
967
|
-
parsed_ports,
|
|
968
|
-
)
|
|
969
|
-
if service_created:
|
|
970
|
-
service_info = {
|
|
971
|
-
"name": service_name,
|
|
972
|
-
"type": "LoadBalancer",
|
|
973
|
-
"ports": [p["port"] for p in parsed_ports],
|
|
974
|
-
}
|
|
975
|
-
# Wait a bit and try to get LoadBalancer IP
|
|
976
|
-
time.sleep(2)
|
|
977
|
-
load_balancer_ip = self._get_loadbalancer_ip(
|
|
978
|
-
service_name,
|
|
979
|
-
)
|
|
980
|
-
|
|
981
|
-
# Wait for deployment to be ready
|
|
982
|
-
if not self.wait_for_deployment_ready(name, timeout=120):
|
|
983
|
-
logger.warning(f"Deployment '{name}' may not be fully ready")
|
|
984
|
-
|
|
985
|
-
logger.debug(
|
|
986
|
-
f"Deployment '{name}' created successfully with service: "
|
|
987
|
-
f"{service_info}",
|
|
988
|
-
)
|
|
989
|
-
|
|
990
|
-
return (
|
|
991
|
-
name,
|
|
992
|
-
service_info["ports"] if service_info else [],
|
|
993
|
-
load_balancer_ip or "",
|
|
994
|
-
)
|
|
995
|
-
|
|
996
|
-
except Exception as e:
|
|
997
|
-
logger.error(
|
|
998
|
-
f"Failed to create deployment: {e}, {traceback.format_exc()}",
|
|
999
|
-
)
|
|
1000
|
-
return None, None, None
|
|
1001
|
-
|
|
1002
|
-
def wait_for_deployment_ready(self, deployment_name, timeout=300):
|
|
1003
|
-
"""Wait for a deployment to be ready."""
|
|
1004
|
-
start_time = time.time()
|
|
1005
|
-
while time.time() - start_time < timeout:
|
|
1006
|
-
try:
|
|
1007
|
-
deployment = self.apps_v1.read_namespaced_deployment(
|
|
1008
|
-
name=deployment_name,
|
|
1009
|
-
namespace=self.namespace,
|
|
1010
|
-
)
|
|
1011
|
-
|
|
1012
|
-
# Check if deployment is ready
|
|
1013
|
-
if (
|
|
1014
|
-
deployment.status.ready_replicas
|
|
1015
|
-
and deployment.status.ready_replicas
|
|
1016
|
-
== deployment.spec.replicas
|
|
1017
|
-
):
|
|
1018
|
-
return True
|
|
1019
|
-
|
|
1020
|
-
time.sleep(3)
|
|
1021
|
-
except ApiException as e:
|
|
1022
|
-
if e.status == 404:
|
|
1023
|
-
return False
|
|
1024
|
-
time.sleep(3)
|
|
1025
|
-
return False
|
|
1026
|
-
|
|
1027
|
-
def _get_loadbalancer_ip(self, service_name, timeout=30):
|
|
1028
|
-
"""Get LoadBalancer external IP address."""
|
|
1029
|
-
start_time = time.time()
|
|
1030
|
-
while time.time() - start_time < timeout:
|
|
1031
|
-
try:
|
|
1032
|
-
service = self.v1.read_namespaced_service(
|
|
1033
|
-
name=service_name,
|
|
1034
|
-
namespace=self.namespace,
|
|
1035
|
-
)
|
|
1036
|
-
|
|
1037
|
-
if (
|
|
1038
|
-
service.status.load_balancer
|
|
1039
|
-
and service.status.load_balancer.ingress
|
|
1040
|
-
):
|
|
1041
|
-
ingress = service.status.load_balancer.ingress[0]
|
|
1042
|
-
# Return IP or hostname
|
|
1043
|
-
return ingress.ip or ingress.hostname
|
|
1044
|
-
|
|
1045
|
-
time.sleep(2)
|
|
1046
|
-
except Exception as e:
|
|
1047
|
-
logger.debug(f"Waiting for LoadBalancer IP: {e}")
|
|
1048
|
-
time.sleep(2)
|
|
1049
|
-
|
|
1050
|
-
logger.debug(
|
|
1051
|
-
f"LoadBalancer IP not available for service {service_name}",
|
|
1052
|
-
)
|
|
1053
|
-
return None
|
|
1054
|
-
|
|
1055
|
-
def remove_deployment(self, deployment_name, remove_service=True):
|
|
1056
|
-
"""Remove a Kubernetes Deployment and optionally its service."""
|
|
1057
|
-
try:
|
|
1058
|
-
# Remove associated LoadBalancer service first if requested
|
|
1059
|
-
if remove_service:
|
|
1060
|
-
service_name = f"{deployment_name}-lb-service"
|
|
1061
|
-
try:
|
|
1062
|
-
self.v1.delete_namespaced_service(
|
|
1063
|
-
name=service_name,
|
|
1064
|
-
namespace=self.namespace,
|
|
1065
|
-
)
|
|
1066
|
-
logger.debug(
|
|
1067
|
-
f"Removed LoadBalancer service {service_name}",
|
|
1068
|
-
)
|
|
1069
|
-
except ApiException as e:
|
|
1070
|
-
if e.status != 404:
|
|
1071
|
-
logger.warning(
|
|
1072
|
-
f"Failed to remove service {service_name}: {e}",
|
|
1073
|
-
)
|
|
1074
|
-
|
|
1075
|
-
# Remove the deployment
|
|
1076
|
-
self.apps_v1.delete_namespaced_deployment(
|
|
1077
|
-
name=deployment_name,
|
|
1078
|
-
namespace=self.namespace,
|
|
1079
|
-
body=client.V1DeleteOptions(
|
|
1080
|
-
propagation_policy="Foreground",
|
|
1081
|
-
),
|
|
1082
|
-
)
|
|
1083
|
-
|
|
1084
|
-
logger.debug(
|
|
1085
|
-
f"Deployment '{deployment_name}' removed successfully",
|
|
1086
|
-
)
|
|
1087
|
-
return True
|
|
1088
|
-
|
|
1089
|
-
except ApiException as e:
|
|
1090
|
-
if e.status == 404:
|
|
1091
|
-
logger.warning(f"Deployment '{deployment_name}' not found")
|
|
1092
|
-
return True
|
|
1093
|
-
logger.error(f"Failed to remove deployment: {e.reason}")
|
|
1094
|
-
return False
|
|
1095
|
-
except Exception as e:
|
|
1096
|
-
logger.error(f"An error occurred: {e}, {traceback.format_exc()}")
|
|
1097
|
-
return False
|
|
1098
|
-
|
|
1099
|
-
def get_deployment_status(self, deployment_name):
|
|
1100
|
-
"""Get the current status of the specified deployment."""
|
|
1101
|
-
try:
|
|
1102
|
-
deployment = self.apps_v1.read_namespaced_deployment(
|
|
1103
|
-
name=deployment_name,
|
|
1104
|
-
namespace=self.namespace,
|
|
1105
|
-
)
|
|
1106
|
-
|
|
1107
|
-
return {
|
|
1108
|
-
"name": deployment_name,
|
|
1109
|
-
"replicas": deployment.spec.replicas,
|
|
1110
|
-
"ready_replicas": deployment.status.ready_replicas or 0,
|
|
1111
|
-
"available_replicas": deployment.status.available_replicas
|
|
1112
|
-
or 0,
|
|
1113
|
-
"unavailable_replicas": deployment.status.unavailable_replicas
|
|
1114
|
-
or 0,
|
|
1115
|
-
"conditions": [
|
|
1116
|
-
{
|
|
1117
|
-
"type": condition.type,
|
|
1118
|
-
"status": condition.status,
|
|
1119
|
-
"reason": condition.reason,
|
|
1120
|
-
"message": condition.message,
|
|
1121
|
-
}
|
|
1122
|
-
for condition in (deployment.status.conditions or [])
|
|
1123
|
-
],
|
|
1124
|
-
}
|
|
1125
|
-
except ApiException as e:
|
|
1126
|
-
if e.status == 404:
|
|
1127
|
-
logger.warning(f"Deployment '{deployment_name}' not found")
|
|
1128
|
-
else:
|
|
1129
|
-
logger.error(f"Failed to get deployment status: {e.reason}")
|
|
1130
|
-
return None
|
|
1131
|
-
except Exception as e:
|
|
1132
|
-
logger.error(f"An error occurred: {e}, {traceback.format_exc()}")
|
|
1133
|
-
return None
|
|
1134
|
-
|
|
1135
|
-
def list_deployments(self, label_selector=None):
|
|
1136
|
-
"""List deployments in the namespace."""
|
|
1137
|
-
try:
|
|
1138
|
-
deployments = self.apps_v1.list_namespaced_deployment(
|
|
1139
|
-
namespace=self.namespace,
|
|
1140
|
-
label_selector=label_selector,
|
|
1141
|
-
)
|
|
1142
|
-
return [
|
|
1143
|
-
deployment.metadata.name for deployment in deployments.items
|
|
1144
|
-
]
|
|
1145
|
-
except ApiException as e:
|
|
1146
|
-
logger.error(f"Failed to list deployments: {e.reason}")
|
|
1147
|
-
return []
|
|
1148
|
-
except Exception as e:
|
|
1149
|
-
logger.error(f"An error occurred: {e}, {traceback.format_exc()}")
|
|
1150
|
-
return []
|