service-forge 0.1.11__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.
Potentially problematic release.
This version of service-forge might be problematic. Click here for more details.
- service_forge/api/deprecated_websocket_api.py +86 -0
- service_forge/api/deprecated_websocket_manager.py +425 -0
- service_forge/api/http_api.py +148 -0
- service_forge/api/http_api_doc.py +455 -0
- service_forge/api/kafka_api.py +126 -0
- service_forge/api/routers/service/__init__.py +4 -0
- service_forge/api/routers/service/service_router.py +137 -0
- service_forge/api/routers/websocket/websocket_manager.py +83 -0
- service_forge/api/routers/websocket/websocket_router.py +78 -0
- service_forge/api/task_manager.py +141 -0
- service_forge/db/__init__.py +1 -0
- service_forge/db/database.py +240 -0
- service_forge/llm/__init__.py +62 -0
- service_forge/llm/llm.py +56 -0
- service_forge/model/__init__.py +0 -0
- service_forge/model/websocket.py +13 -0
- service_forge/proto/foo_input.py +5 -0
- service_forge/service.py +288 -0
- service_forge/service_config.py +158 -0
- service_forge/sft/cli.py +91 -0
- service_forge/sft/cmd/config_command.py +67 -0
- service_forge/sft/cmd/deploy_service.py +123 -0
- service_forge/sft/cmd/list_tars.py +41 -0
- service_forge/sft/cmd/service_command.py +149 -0
- service_forge/sft/cmd/upload_service.py +36 -0
- service_forge/sft/config/injector.py +119 -0
- service_forge/sft/config/injector_default_files.py +131 -0
- service_forge/sft/config/sf_metadata.py +30 -0
- service_forge/sft/config/sft_config.py +153 -0
- service_forge/sft/file/__init__.py +0 -0
- service_forge/sft/file/ignore_pattern.py +80 -0
- service_forge/sft/file/sft_file_manager.py +107 -0
- service_forge/sft/kubernetes/kubernetes_manager.py +257 -0
- service_forge/sft/util/assert_util.py +25 -0
- service_forge/sft/util/logger.py +16 -0
- service_forge/sft/util/name_util.py +8 -0
- service_forge/sft/util/yaml_utils.py +57 -0
- service_forge/utils/__init__.py +0 -0
- service_forge/utils/default_type_converter.py +12 -0
- service_forge/utils/register.py +39 -0
- service_forge/utils/type_converter.py +99 -0
- service_forge/utils/workflow_clone.py +124 -0
- service_forge/workflow/__init__.py +1 -0
- service_forge/workflow/context.py +14 -0
- service_forge/workflow/edge.py +24 -0
- service_forge/workflow/node.py +184 -0
- service_forge/workflow/nodes/__init__.py +8 -0
- service_forge/workflow/nodes/control/if_node.py +29 -0
- service_forge/workflow/nodes/control/switch_node.py +28 -0
- service_forge/workflow/nodes/input/console_input_node.py +26 -0
- service_forge/workflow/nodes/llm/query_llm_node.py +41 -0
- service_forge/workflow/nodes/nested/workflow_node.py +28 -0
- service_forge/workflow/nodes/output/kafka_output_node.py +27 -0
- service_forge/workflow/nodes/output/print_node.py +29 -0
- service_forge/workflow/nodes/test/if_console_input_node.py +33 -0
- service_forge/workflow/nodes/test/time_consuming_node.py +62 -0
- service_forge/workflow/port.py +89 -0
- service_forge/workflow/trigger.py +24 -0
- service_forge/workflow/triggers/__init__.py +6 -0
- service_forge/workflow/triggers/a2a_api_trigger.py +255 -0
- service_forge/workflow/triggers/fast_api_trigger.py +169 -0
- service_forge/workflow/triggers/kafka_api_trigger.py +44 -0
- service_forge/workflow/triggers/once_trigger.py +20 -0
- service_forge/workflow/triggers/period_trigger.py +26 -0
- service_forge/workflow/triggers/websocket_api_trigger.py +184 -0
- service_forge/workflow/workflow.py +210 -0
- service_forge/workflow/workflow_callback.py +141 -0
- service_forge/workflow/workflow_event.py +15 -0
- service_forge/workflow/workflow_factory.py +246 -0
- service_forge/workflow/workflow_group.py +27 -0
- service_forge/workflow/workflow_type.py +52 -0
- service_forge-0.1.11.dist-info/METADATA +98 -0
- service_forge-0.1.11.dist-info/RECORD +75 -0
- service_forge-0.1.11.dist-info/WHEEL +4 -0
- service_forge-0.1.11.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from service_forge.sft.config.sft_config import SftConfig
|
|
7
|
+
from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
|
|
8
|
+
from service_forge.sft.config.sft_config import sft_config
|
|
9
|
+
|
|
10
|
+
def list_config() -> None:
|
|
11
|
+
try:
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
table = Table(title="SFT Configuration", show_header=True, header_style="bold magenta")
|
|
15
|
+
table.add_column("Key", style="cyan", no_wrap=True)
|
|
16
|
+
table.add_column("Value", style="green")
|
|
17
|
+
table.add_column("Description", style="yellow")
|
|
18
|
+
|
|
19
|
+
# Automatically add rows for all config items
|
|
20
|
+
config_dict = sft_config.to_dict()
|
|
21
|
+
for key, value in sorted(config_dict.items()):
|
|
22
|
+
description = SftConfig.CONFIG_DESCRIPTIONS.get(key, "No description available")
|
|
23
|
+
table.add_row(key, str(value), description)
|
|
24
|
+
|
|
25
|
+
console.print(table)
|
|
26
|
+
console.print(f"\n[dim]Config file location: {sft_config.config_file_path}[/dim]")
|
|
27
|
+
except Exception as e:
|
|
28
|
+
log_error(f"Failed to load config: {e}")
|
|
29
|
+
raise typer.Exit(1)
|
|
30
|
+
|
|
31
|
+
def get_config(key: str) -> None:
|
|
32
|
+
try:
|
|
33
|
+
value = sft_config.get(key)
|
|
34
|
+
|
|
35
|
+
if value is None:
|
|
36
|
+
log_error(f"Config key '{key}' not found")
|
|
37
|
+
log_info("Available keys: config_root, sft_file_root, k8s_namespace")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
log_info(f"{key} = {value}")
|
|
41
|
+
except ValueError as e:
|
|
42
|
+
log_error(str(e))
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
log_error(f"Failed to get config: {e}")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
|
|
48
|
+
def set_config(key: str, value: str) -> None:
|
|
49
|
+
try:
|
|
50
|
+
current_value = sft_config.get(key)
|
|
51
|
+
if current_value is None:
|
|
52
|
+
log_error(f"Unknown config key: {key}")
|
|
53
|
+
log_info("Available keys: config_root, sft_file_root, k8s_namespace")
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
56
|
+
sft_config.set(key, value)
|
|
57
|
+
sft_config.save()
|
|
58
|
+
|
|
59
|
+
log_success(f"Updated {key} = {value}")
|
|
60
|
+
log_info(f"Config saved to {sft_config.config_file_path}")
|
|
61
|
+
except ValueError as e:
|
|
62
|
+
log_error(str(e))
|
|
63
|
+
raise typer.Exit(1)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
log_error(f"Failed to set config: {e}")
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import tarfile
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from omegaconf import OmegaConf
|
|
10
|
+
from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
|
|
11
|
+
from service_forge.sft.file.sft_file_manager import sft_file_manager
|
|
12
|
+
from service_forge.sft.config.sft_config import sft_config
|
|
13
|
+
from service_forge.sft.util.assert_util import assert_file_exists, assert_dir_exists
|
|
14
|
+
from service_forge.sft.config.sf_metadata import load_metadata
|
|
15
|
+
from service_forge.sft.kubernetes.kubernetes_manager import KubernetesManager
|
|
16
|
+
from service_forge.sft.config.injector import Injector
|
|
17
|
+
from service_forge.sft.util.name_util import get_service_name
|
|
18
|
+
|
|
19
|
+
def _extract_tar_file(tar_file: Path, temp_path: Path) -> None:
|
|
20
|
+
log_info(f"Extracting tar file to: {temp_path}")
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
with tarfile.open(tar_file, 'r') as tar:
|
|
24
|
+
tar.extractall(temp_path)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
log_error(f"Failed to extract tar file: {e}")
|
|
27
|
+
raise typer.Exit(1)
|
|
28
|
+
|
|
29
|
+
log_success("Tar file extracted successfully")
|
|
30
|
+
|
|
31
|
+
def _build_docker_image(project_dir: Path, name: str, version: str) -> None:
|
|
32
|
+
image_name = f"sf-{name}:{version}"
|
|
33
|
+
full_image_name = sft_config.registry_address + "/" + image_name
|
|
34
|
+
log_info(f"Building Docker image: {image_name}")
|
|
35
|
+
try:
|
|
36
|
+
# build docker image
|
|
37
|
+
build_result = subprocess.run(
|
|
38
|
+
["docker", "build", "-t", full_image_name, str(project_dir)],
|
|
39
|
+
capture_output=True,
|
|
40
|
+
text=True,
|
|
41
|
+
check=True
|
|
42
|
+
)
|
|
43
|
+
log_success(f"Docker image built successfully: {image_name}")
|
|
44
|
+
if build_result.stdout:
|
|
45
|
+
log_info(build_result.stdout)
|
|
46
|
+
|
|
47
|
+
# push docker image to registry
|
|
48
|
+
log_info(f"Pushing Docker image to registry: {full_image_name}")
|
|
49
|
+
push_result = subprocess.run(
|
|
50
|
+
["docker", "push", full_image_name],
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
check=True
|
|
54
|
+
)
|
|
55
|
+
log_success(f"Docker image pushed successfully: {full_image_name}")
|
|
56
|
+
if push_result.stdout:
|
|
57
|
+
log_info(push_result.stdout)
|
|
58
|
+
|
|
59
|
+
except subprocess.CalledProcessError as e:
|
|
60
|
+
log_error(f"Docker operation failed: {e}")
|
|
61
|
+
if e.stderr:
|
|
62
|
+
log_error(e.stderr)
|
|
63
|
+
raise typer.Exit(1)
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
log_error("Docker command not found. Please install Docker.")
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
|
|
68
|
+
def _apply_k8s_deployment(deployment_yaml: Path, ingress_yaml: Path, name: str, version: str) -> None:
|
|
69
|
+
log_info("Applying k8s deployment...")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
k8s_manager = KubernetesManager()
|
|
73
|
+
k8s_manager.delete_service(sft_config.k8s_namespace, get_service_name(name, version), force=True)
|
|
74
|
+
k8s_manager.apply_deployment_yaml(deployment_yaml, sft_config.k8s_namespace)
|
|
75
|
+
k8s_manager.apply_deployment_yaml(ingress_yaml, sft_config.k8s_namespace)
|
|
76
|
+
log_success("K8s deployment applied successfully")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
log_error(f"K8s deployment failed: {e}")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
log_success(f"Deployment process completed for {name}:{version}")
|
|
82
|
+
|
|
83
|
+
def _inject_config(project_dir: Path) -> None:
|
|
84
|
+
injector = Injector(project_dir)
|
|
85
|
+
injector.inject()
|
|
86
|
+
|
|
87
|
+
def deploy_service(name: str, version: str) -> None:
|
|
88
|
+
tar_file = sft_file_manager.tar_path / f"sf_{name}_{version}.tar"
|
|
89
|
+
|
|
90
|
+
assert_file_exists(tar_file)
|
|
91
|
+
|
|
92
|
+
temp_parent = os.path.join(tempfile.gettempdir(), "sft")
|
|
93
|
+
os.makedirs(temp_parent, exist_ok=True)
|
|
94
|
+
|
|
95
|
+
with tempfile.TemporaryDirectory(prefix=f"deploy_{name}_{version}", dir=temp_parent) as temp_dir:
|
|
96
|
+
temp_path = Path(temp_dir)
|
|
97
|
+
|
|
98
|
+
_extract_tar_file(tar_file, temp_path)
|
|
99
|
+
|
|
100
|
+
project_dir = temp_path / f"{name}_{version}"
|
|
101
|
+
|
|
102
|
+
_inject_config(project_dir)
|
|
103
|
+
|
|
104
|
+
dockerfile_path = project_dir / "Dockerfile"
|
|
105
|
+
metadata_path = project_dir / "sf-meta.yaml"
|
|
106
|
+
deployment_yaml = project_dir / "deployment.yaml"
|
|
107
|
+
ingress_yaml = project_dir / "ingress.yaml"
|
|
108
|
+
|
|
109
|
+
assert_dir_exists(project_dir)
|
|
110
|
+
assert_file_exists(dockerfile_path)
|
|
111
|
+
assert_file_exists(metadata_path)
|
|
112
|
+
assert_file_exists(deployment_yaml)
|
|
113
|
+
assert_file_exists(ingress_yaml)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
meta_data = load_metadata(metadata_path)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
log_error(f"Failed to read sf-meta.yaml: {e}")
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
|
|
121
|
+
_build_docker_image(project_dir, meta_data.name, meta_data.version)
|
|
122
|
+
# TODO: create new user in mongodb and redis
|
|
123
|
+
_apply_k8s_deployment(deployment_yaml, ingress_yaml, meta_data.name, meta_data.version)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from service_forge.sft.util.logger import log_error, log_info
|
|
7
|
+
from service_forge.sft.file.sft_file_manager import sft_file_manager
|
|
8
|
+
|
|
9
|
+
def list_tars() -> None:
|
|
10
|
+
tar_files = sft_file_manager.load_tars()
|
|
11
|
+
|
|
12
|
+
if not tar_files:
|
|
13
|
+
log_info("No tar files found.")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
table = Table(title="Service Tar Files", show_header=True, header_style="bold magenta")
|
|
18
|
+
table.add_column("Project", style="cyan", no_wrap=True)
|
|
19
|
+
table.add_column("Version", style="cyan", no_wrap=True)
|
|
20
|
+
table.add_column("File Name", style="cyan", no_wrap=True)
|
|
21
|
+
table.add_column("Size", justify="right", style="green")
|
|
22
|
+
table.add_column("Modified Time", style="yellow")
|
|
23
|
+
|
|
24
|
+
for tar_file in tar_files:
|
|
25
|
+
table.add_row(tar_file.project_name, tar_file.version, tar_file.path.name, tar_file._format_size(), tar_file._format_modified_time())
|
|
26
|
+
|
|
27
|
+
console.print(table)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _format_size(size_bytes: int) -> str:
|
|
31
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
32
|
+
if size_bytes < 1024.0:
|
|
33
|
+
return f"{size_bytes:.2f} {unit}"
|
|
34
|
+
size_bytes /= 1024.0
|
|
35
|
+
return f"{size_bytes:.2f} TB"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _format_time(timestamp: float) -> str:
|
|
39
|
+
from datetime import datetime
|
|
40
|
+
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
|
41
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from kubernetes import client, config
|
|
6
|
+
from kubernetes.client.rest import ApiException
|
|
7
|
+
|
|
8
|
+
from service_forge.sft.config.sft_config import sft_config
|
|
9
|
+
from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
|
|
10
|
+
from service_forge.sft.kubernetes.kubernetes_manager import KubernetesManager
|
|
11
|
+
|
|
12
|
+
def list_services() -> None:
|
|
13
|
+
namespace = sft_config.k8s_namespace
|
|
14
|
+
kubernetes_manager = KubernetesManager()
|
|
15
|
+
services = kubernetes_manager.get_services_in_namespace(namespace)
|
|
16
|
+
|
|
17
|
+
if not services:
|
|
18
|
+
log_warning(f"No services starting with 'sf-' found in namespace '{namespace}'")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
table = Table(title=f"Services in namespace '{namespace}' (sf-*)", show_header=True, header_style="bold magenta")
|
|
23
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
24
|
+
table.add_column("Type", style="green")
|
|
25
|
+
table.add_column("Port", style="yellow")
|
|
26
|
+
table.add_column("Target Port", style="yellow")
|
|
27
|
+
|
|
28
|
+
for service_name in sorted(services):
|
|
29
|
+
details = kubernetes_manager.get_service_details(namespace, service_name)
|
|
30
|
+
table.add_row(
|
|
31
|
+
details.name,
|
|
32
|
+
details.type or "-",
|
|
33
|
+
str(details.port or "-"),
|
|
34
|
+
str(details.target_port or "-")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
console.print(table)
|
|
38
|
+
log_info(f"Found {len(services)} service(s)")
|
|
39
|
+
|
|
40
|
+
def show_logs(
|
|
41
|
+
namespace: str,
|
|
42
|
+
pod_name: str,
|
|
43
|
+
container_name: str,
|
|
44
|
+
tail: int,
|
|
45
|
+
follow: bool,
|
|
46
|
+
previous: bool
|
|
47
|
+
) -> None:
|
|
48
|
+
kubernetes_manager = KubernetesManager()
|
|
49
|
+
try:
|
|
50
|
+
if not follow:
|
|
51
|
+
log_info(f"Fetching logs from pod '{pod_name}' (container: {container_name})...")
|
|
52
|
+
|
|
53
|
+
logs = kubernetes_manager.get_pod_logs(
|
|
54
|
+
namespace=namespace,
|
|
55
|
+
pod_name=pod_name,
|
|
56
|
+
container_name=container_name,
|
|
57
|
+
tail=tail,
|
|
58
|
+
follow=follow,
|
|
59
|
+
previous=previous
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if follow:
|
|
63
|
+
try:
|
|
64
|
+
for line in logs:
|
|
65
|
+
log_info(line, end="")
|
|
66
|
+
except KeyboardInterrupt:
|
|
67
|
+
log_warning("Log streaming interrupted")
|
|
68
|
+
raise typer.Exit(0)
|
|
69
|
+
else:
|
|
70
|
+
if logs:
|
|
71
|
+
log_info(logs)
|
|
72
|
+
else:
|
|
73
|
+
log_warning(f"No logs available for pod '{pod_name}' container '{container_name}'")
|
|
74
|
+
|
|
75
|
+
except ApiException as e:
|
|
76
|
+
if e.status == 404:
|
|
77
|
+
log_error(f"Pod '{pod_name}' or container '{container_name}' not found")
|
|
78
|
+
elif e.status == 400:
|
|
79
|
+
log_error(f"Bad request: {e.reason}")
|
|
80
|
+
if "previous" in str(e.body).lower():
|
|
81
|
+
log_info("Note: 'previous' flag only works for stopped containers")
|
|
82
|
+
else:
|
|
83
|
+
log_error(f"Failed to get logs: {e.reason}")
|
|
84
|
+
if e.body:
|
|
85
|
+
log_error(f"Error details: {e.body}")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
log_error(f"Failed to get logs: {e}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def show_service_logs(
|
|
91
|
+
service_name: str,
|
|
92
|
+
container: Optional[str] = None,
|
|
93
|
+
tail: int = 100,
|
|
94
|
+
follow: bool = False,
|
|
95
|
+
previous: bool = False
|
|
96
|
+
) -> None:
|
|
97
|
+
namespace = sft_config.k8s_namespace
|
|
98
|
+
kubernetes_manager = KubernetesManager()
|
|
99
|
+
|
|
100
|
+
if not service_name.startswith("sf-"):
|
|
101
|
+
log_warning(f"Service name '{service_name}' does not start with 'sf-'. Proceeding anyway...")
|
|
102
|
+
|
|
103
|
+
services = kubernetes_manager.get_services_in_namespace(namespace)
|
|
104
|
+
if service_name not in services:
|
|
105
|
+
log_error(f"Service '{service_name}' not found in namespace '{namespace}'")
|
|
106
|
+
log_info(f"Available services: {', '.join(services) if services else 'None'}")
|
|
107
|
+
raise typer.Exit(1)
|
|
108
|
+
|
|
109
|
+
pod_names = kubernetes_manager.get_pods_for_service(namespace, service_name)
|
|
110
|
+
if not pod_names:
|
|
111
|
+
log_error(f"No pods found for service '{service_name}' in namespace '{namespace}'")
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
|
|
114
|
+
log_info(f"Found {len(pod_names)} pod(s) for service '{service_name}'")
|
|
115
|
+
|
|
116
|
+
for pod_name in pod_names:
|
|
117
|
+
containers = kubernetes_manager.get_pod_containers(namespace, pod_name)
|
|
118
|
+
|
|
119
|
+
if not containers:
|
|
120
|
+
log_warning(f"No containers found in pod '{pod_name}'")
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if container and container not in containers:
|
|
124
|
+
log_error(f"Container '{container}' not found in pod '{pod_name}'")
|
|
125
|
+
log_info(f"Available containers: {', '.join(containers)}")
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
target_containers = [container] if container else containers
|
|
129
|
+
|
|
130
|
+
for container_name in target_containers:
|
|
131
|
+
show_logs(namespace, pod_name, container_name, tail, follow, previous)
|
|
132
|
+
|
|
133
|
+
def delete_service(service_name: str, force: bool = False) -> None:
|
|
134
|
+
namespace = sft_config.k8s_namespace
|
|
135
|
+
|
|
136
|
+
if not service_name.startswith("sf-"):
|
|
137
|
+
log_error(f"Service name '{service_name}' does not start with 'sf-'")
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
kubernetes_manager = KubernetesManager()
|
|
141
|
+
|
|
142
|
+
services = kubernetes_manager.get_services_in_namespace(namespace)
|
|
143
|
+
if service_name not in services:
|
|
144
|
+
log_warning(f"Service '{service_name}' not found in namespace '{namespace}'")
|
|
145
|
+
log_info(f"Available services: {', '.join(services) if services else 'None'}")
|
|
146
|
+
|
|
147
|
+
log_info(f"Deleting service '{service_name}' from namespace '{namespace}'...")
|
|
148
|
+
kubernetes_manager.delete_service(namespace, service_name, force)
|
|
149
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from service_forge.sft.util.logger import log_error, log_success
|
|
4
|
+
from service_forge.sft.file.sft_file_manager import sft_file_manager
|
|
5
|
+
from service_forge.sft.util.assert_util import assert_dir_exists, assert_file_exists
|
|
6
|
+
from service_forge.sft.config.sf_metadata import load_metadata
|
|
7
|
+
|
|
8
|
+
def upload_service(project_path: str) -> None:
|
|
9
|
+
project_dir = Path(project_path).resolve()
|
|
10
|
+
assert_dir_exists(project_dir)
|
|
11
|
+
|
|
12
|
+
metadata_path = project_dir / "sf-meta.yaml"
|
|
13
|
+
assert_file_exists(metadata_path)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
meta_data = load_metadata(metadata_path)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
log_error(f"Failed to read sf-meta.yaml: {e}")
|
|
19
|
+
raise typer.Exit(1)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
tar_file = sft_file_manager.create_tar(project_dir, meta_data.name, meta_data.version)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
log_error(f"Failed to create tar file: {e}")
|
|
25
|
+
raise typer.Exit(1)
|
|
26
|
+
|
|
27
|
+
log_success(f"Packaging successful: {tar_file}")
|
|
28
|
+
|
|
29
|
+
# upload to the service
|
|
30
|
+
try:
|
|
31
|
+
sft_file_manager.upload_tar(tar_file)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
log_error(f"Failed to upload tar file: {e}")
|
|
34
|
+
raise typer.Exit(1)
|
|
35
|
+
|
|
36
|
+
log_success(f"Upload successful: {tar_file}")
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from omegaconf import OmegaConf
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from service_forge.sft.util.logger import log_info, log_error
|
|
4
|
+
from service_forge.sft.config.injector_default_files import *
|
|
5
|
+
from service_forge.sft.config.sf_metadata import load_metadata
|
|
6
|
+
from service_forge.sft.config.sft_config import sft_config
|
|
7
|
+
from service_forge.service_config import ServiceConfig
|
|
8
|
+
from service_forge.sft.util.name_util import get_service_name
|
|
9
|
+
from service_forge.sft.util.yaml_utils import load_sf_metadata_as_string
|
|
10
|
+
|
|
11
|
+
class Injector:
|
|
12
|
+
def __init__(self, project_dir: Path):
|
|
13
|
+
self.project_dir = project_dir
|
|
14
|
+
self.deployment_yaml_path = project_dir / "deployment.yaml"
|
|
15
|
+
self.metadata_path = project_dir / "sf-meta.yaml"
|
|
16
|
+
self.ingress_yaml_path = project_dir / "ingress.yaml"
|
|
17
|
+
self.dockerfile_path = project_dir / "Dockerfile"
|
|
18
|
+
self.pyproject_toml_path = project_dir / "pyproject.toml"
|
|
19
|
+
self.metadata = load_metadata(self.metadata_path)
|
|
20
|
+
self.name = self.metadata.name
|
|
21
|
+
self.version = self.metadata.version
|
|
22
|
+
self.namespace = sft_config.k8s_namespace
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
self.sf_metadata_string = load_sf_metadata_as_string(self.metadata_path)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
log_error(f"Failed to load sf-metadata as string: {e}")
|
|
28
|
+
self.sf_metadata_string = ""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def service_name(self) -> str:
|
|
32
|
+
return get_service_name(self.name, self.version)
|
|
33
|
+
|
|
34
|
+
def inject_deployment(self) -> None:
|
|
35
|
+
single_line_metadata = self.sf_metadata_string.replace('\n', '\\n').replace('"', '\\"')
|
|
36
|
+
|
|
37
|
+
envs = {
|
|
38
|
+
"DEEPSEEK_API_KEY": sft_config.deepseek_api_key,
|
|
39
|
+
"DEEPSEEK_BASE_URL": sft_config.deepseek_base_url,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for env in self.metadata.env:
|
|
43
|
+
envs[env['name']] = env['value']
|
|
44
|
+
|
|
45
|
+
env_str = ""
|
|
46
|
+
for key, value in envs.items():
|
|
47
|
+
env_str += f" - name: {key}\n value: {value}\n"
|
|
48
|
+
|
|
49
|
+
deployment_yaml = DEFAULT_DEPLOYMENT_YAML.format(
|
|
50
|
+
service_name=self.service_name,
|
|
51
|
+
name=self.name,
|
|
52
|
+
version=self.version,
|
|
53
|
+
namespace=self.namespace,
|
|
54
|
+
sf_metadata=f'"{single_line_metadata}"',
|
|
55
|
+
env=env_str,
|
|
56
|
+
)
|
|
57
|
+
with open(self.deployment_yaml_path, "w") as f:
|
|
58
|
+
f.write(deployment_yaml)
|
|
59
|
+
print("deployment_yaml_path: ", self.deployment_yaml_path)
|
|
60
|
+
|
|
61
|
+
def inject_service_config(self) -> None:
|
|
62
|
+
service_config_path = self.project_dir / Path(self.metadata.service_config)
|
|
63
|
+
config = ServiceConfig.from_dict(OmegaConf.to_object(OmegaConf.load(self.project_dir / self.metadata.service_config)))
|
|
64
|
+
|
|
65
|
+
config.http_port = sft_config.inject_http_port
|
|
66
|
+
config.kafka_host = sft_config.inject_kafka_host
|
|
67
|
+
config.kafka_port = sft_config.inject_kafka_port
|
|
68
|
+
for database in config.databases:
|
|
69
|
+
if database.postgres_host is not None:
|
|
70
|
+
database.postgres_host = sft_config.inject_postgres_host
|
|
71
|
+
database.postgres_port = sft_config.inject_postgres_port
|
|
72
|
+
database.postgres_user = sft_config.inject_postgres_user
|
|
73
|
+
database.postgres_password = sft_config.inject_postgres_password
|
|
74
|
+
database.postgres_db = self.service_name
|
|
75
|
+
if database.mongo_host is not None:
|
|
76
|
+
database.mongo_host = sft_config.inject_mongo_host
|
|
77
|
+
database.mongo_port = sft_config.inject_mongo_port
|
|
78
|
+
database.mongo_user = sft_config.inject_mongo_user
|
|
79
|
+
database.mongo_password = sft_config.inject_mongo_password
|
|
80
|
+
database.mongo_db = sft_config.inject_mongo_db
|
|
81
|
+
if database.redis_host is not None:
|
|
82
|
+
database.redis_host = sft_config.inject_redis_host
|
|
83
|
+
database.redis_port = sft_config.inject_redis_port
|
|
84
|
+
database.redis_password = sft_config.inject_redis_password
|
|
85
|
+
|
|
86
|
+
with open(service_config_path, "w") as f:
|
|
87
|
+
f.write(OmegaConf.to_yaml(config.to_dict()))
|
|
88
|
+
|
|
89
|
+
def inject_ingress(self) -> None:
|
|
90
|
+
ingress_yaml = DEFAULT_TRAEFIK_INGRESS_YAML.format(
|
|
91
|
+
name=self.name,
|
|
92
|
+
version=self.version.replace(".", "-"),
|
|
93
|
+
namespace=self.namespace,
|
|
94
|
+
)
|
|
95
|
+
with open(self.ingress_yaml_path, "w") as f:
|
|
96
|
+
f.write(ingress_yaml)
|
|
97
|
+
print("ingress_yaml_path: ", self.ingress_yaml_path)
|
|
98
|
+
|
|
99
|
+
def inject_dockerfile(self) -> None:
|
|
100
|
+
dockerfile = DEFAULT_DOCKERFILE
|
|
101
|
+
with open(self.dockerfile_path, "w") as f:
|
|
102
|
+
f.write(dockerfile)
|
|
103
|
+
print("dockerfile_path: ", self.dockerfile_path)
|
|
104
|
+
|
|
105
|
+
def inject_pyproject_toml(self) -> None:
|
|
106
|
+
pyproject_toml = DEFAULT_PYPROJECT_TOML
|
|
107
|
+
with open(self.pyproject_toml_path, "r") as f:
|
|
108
|
+
existing_pyproject_toml = f.read()
|
|
109
|
+
if pyproject_toml.strip() not in existing_pyproject_toml.strip():
|
|
110
|
+
with open(self.pyproject_toml_path, "a") as f:
|
|
111
|
+
f.write(pyproject_toml)
|
|
112
|
+
print("pyproject_toml_path: ", self.pyproject_toml_path)
|
|
113
|
+
|
|
114
|
+
def inject(self) -> None:
|
|
115
|
+
self.inject_deployment()
|
|
116
|
+
self.inject_service_config()
|
|
117
|
+
self.inject_ingress()
|
|
118
|
+
self.inject_dockerfile()
|
|
119
|
+
self.inject_pyproject_toml()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
DEFAULT_DEPLOYMENT_YAML = """
|
|
2
|
+
apiVersion: apps/v1
|
|
3
|
+
kind: Deployment
|
|
4
|
+
metadata:
|
|
5
|
+
name: {service_name}
|
|
6
|
+
namespace: {namespace}
|
|
7
|
+
spec:
|
|
8
|
+
replicas: 1
|
|
9
|
+
selector:
|
|
10
|
+
matchLabels:
|
|
11
|
+
app: {service_name}
|
|
12
|
+
template:
|
|
13
|
+
metadata:
|
|
14
|
+
labels:
|
|
15
|
+
app: {service_name}
|
|
16
|
+
spec:
|
|
17
|
+
imagePullSecrets:
|
|
18
|
+
- name: aliyun-regcred
|
|
19
|
+
containers:
|
|
20
|
+
- name: {service_name}
|
|
21
|
+
image: crpi-cev6qq28wwgwwj0y.cn-beijing.personal.cr.aliyuncs.com/nexthci/sf-{name}:{version}
|
|
22
|
+
imagePullPolicy: Always
|
|
23
|
+
ports:
|
|
24
|
+
- containerPort: 8000
|
|
25
|
+
env:
|
|
26
|
+
{env}
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
apiVersion: v1
|
|
30
|
+
kind: Service
|
|
31
|
+
metadata:
|
|
32
|
+
name: {service_name}
|
|
33
|
+
namespace: {namespace}
|
|
34
|
+
annotations:
|
|
35
|
+
metadata: {sf_metadata}
|
|
36
|
+
spec:
|
|
37
|
+
type: ClusterIP
|
|
38
|
+
selector:
|
|
39
|
+
app: {service_name}
|
|
40
|
+
ports:
|
|
41
|
+
- protocol: TCP
|
|
42
|
+
port: 80
|
|
43
|
+
targetPort: 8000
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
DEFAULT_TRAEFIK_INGRESS_YAML = """
|
|
47
|
+
apiVersion: traefik.io/v1alpha1
|
|
48
|
+
kind: Middleware
|
|
49
|
+
metadata:
|
|
50
|
+
name: strip-prefix-sf-{name}-{version}v
|
|
51
|
+
namespace: {namespace}
|
|
52
|
+
spec:
|
|
53
|
+
stripPrefix:
|
|
54
|
+
prefixes:
|
|
55
|
+
- /api/v1/{name}-{version}
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
apiVersion: traefik.io/v1alpha1
|
|
60
|
+
kind: IngressRoute
|
|
61
|
+
metadata:
|
|
62
|
+
name: sf-{name}-{version}v
|
|
63
|
+
namespace: {namespace}
|
|
64
|
+
spec:
|
|
65
|
+
entryPoints:
|
|
66
|
+
- web
|
|
67
|
+
routes:
|
|
68
|
+
- match: PathPrefix(`/api/v1/{name}-{version}/openapi.json`)
|
|
69
|
+
kind: Rule
|
|
70
|
+
services:
|
|
71
|
+
- name: sf-{name}-{version}v
|
|
72
|
+
namespace: {namespace}
|
|
73
|
+
port: 80
|
|
74
|
+
middlewares:
|
|
75
|
+
- name: strip-prefix-sf-{name}-{version}v
|
|
76
|
+
namespace: {namespace}
|
|
77
|
+
- name: cors
|
|
78
|
+
namespace: {namespace}
|
|
79
|
+
|
|
80
|
+
- match: PathPrefix(`/api/v1/{name}-{version}/docs`)
|
|
81
|
+
kind: Rule
|
|
82
|
+
services:
|
|
83
|
+
- name: sf-{name}-{version}v
|
|
84
|
+
namespace: {namespace}
|
|
85
|
+
port: 80
|
|
86
|
+
middlewares:
|
|
87
|
+
- name: strip-prefix-sf-{name}-{version}v
|
|
88
|
+
namespace: {namespace}
|
|
89
|
+
- name: cors
|
|
90
|
+
namespace: {namespace}
|
|
91
|
+
|
|
92
|
+
- match: PathPrefix(`/api/v1/{name}-{version}`)
|
|
93
|
+
kind: Rule
|
|
94
|
+
services:
|
|
95
|
+
- name: sf-{name}-{version}v
|
|
96
|
+
namespace: {namespace}
|
|
97
|
+
port: 80
|
|
98
|
+
middlewares:
|
|
99
|
+
- name: strip-prefix-sf-{name}-{version}v
|
|
100
|
+
namespace: {namespace}
|
|
101
|
+
- name: cors
|
|
102
|
+
namespace: {namespace}
|
|
103
|
+
- name: jwt-auth
|
|
104
|
+
namespace: {namespace}
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
DEFAULT_DOCKERFILE = """
|
|
108
|
+
FROM crpi-cev6qq28wwgwwj0y.cn-beijing.personal.cr.aliyuncs.com/nexthci/service-forge:latest
|
|
109
|
+
|
|
110
|
+
WORKDIR /app
|
|
111
|
+
|
|
112
|
+
COPY . ./service
|
|
113
|
+
|
|
114
|
+
WORKDIR /app
|
|
115
|
+
RUN uv sync
|
|
116
|
+
|
|
117
|
+
ENV PYTHONPATH=/app/service:/app:/app/src
|
|
118
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
119
|
+
|
|
120
|
+
WORKDIR /app/service
|
|
121
|
+
|
|
122
|
+
RUN chmod +x start.sh
|
|
123
|
+
|
|
124
|
+
CMD ["./start.sh"]
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
DEFAULT_PYPROJECT_TOML = """
|
|
128
|
+
|
|
129
|
+
[tool.uv.sources]
|
|
130
|
+
service-forge = { workspace = true }
|
|
131
|
+
"""
|