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.

Files changed (75) hide show
  1. service_forge/api/deprecated_websocket_api.py +86 -0
  2. service_forge/api/deprecated_websocket_manager.py +425 -0
  3. service_forge/api/http_api.py +148 -0
  4. service_forge/api/http_api_doc.py +455 -0
  5. service_forge/api/kafka_api.py +126 -0
  6. service_forge/api/routers/service/__init__.py +4 -0
  7. service_forge/api/routers/service/service_router.py +137 -0
  8. service_forge/api/routers/websocket/websocket_manager.py +83 -0
  9. service_forge/api/routers/websocket/websocket_router.py +78 -0
  10. service_forge/api/task_manager.py +141 -0
  11. service_forge/db/__init__.py +1 -0
  12. service_forge/db/database.py +240 -0
  13. service_forge/llm/__init__.py +62 -0
  14. service_forge/llm/llm.py +56 -0
  15. service_forge/model/__init__.py +0 -0
  16. service_forge/model/websocket.py +13 -0
  17. service_forge/proto/foo_input.py +5 -0
  18. service_forge/service.py +288 -0
  19. service_forge/service_config.py +158 -0
  20. service_forge/sft/cli.py +91 -0
  21. service_forge/sft/cmd/config_command.py +67 -0
  22. service_forge/sft/cmd/deploy_service.py +123 -0
  23. service_forge/sft/cmd/list_tars.py +41 -0
  24. service_forge/sft/cmd/service_command.py +149 -0
  25. service_forge/sft/cmd/upload_service.py +36 -0
  26. service_forge/sft/config/injector.py +119 -0
  27. service_forge/sft/config/injector_default_files.py +131 -0
  28. service_forge/sft/config/sf_metadata.py +30 -0
  29. service_forge/sft/config/sft_config.py +153 -0
  30. service_forge/sft/file/__init__.py +0 -0
  31. service_forge/sft/file/ignore_pattern.py +80 -0
  32. service_forge/sft/file/sft_file_manager.py +107 -0
  33. service_forge/sft/kubernetes/kubernetes_manager.py +257 -0
  34. service_forge/sft/util/assert_util.py +25 -0
  35. service_forge/sft/util/logger.py +16 -0
  36. service_forge/sft/util/name_util.py +8 -0
  37. service_forge/sft/util/yaml_utils.py +57 -0
  38. service_forge/utils/__init__.py +0 -0
  39. service_forge/utils/default_type_converter.py +12 -0
  40. service_forge/utils/register.py +39 -0
  41. service_forge/utils/type_converter.py +99 -0
  42. service_forge/utils/workflow_clone.py +124 -0
  43. service_forge/workflow/__init__.py +1 -0
  44. service_forge/workflow/context.py +14 -0
  45. service_forge/workflow/edge.py +24 -0
  46. service_forge/workflow/node.py +184 -0
  47. service_forge/workflow/nodes/__init__.py +8 -0
  48. service_forge/workflow/nodes/control/if_node.py +29 -0
  49. service_forge/workflow/nodes/control/switch_node.py +28 -0
  50. service_forge/workflow/nodes/input/console_input_node.py +26 -0
  51. service_forge/workflow/nodes/llm/query_llm_node.py +41 -0
  52. service_forge/workflow/nodes/nested/workflow_node.py +28 -0
  53. service_forge/workflow/nodes/output/kafka_output_node.py +27 -0
  54. service_forge/workflow/nodes/output/print_node.py +29 -0
  55. service_forge/workflow/nodes/test/if_console_input_node.py +33 -0
  56. service_forge/workflow/nodes/test/time_consuming_node.py +62 -0
  57. service_forge/workflow/port.py +89 -0
  58. service_forge/workflow/trigger.py +24 -0
  59. service_forge/workflow/triggers/__init__.py +6 -0
  60. service_forge/workflow/triggers/a2a_api_trigger.py +255 -0
  61. service_forge/workflow/triggers/fast_api_trigger.py +169 -0
  62. service_forge/workflow/triggers/kafka_api_trigger.py +44 -0
  63. service_forge/workflow/triggers/once_trigger.py +20 -0
  64. service_forge/workflow/triggers/period_trigger.py +26 -0
  65. service_forge/workflow/triggers/websocket_api_trigger.py +184 -0
  66. service_forge/workflow/workflow.py +210 -0
  67. service_forge/workflow/workflow_callback.py +141 -0
  68. service_forge/workflow/workflow_event.py +15 -0
  69. service_forge/workflow/workflow_factory.py +246 -0
  70. service_forge/workflow/workflow_group.py +27 -0
  71. service_forge/workflow/workflow_type.py +52 -0
  72. service_forge-0.1.11.dist-info/METADATA +98 -0
  73. service_forge-0.1.11.dist-info/RECORD +75 -0
  74. service_forge-0.1.11.dist-info/WHEEL +4 -0
  75. 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
+ """