service-forge 0.1.18__py3-none-any.whl → 0.1.39__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/__init__.py +0 -0
- service_forge/api/deprecated_websocket_api.py +91 -33
- service_forge/api/deprecated_websocket_manager.py +70 -53
- service_forge/api/http_api.py +205 -55
- service_forge/api/kafka_api.py +113 -25
- service_forge/api/routers/meta_api/meta_api_router.py +57 -0
- service_forge/api/routers/service/service_router.py +42 -6
- service_forge/api/routers/trace/trace_router.py +326 -0
- service_forge/api/routers/websocket/websocket_router.py +69 -1
- service_forge/api/service_studio.py +9 -0
- service_forge/db/database.py +17 -0
- service_forge/execution_context.py +106 -0
- service_forge/frontend/static/assets/CreateNewNodeDialog-DkrEMxSH.js +1 -0
- service_forge/frontend/static/assets/CreateNewNodeDialog-DwFcBiGp.css +1 -0
- service_forge/frontend/static/assets/EditorSidePanel-BNVms9Fq.css +1 -0
- service_forge/frontend/static/assets/EditorSidePanel-DZbB3ILL.js +1 -0
- service_forge/frontend/static/assets/FeedbackPanel-CC8HX7Yo.js +1 -0
- service_forge/frontend/static/assets/FeedbackPanel-ClgniIVk.css +1 -0
- service_forge/frontend/static/assets/FormattedCodeViewer.vue_vue_type_script_setup_true_lang-BNuI1NCs.js +1 -0
- service_forge/frontend/static/assets/NodeDetailWrapper-BqFFM7-r.js +1 -0
- service_forge/frontend/static/assets/NodeDetailWrapper-pZBxv3J0.css +1 -0
- service_forge/frontend/static/assets/TestRunningDialog-D0GrCoYs.js +1 -0
- service_forge/frontend/static/assets/TestRunningDialog-dhXOsPgH.css +1 -0
- service_forge/frontend/static/assets/TracePanelWrapper-B9zvDSc_.js +1 -0
- service_forge/frontend/static/assets/TracePanelWrapper-BiednCrq.css +1 -0
- service_forge/frontend/static/assets/WorkflowEditor-CcaGGbko.js +3 -0
- service_forge/frontend/static/assets/WorkflowEditor-CmasOOYK.css +1 -0
- service_forge/frontend/static/assets/WorkflowList-Copuwi-a.css +1 -0
- service_forge/frontend/static/assets/WorkflowList-LrRJ7B7h.js +1 -0
- service_forge/frontend/static/assets/WorkflowStudio-CthjgII2.css +1 -0
- service_forge/frontend/static/assets/WorkflowStudio-FCyhGD4y.js +2 -0
- service_forge/frontend/static/assets/api-BDer3rj7.css +1 -0
- service_forge/frontend/static/assets/api-DyiqpKJK.js +1 -0
- service_forge/frontend/static/assets/code-editor-DBSql_sc.js +12 -0
- service_forge/frontend/static/assets/el-collapse-item-D4LG0FJ0.css +1 -0
- service_forge/frontend/static/assets/el-empty-D4ZqTl4F.css +1 -0
- service_forge/frontend/static/assets/el-form-item-BWkJzdQ_.css +1 -0
- service_forge/frontend/static/assets/el-input-D6B3r8CH.css +1 -0
- service_forge/frontend/static/assets/el-select-B0XIb2QK.css +1 -0
- service_forge/frontend/static/assets/el-tag-DljBBxJR.css +1 -0
- service_forge/frontend/static/assets/element-ui-D3x2y3TA.js +12 -0
- service_forge/frontend/static/assets/elkjs-Dm5QV7uy.js +24 -0
- service_forge/frontend/static/assets/highlightjs-D4ATuRwX.js +3 -0
- service_forge/frontend/static/assets/index-BMvodlwc.js +2 -0
- service_forge/frontend/static/assets/index-CjSe8i2q.css +1 -0
- service_forge/frontend/static/assets/js-yaml-yTPt38rv.js +32 -0
- service_forge/frontend/static/assets/time-DKCKV6Ug.js +1 -0
- service_forge/frontend/static/assets/ui-components-DQ7-U3pr.js +1 -0
- service_forge/frontend/static/assets/vue-core-DL-LgTX0.js +1 -0
- service_forge/frontend/static/assets/vue-flow-Dn7R8GPr.js +39 -0
- service_forge/frontend/static/index.html +16 -0
- service_forge/frontend/static/vite.svg +1 -0
- service_forge/model/meta_api/__init__.py +0 -0
- service_forge/model/meta_api/schema.py +29 -0
- service_forge/model/trace.py +82 -0
- service_forge/service.py +39 -11
- service_forge/service_config.py +14 -0
- service_forge/sft/cli.py +39 -0
- service_forge/sft/cmd/remote_deploy.py +160 -0
- service_forge/sft/cmd/remote_list_tars.py +111 -0
- service_forge/sft/config/injector.py +54 -7
- service_forge/sft/config/injector_default_files.py +13 -1
- service_forge/sft/config/sf_metadata.py +31 -27
- service_forge/sft/config/sft_config.py +18 -0
- service_forge/sft/util/assert_util.py +0 -1
- service_forge/telemetry.py +66 -0
- service_forge/utils/default_type_converter.py +1 -1
- service_forge/utils/type_converter.py +5 -0
- service_forge/utils/workflow_clone.py +1 -0
- service_forge/workflow/node.py +274 -27
- service_forge/workflow/triggers/fast_api_trigger.py +64 -28
- service_forge/workflow/triggers/websocket_api_trigger.py +66 -38
- service_forge/workflow/workflow.py +140 -37
- service_forge/workflow/workflow_callback.py +27 -4
- service_forge/workflow/workflow_factory.py +14 -0
- {service_forge-0.1.18.dist-info → service_forge-0.1.39.dist-info}/METADATA +4 -1
- service_forge-0.1.39.dist-info/RECORD +134 -0
- service_forge-0.1.18.dist-info/RECORD +0 -83
- {service_forge-0.1.18.dist-info → service_forge-0.1.39.dist-info}/WHEEL +0 -0
- {service_forge-0.1.18.dist-info → service_forge-0.1.39.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>vite-project</title>
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-BMvodlwc.js"></script>
|
|
9
|
+
<link rel="modulepreload" crossorigin href="./assets/vue-core-DL-LgTX0.js">
|
|
10
|
+
<link rel="modulepreload" crossorigin href="./assets/ui-components-DQ7-U3pr.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CjSe8i2q.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="app"></div>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Literal, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
class NodeInputPortSchema(BaseModel):
|
|
6
|
+
"""
|
|
7
|
+
节点的输入端口定义数据
|
|
8
|
+
"""
|
|
9
|
+
name: str
|
|
10
|
+
type: str
|
|
11
|
+
default_value: Optional[str | float | bool | int] = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NodeOutputPortSchema(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
节点的输出端口定义数据
|
|
17
|
+
"""
|
|
18
|
+
name: str
|
|
19
|
+
type: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NodeTypeSchema(BaseModel):
|
|
23
|
+
"""
|
|
24
|
+
节点的定义数据
|
|
25
|
+
"""
|
|
26
|
+
name: str
|
|
27
|
+
is_trigger: bool
|
|
28
|
+
inputs: dict[str, NodeInputPortSchema]
|
|
29
|
+
outputs: dict[str, NodeOutputPortSchema]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import Optional, List, Dict, Any
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
|
|
5
|
+
# Type aliases
|
|
6
|
+
TraceId = str
|
|
7
|
+
SpanId = str
|
|
8
|
+
ParentSpanId = str
|
|
9
|
+
|
|
10
|
+
# Enums
|
|
11
|
+
class SpanKind(IntEnum):
|
|
12
|
+
INTERNAL = 0
|
|
13
|
+
SERVER = 1
|
|
14
|
+
CLIENT = 2
|
|
15
|
+
PRODUCER = 3
|
|
16
|
+
CONSUMER = 4
|
|
17
|
+
UNKNOWN = 5
|
|
18
|
+
|
|
19
|
+
class StatusCode(IntEnum):
|
|
20
|
+
UNSET = 0
|
|
21
|
+
OK = 1
|
|
22
|
+
ERROR = 2
|
|
23
|
+
|
|
24
|
+
# Models
|
|
25
|
+
class Span(BaseModel):
|
|
26
|
+
span_id: SpanId
|
|
27
|
+
parent_span_id: ParentSpanId
|
|
28
|
+
name: str
|
|
29
|
+
kind: SpanKind
|
|
30
|
+
timestamp: str
|
|
31
|
+
duration_nano: int
|
|
32
|
+
status_code: StatusCode
|
|
33
|
+
service_name: str
|
|
34
|
+
workflow_name: Optional[str] = None
|
|
35
|
+
workflow_task_id: Optional[str] = None
|
|
36
|
+
node_name: Optional[str] = None
|
|
37
|
+
attributes: Optional[Dict[str, Any]] = None
|
|
38
|
+
|
|
39
|
+
class TraceListItem(BaseModel):
|
|
40
|
+
trace_id: TraceId
|
|
41
|
+
service_name: str
|
|
42
|
+
workflow_name: Optional[str] = None
|
|
43
|
+
workflow_task_id: Optional[str] = None
|
|
44
|
+
timestamp: str
|
|
45
|
+
duration_nano: int
|
|
46
|
+
span_count: int
|
|
47
|
+
has_error: bool
|
|
48
|
+
status_code: StatusCode
|
|
49
|
+
|
|
50
|
+
class TraceDetail(BaseModel):
|
|
51
|
+
trace_id: TraceId
|
|
52
|
+
service_name: str
|
|
53
|
+
workflow_name: Optional[str] = None
|
|
54
|
+
workflow_task_id: Optional[str] = None
|
|
55
|
+
spans: List[Span]
|
|
56
|
+
start_time: Optional[str] = None
|
|
57
|
+
end_time: Optional[str] = None
|
|
58
|
+
span_count: int
|
|
59
|
+
has_error: bool
|
|
60
|
+
|
|
61
|
+
class GetTraceListParams(BaseModel):
|
|
62
|
+
service_name: Optional[str] = None
|
|
63
|
+
workflow_name: Optional[str] = None
|
|
64
|
+
workflow_task_id: Optional[str] = None
|
|
65
|
+
start_time: Optional[str] = None
|
|
66
|
+
end_time: Optional[str] = None
|
|
67
|
+
limit: Optional[int] = None
|
|
68
|
+
offset: Optional[int] = None
|
|
69
|
+
has_error: Optional[bool] = None
|
|
70
|
+
|
|
71
|
+
class GetTraceListResponse(BaseModel):
|
|
72
|
+
traces: List[TraceListItem]
|
|
73
|
+
total: int
|
|
74
|
+
limit: int
|
|
75
|
+
offset: int
|
|
76
|
+
|
|
77
|
+
class GetTraceDetailParams(BaseModel):
|
|
78
|
+
trace_id: TraceId
|
|
79
|
+
service_name: Optional[str] = None
|
|
80
|
+
|
|
81
|
+
class GetTraceDetailResponse(BaseModel):
|
|
82
|
+
trace: TraceDetail
|
service_forge/service.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import uuid
|
|
4
5
|
import asyncio
|
|
5
6
|
import threading
|
|
6
|
-
import
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from service_forge.telemetry import setup_tracing
|
|
10
|
+
from importlib.metadata import version
|
|
7
11
|
from loguru import logger
|
|
8
12
|
from typing import Callable, AsyncIterator, Awaitable, Any, TYPE_CHECKING
|
|
9
13
|
from service_forge.workflow.node import node_register
|
|
@@ -57,6 +61,7 @@ class Service:
|
|
|
57
61
|
return self.metadata.description
|
|
58
62
|
|
|
59
63
|
async def start(self):
|
|
64
|
+
setup_tracing(service_name=self.name, config=self.config.trace)
|
|
60
65
|
set_service(self)
|
|
61
66
|
|
|
62
67
|
if self.config.enable_http:
|
|
@@ -78,7 +83,7 @@ class Service:
|
|
|
78
83
|
workflow_group = create_workflow_group(
|
|
79
84
|
config_path=self.parse_workflow_path(workflow_config_path),
|
|
80
85
|
service_env=self.service_env,
|
|
81
|
-
_handle_stream_output=self._handle_stream_output,
|
|
86
|
+
_handle_stream_output=self._handle_stream_output,
|
|
82
87
|
_handle_query_user=self._handle_query_user,
|
|
83
88
|
database_manager=self.database_manager,
|
|
84
89
|
)
|
|
@@ -139,23 +144,23 @@ class Service:
|
|
|
139
144
|
|
|
140
145
|
def get_workflow_group_by_id(self, workflow_id: str, allow_none: bool = True) -> WorkflowGroup | None:
|
|
141
146
|
for workflow_group in self.workflow_groups:
|
|
142
|
-
if workflow_group.get_workflow_by_id(workflow_id) is not None:
|
|
147
|
+
if workflow_group.get_workflow_by_id(uuid.UUID(workflow_id)) is not None:
|
|
143
148
|
return workflow_group
|
|
144
149
|
if not allow_none:
|
|
145
150
|
raise ValueError(f"Workflow group with id {workflow_id} not found in service {self.name}")
|
|
146
151
|
return None
|
|
147
152
|
|
|
148
|
-
def trigger_workflow(self, workflow_group: WorkflowGroup, trigger_name: str, **kwargs) -> uuid.UUID:
|
|
153
|
+
def trigger_workflow(self, workflow_group: WorkflowGroup, trigger_name: str, assigned_task_id: uuid.UUID | None, **kwargs) -> uuid.UUID:
|
|
149
154
|
workflow = workflow_group.get_main_workflow(allow_none=False)
|
|
150
|
-
return workflow.trigger(trigger_name, **kwargs)
|
|
155
|
+
return workflow.trigger(trigger_name, assigned_task_id, **kwargs)
|
|
151
156
|
|
|
152
|
-
def trigger_workflow_by_name(self, workflow_name: str, workflow_version: str, trigger_name: str, **kwargs) -> uuid.UUID:
|
|
157
|
+
def trigger_workflow_by_name(self, workflow_name: str, workflow_version: str, trigger_name: str, assigned_task_id: uuid.UUID | None, **kwargs) -> uuid.UUID:
|
|
153
158
|
workflow_group = self.get_workflow_group_by_name(workflow_name, workflow_version, allow_none=False)
|
|
154
|
-
return self.trigger_workflow(workflow_group, trigger_name, **kwargs)
|
|
159
|
+
return self.trigger_workflow(workflow_group, trigger_name, assigned_task_id, **kwargs)
|
|
155
160
|
|
|
156
|
-
def trigger_workflow_by_id(self, workflow_id: str, trigger_name: str, **kwargs) -> uuid.UUID:
|
|
161
|
+
def trigger_workflow_by_id(self, workflow_id: str, trigger_name: str, assigned_task_id: uuid.UUID | None, **kwargs) -> uuid.UUID:
|
|
157
162
|
workflow_group = self.get_workflow_group_by_id(workflow_id, allow_none=False)
|
|
158
|
-
return self.trigger_workflow(workflow_group, trigger_name, **kwargs)
|
|
163
|
+
return self.trigger_workflow(workflow_group, trigger_name, assigned_task_id, **kwargs)
|
|
159
164
|
|
|
160
165
|
def start_workflow(self, workflow_group: WorkflowGroup) -> bool:
|
|
161
166
|
workflow = workflow_group.get_main_workflow(allow_none=False)
|
|
@@ -207,7 +212,7 @@ class Service:
|
|
|
207
212
|
workflow_group = self.get_workflow_group_by_id(workflow_id, allow_none=False)
|
|
208
213
|
return await self.stop_workflow(workflow_group)
|
|
209
214
|
|
|
210
|
-
async def load_workflow_from_config(self, config_path: str = None, config: dict = None) -> uuid.UUID:
|
|
215
|
+
async def load_workflow_from_config(self, config_path: str = None, config: dict = None, debug_version: bool = False) -> uuid.UUID:
|
|
211
216
|
workflow_group = create_workflow_group(
|
|
212
217
|
config_path=config_path,
|
|
213
218
|
config=config,
|
|
@@ -215,6 +220,7 @@ class Service:
|
|
|
215
220
|
_handle_stream_output=self._handle_stream_output,
|
|
216
221
|
_handle_query_user=self._handle_query_user,
|
|
217
222
|
database_manager=self.database_manager,
|
|
223
|
+
debug_version=debug_version,
|
|
218
224
|
)
|
|
219
225
|
|
|
220
226
|
for workflow in workflow_group.workflows:
|
|
@@ -231,10 +237,12 @@ class Service:
|
|
|
231
237
|
self.start_workflow(workflow_group)
|
|
232
238
|
return main_workflow.id
|
|
233
239
|
|
|
234
|
-
def get_service_status(self) -> dict[str, Any]:
|
|
240
|
+
def get_service_status(self, exclude_debug: bool = False) -> dict[str, Any]:
|
|
235
241
|
workflow_statuses = []
|
|
236
242
|
for workflow_group in self.workflow_groups:
|
|
237
243
|
for workflow in workflow_group.workflows:
|
|
244
|
+
if exclude_debug and workflow.debug_version:
|
|
245
|
+
continue
|
|
238
246
|
workflow_id = workflow.id
|
|
239
247
|
workflow_version = workflow.version
|
|
240
248
|
workflow_config = workflow.config
|
|
@@ -257,9 +265,29 @@ class Service:
|
|
|
257
265
|
"description": self.description,
|
|
258
266
|
"workflows": workflow_statuses,
|
|
259
267
|
}
|
|
268
|
+
|
|
269
|
+
def get_workflow_status(self, workflow_id: str) -> dict[str, Any]:
|
|
270
|
+
workflow_group = self.get_workflow_group_by_id(workflow_id, allow_none=False)
|
|
271
|
+
main_workflow = workflow_group.get_workflow_by_id(uuid.UUID(workflow_id))
|
|
272
|
+
is_running = workflow_id in self.workflow_tasks and not self.workflow_tasks[workflow_id].done()
|
|
273
|
+
return {
|
|
274
|
+
"name": main_workflow.name,
|
|
275
|
+
"id": main_workflow.id,
|
|
276
|
+
"version": main_workflow.version,
|
|
277
|
+
"config": main_workflow.config,
|
|
278
|
+
"description": main_workflow.description,
|
|
279
|
+
"status": "running" if is_running else "stopped",
|
|
280
|
+
"debug": main_workflow.debug_version or False
|
|
281
|
+
}
|
|
260
282
|
|
|
261
283
|
@staticmethod
|
|
262
284
|
def from_config(metadata: SfMetadata, service_env: dict[str, Any] = None, config: ServiceConfig = None) -> Service:
|
|
285
|
+
try:
|
|
286
|
+
service_forge_version = version("service-forge")
|
|
287
|
+
logger.info(f"service-forge version: {service_forge_version}")
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.warning(f"Failed to get service-forge version: {e}")
|
|
290
|
+
|
|
263
291
|
if config is not None:
|
|
264
292
|
config_path = None
|
|
265
293
|
else:
|
service_forge/service_config.py
CHANGED
|
@@ -7,6 +7,10 @@ class ServiceFeedbackConfig(BaseModel):
|
|
|
7
7
|
api_url: str
|
|
8
8
|
api_timeout: int = 5
|
|
9
9
|
|
|
10
|
+
class SignozConfig(BaseModel):
|
|
11
|
+
api_url: str
|
|
12
|
+
api_key: str
|
|
13
|
+
|
|
10
14
|
class ServiceDatabaseConfig(BaseModel):
|
|
11
15
|
name: str
|
|
12
16
|
postgres_user: str | None = None
|
|
@@ -25,6 +29,14 @@ class ServiceDatabaseConfig(BaseModel):
|
|
|
25
29
|
redis_port: int | None = None
|
|
26
30
|
redis_password: str | None = None
|
|
27
31
|
|
|
32
|
+
class TraceConfig(BaseModel):
|
|
33
|
+
enable: bool = False
|
|
34
|
+
url: str | None = None
|
|
35
|
+
headers: str | None = None
|
|
36
|
+
arg: float | None = None
|
|
37
|
+
namespace: str | None = None
|
|
38
|
+
hostname: str | None = None
|
|
39
|
+
|
|
28
40
|
class ServiceConfig(BaseModel):
|
|
29
41
|
name: str
|
|
30
42
|
workflows: list[str]
|
|
@@ -36,6 +48,8 @@ class ServiceConfig(BaseModel):
|
|
|
36
48
|
kafka_port: int | None = None
|
|
37
49
|
databases: list[ServiceDatabaseConfig] | None = None
|
|
38
50
|
feedback: ServiceFeedbackConfig | None = None
|
|
51
|
+
signoz: SignozConfig | None = None
|
|
52
|
+
trace: TraceConfig | None = None
|
|
39
53
|
|
|
40
54
|
@classmethod
|
|
41
55
|
def from_yaml_file(cls, filepath: str) -> ServiceConfig:
|
service_forge/sft/cli.py
CHANGED
|
@@ -9,6 +9,8 @@ from service_forge.sft.cmd.upload_service import upload_service
|
|
|
9
9
|
from service_forge.sft.cmd.deploy_service import deploy_service
|
|
10
10
|
from service_forge.sft.cmd.config_command import list_config, get_config, set_config
|
|
11
11
|
from service_forge.sft.cmd.service_command import list_services, delete_service, show_service_logs
|
|
12
|
+
from service_forge.sft.cmd.remote_list_tars import remote_list_tars
|
|
13
|
+
from service_forge.sft.cmd.remote_deploy import remote_deploy_tar, remote_list_and_deploy
|
|
12
14
|
|
|
13
15
|
app = typer.Typer(
|
|
14
16
|
name="sft",
|
|
@@ -33,6 +35,43 @@ def list_tars_command() -> None:
|
|
|
33
35
|
def deploy_service_command(name: str, version: str) -> None:
|
|
34
36
|
deploy_service(name, version)
|
|
35
37
|
|
|
38
|
+
@app.command(name="remote-list")
|
|
39
|
+
def remote_list_tars_command(
|
|
40
|
+
url: str = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
"--url",
|
|
43
|
+
"-u",
|
|
44
|
+
help="Service Center URL (default: http://localhost:5000 or from service_center_address config)"
|
|
45
|
+
)
|
|
46
|
+
) -> None:
|
|
47
|
+
"""List tar packages and their status on remote server"""
|
|
48
|
+
remote_list_tars(url)
|
|
49
|
+
|
|
50
|
+
@app.command(name="remote-deploy")
|
|
51
|
+
def remote_deploy_command(
|
|
52
|
+
filename: str = typer.Argument(help="Filename of the tar package to deploy"),
|
|
53
|
+
url: str = typer.Option(
|
|
54
|
+
None,
|
|
55
|
+
"--url",
|
|
56
|
+
"-u",
|
|
57
|
+
help="Service Center URL (default: http://localhost:5000 or from service_center_address config)"
|
|
58
|
+
)
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Remote deploy specified tar package"""
|
|
61
|
+
remote_deploy_tar(filename, url)
|
|
62
|
+
|
|
63
|
+
@app.command(name="remote-deploy-interactive")
|
|
64
|
+
def remote_deploy_interactive_command(
|
|
65
|
+
url: str = typer.Option(
|
|
66
|
+
None,
|
|
67
|
+
"--url",
|
|
68
|
+
"-u",
|
|
69
|
+
help="Service Center URL (default: http://localhost:5000 or from service_center_address config)"
|
|
70
|
+
)
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Interactive remote deployment of tar packages (list available packages first, then select for deployment)"""
|
|
73
|
+
remote_list_and_deploy(url)
|
|
74
|
+
|
|
36
75
|
config_app = typer.Typer(
|
|
37
76
|
name="config",
|
|
38
77
|
help="Configuration management commands",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
|
|
6
|
+
from service_forge.sft.config.sft_config import sft_config
|
|
7
|
+
|
|
8
|
+
def remote_deploy_tar(filename: str, service_center_url: str = None) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Remote deploy specified tar package from service-center
|
|
11
|
+
"""
|
|
12
|
+
# If URL is not provided, try to get it from configuration
|
|
13
|
+
if not service_center_url:
|
|
14
|
+
service_center_url = getattr(sft_config, 'service_center_address', 'http://localhost:5000')
|
|
15
|
+
|
|
16
|
+
# Ensure URL ends with /
|
|
17
|
+
if not service_center_url.endswith('/'):
|
|
18
|
+
service_center_url += '/'
|
|
19
|
+
|
|
20
|
+
api_url = f"{service_center_url}api/v1/services/deploy-from-tar"
|
|
21
|
+
|
|
22
|
+
log_info(f"Sending deployment request to {api_url}...")
|
|
23
|
+
log_info(f"Tar package to deploy: {filename}")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# Prepare request data
|
|
27
|
+
data = {
|
|
28
|
+
"filename": filename
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Send POST request
|
|
32
|
+
response = requests.post(
|
|
33
|
+
api_url,
|
|
34
|
+
json=data,
|
|
35
|
+
headers={'Content-Type': 'application/json'},
|
|
36
|
+
timeout=300 # 5 minute timeout
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if response.status_code != 200:
|
|
40
|
+
log_error(f"Deployment request failed, status code: {response.status_code}")
|
|
41
|
+
try:
|
|
42
|
+
error_data = response.json()
|
|
43
|
+
log_error(f"Error message: {error_data.get('message', 'Unknown error')}")
|
|
44
|
+
if 'data' in error_data and error_data['data']:
|
|
45
|
+
log_error(f"Details: {json.dumps(error_data['data'], indent=2, ensure_ascii=False)}")
|
|
46
|
+
except:
|
|
47
|
+
log_error(f"Response content: {response.text}")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# Parse response data
|
|
51
|
+
result = response.json()
|
|
52
|
+
|
|
53
|
+
if result.get('code') != 200:
|
|
54
|
+
log_error(f"Deployment failed: {result.get('message', 'Unknown error')}")
|
|
55
|
+
if 'data' in result and result['data']:
|
|
56
|
+
log_error(f"Details: {json.dumps(result['data'], indent=2, ensure_ascii=False)}")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Deployment successful
|
|
60
|
+
data = result.get('data', {})
|
|
61
|
+
service_name = data.get('service_name', 'Unknown')
|
|
62
|
+
version = data.get('version', 'Unknown')
|
|
63
|
+
deploy_output = data.get('deploy_output', '')
|
|
64
|
+
|
|
65
|
+
log_success(f"Successfully deployed service: {service_name} version: {version}")
|
|
66
|
+
|
|
67
|
+
if deploy_output:
|
|
68
|
+
log_info("Deployment output:")
|
|
69
|
+
print(deploy_output)
|
|
70
|
+
|
|
71
|
+
except requests.exceptions.Timeout:
|
|
72
|
+
log_error("Deployment request timed out (exceeded 5 minutes), please check service status or try again later")
|
|
73
|
+
except requests.exceptions.RequestException as e:
|
|
74
|
+
log_error(f"Request failed: {str(e)}")
|
|
75
|
+
log_info(f"Please check if service-center service is running normally and if the URL is correct: {service_center_url}")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
log_error(f"Exception occurred while deploying tar package: {str(e)}")
|
|
78
|
+
|
|
79
|
+
def remote_list_and_deploy(service_center_url: str = None) -> None:
|
|
80
|
+
"""
|
|
81
|
+
List remote tar packages first, then let user select which package to deploy
|
|
82
|
+
"""
|
|
83
|
+
# If URL is not provided, try to get it from configuration
|
|
84
|
+
if not service_center_url:
|
|
85
|
+
service_center_url = getattr(sft_config, 'service_center_address', 'http://localhost:5000')
|
|
86
|
+
|
|
87
|
+
# Ensure URL ends with /
|
|
88
|
+
if not service_center_url.endswith('/'):
|
|
89
|
+
service_center_url += '/'
|
|
90
|
+
|
|
91
|
+
api_url = f"{service_center_url}api/v1/services/tar-list"
|
|
92
|
+
|
|
93
|
+
log_info(f"Getting tar package list from {api_url}...")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# 发送GET请求获取tar包列表
|
|
97
|
+
response = requests.get(api_url, timeout=30)
|
|
98
|
+
|
|
99
|
+
if response.status_code != 200:
|
|
100
|
+
log_error(f"Failed to get tar package list, status code: {response.status_code}")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Parse response data
|
|
104
|
+
result = response.json()
|
|
105
|
+
|
|
106
|
+
if result.get('code') != 200:
|
|
107
|
+
log_error(f"Failed to get tar package list: {result.get('message', 'Unknown error')}")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
tar_files = result.get('data', [])
|
|
111
|
+
|
|
112
|
+
if not tar_files:
|
|
113
|
+
log_info("No tar packages found")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Display tar package list
|
|
117
|
+
log_info("Available tar package list:")
|
|
118
|
+
for i, tar_file in enumerate(tar_files, 1):
|
|
119
|
+
filename = tar_file.get('filename', '-')
|
|
120
|
+
service_name = tar_file.get('service_name', '-')
|
|
121
|
+
version = tar_file.get('version', '-')
|
|
122
|
+
deployed_status = "Deployed" if tar_file.get('deployed_status', False) else "Not Deployed"
|
|
123
|
+
|
|
124
|
+
print(f"{i}. {filename} (service: {service_name}, version: {version}, status: {deployed_status})")
|
|
125
|
+
|
|
126
|
+
# Let user choose
|
|
127
|
+
try:
|
|
128
|
+
choice = input("\nEnter the number of the tar package to deploy (enter 'q' to exit): ").strip()
|
|
129
|
+
|
|
130
|
+
if choice.lower() == 'q':
|
|
131
|
+
log_info("Deployment cancelled")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
index = int(choice) - 1
|
|
135
|
+
if 0 <= index < len(tar_files):
|
|
136
|
+
selected_tar = tar_files[index]
|
|
137
|
+
filename = selected_tar.get('filename')
|
|
138
|
+
|
|
139
|
+
if selected_tar.get('deployed_status', False):
|
|
140
|
+
log_warning(f"Tar package {filename} is already deployed, continue deployment?")
|
|
141
|
+
confirm = input("Enter 'y' to continue, any other key to cancel: ").strip().lower()
|
|
142
|
+
if confirm != 'y':
|
|
143
|
+
log_info("Deployment cancelled")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
log_info(f"Selected for deployment: {filename}")
|
|
147
|
+
remote_deploy_tar(filename, service_center_url)
|
|
148
|
+
else:
|
|
149
|
+
log_error("Invalid selection")
|
|
150
|
+
|
|
151
|
+
except ValueError:
|
|
152
|
+
log_error("Please enter a valid number")
|
|
153
|
+
except KeyboardInterrupt:
|
|
154
|
+
log_info("\nDeployment cancelled")
|
|
155
|
+
|
|
156
|
+
except requests.exceptions.RequestException as e:
|
|
157
|
+
log_error(f"Request failed: {str(e)}")
|
|
158
|
+
log_info(f"Please check if service-center service is running normally and if the URL is correct: {service_center_url}")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
log_error(f"Exception occurred while getting tar package list: {str(e)}")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
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 remote_list_tars(service_center_url: str = None) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Get remote tar package list and status from service-center
|
|
13
|
+
"""
|
|
14
|
+
# If URL is not provided, try to get it from configuration
|
|
15
|
+
if not service_center_url:
|
|
16
|
+
service_center_url = getattr(sft_config, 'service_center_address', 'http://localhost:5000')
|
|
17
|
+
|
|
18
|
+
# Ensure URL ends with /
|
|
19
|
+
if not service_center_url.endswith('/'):
|
|
20
|
+
service_center_url += '/'
|
|
21
|
+
|
|
22
|
+
api_url = f"{service_center_url}api/v1/services/tar-list"
|
|
23
|
+
|
|
24
|
+
log_info(f"Getting tar package list from {api_url}...")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# 发送GET请求
|
|
28
|
+
response = requests.get(api_url, timeout=30)
|
|
29
|
+
|
|
30
|
+
if response.status_code != 200:
|
|
31
|
+
log_error(f"Failed to get tar package list, status code: {response.status_code}")
|
|
32
|
+
try:
|
|
33
|
+
error_data = response.json()
|
|
34
|
+
log_error(f"Error message: {error_data.get('message', 'Unknown error')}")
|
|
35
|
+
except:
|
|
36
|
+
log_error(f"Response content: {response.text}")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Parse response data
|
|
40
|
+
result = response.json()
|
|
41
|
+
|
|
42
|
+
if result.get('code') != 200:
|
|
43
|
+
log_error(f"Failed to get tar package list: {result.get('message', 'Unknown error')}")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
tar_files = result.get('data', [])
|
|
47
|
+
|
|
48
|
+
if not tar_files:
|
|
49
|
+
log_info("No tar packages found")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Use rich table to display results
|
|
53
|
+
console = Console()
|
|
54
|
+
table = Table(title="Remote Server Tar Package List", show_header=True, header_style="bold magenta")
|
|
55
|
+
table.add_column("Filename", style="cyan", no_wrap=True)
|
|
56
|
+
table.add_column("Service Name", style="green", no_wrap=True)
|
|
57
|
+
table.add_column("Version", style="blue", no_wrap=True)
|
|
58
|
+
table.add_column("Size", justify="right", style="yellow")
|
|
59
|
+
table.add_column("Modified Time", style="dim")
|
|
60
|
+
table.add_column("Deploy Status", justify="center", style="bold")
|
|
61
|
+
|
|
62
|
+
for tar_file in tar_files:
|
|
63
|
+
# Format file size
|
|
64
|
+
size = _format_size(tar_file.get('file_size', 0))
|
|
65
|
+
|
|
66
|
+
# Format modification time
|
|
67
|
+
modified_time = _format_time(tar_file.get('modified_time', 0))
|
|
68
|
+
|
|
69
|
+
# Deployment status
|
|
70
|
+
deployed_status = "✅ Deployed" if tar_file.get('deployed_status', False) else "❌ Not Deployed"
|
|
71
|
+
status_style = "green" if tar_file.get('deployed_status', False) else "red"
|
|
72
|
+
|
|
73
|
+
table.add_row(
|
|
74
|
+
tar_file.get('filename', '-'),
|
|
75
|
+
tar_file.get('service_name', '-'),
|
|
76
|
+
tar_file.get('version', '-'),
|
|
77
|
+
size,
|
|
78
|
+
modified_time,
|
|
79
|
+
f"[{status_style}]{deployed_status}[/{status_style}]"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
console.print(table)
|
|
83
|
+
log_success(f"Found {len(tar_files)} tar packages in total")
|
|
84
|
+
|
|
85
|
+
except requests.exceptions.RequestException as e:
|
|
86
|
+
log_error(f"Request failed: {str(e)}")
|
|
87
|
+
log_info(f"Please check if service-center service is running normally and if the URL is correct: {service_center_url}")
|
|
88
|
+
except Exception as e:
|
|
89
|
+
log_error(f"Exception occurred while getting tar package list: {str(e)}")
|
|
90
|
+
|
|
91
|
+
def _format_size(size_bytes: int) -> str:
|
|
92
|
+
"""Format file size"""
|
|
93
|
+
if size_bytes == 0:
|
|
94
|
+
return "0 B"
|
|
95
|
+
|
|
96
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
97
|
+
if size_bytes < 1024.0:
|
|
98
|
+
return f"{size_bytes:.2f} {unit}"
|
|
99
|
+
size_bytes /= 1024.0
|
|
100
|
+
return f"{size_bytes:.2f} TB"
|
|
101
|
+
|
|
102
|
+
def _format_time(timestamp: float) -> str:
|
|
103
|
+
"""Format timestamp"""
|
|
104
|
+
if timestamp == 0:
|
|
105
|
+
return "-"
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
from datetime import datetime
|
|
109
|
+
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
|
110
|
+
except:
|
|
111
|
+
return "-"
|