quantum-framework 0.18.0__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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deploy Service for Quantum Admin
|
|
3
|
+
Handles building and deploying Quantum applications
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
import uuid
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Optional, Dict, Any, List
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DeployStatus(Enum):
|
|
19
|
+
PENDING = "pending"
|
|
20
|
+
RUNNING = "running"
|
|
21
|
+
COMPLETED = "completed"
|
|
22
|
+
FAILED = "failed"
|
|
23
|
+
CANCELLED = "cancelled"
|
|
24
|
+
ROLLED_BACK = "rolled_back"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DeployStep(Enum):
|
|
28
|
+
PREPARE = "prepare"
|
|
29
|
+
BUILD = "build"
|
|
30
|
+
PUSH = "push"
|
|
31
|
+
DEPLOY = "deploy"
|
|
32
|
+
HEALTH = "health"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class StepResult:
|
|
37
|
+
name: str
|
|
38
|
+
status: str # pending, running, completed, failed, skipped
|
|
39
|
+
message: str = ""
|
|
40
|
+
started_at: Optional[datetime] = None
|
|
41
|
+
completed_at: Optional[datetime] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Deployment:
|
|
46
|
+
id: str
|
|
47
|
+
project_id: int
|
|
48
|
+
project_name: str
|
|
49
|
+
environment: str
|
|
50
|
+
branch: str
|
|
51
|
+
strategy: str
|
|
52
|
+
status: DeployStatus
|
|
53
|
+
steps: List[StepResult] = field(default_factory=list)
|
|
54
|
+
logs: str = ""
|
|
55
|
+
started_at: Optional[datetime] = None
|
|
56
|
+
completed_at: Optional[datetime] = None
|
|
57
|
+
triggered_by: str = "user"
|
|
58
|
+
error_message: Optional[str] = None
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"id": self.id,
|
|
63
|
+
"project_id": self.project_id,
|
|
64
|
+
"project_name": self.project_name,
|
|
65
|
+
"environment": self.environment,
|
|
66
|
+
"branch": self.branch,
|
|
67
|
+
"strategy": self.strategy,
|
|
68
|
+
"status": self.status.value,
|
|
69
|
+
"steps": [{"name": s.name, "status": s.status, "message": s.message} for s in self.steps],
|
|
70
|
+
"logs": self.logs,
|
|
71
|
+
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
72
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
73
|
+
"triggered_by": self.triggered_by,
|
|
74
|
+
"error_message": self.error_message
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DeployService:
|
|
79
|
+
"""Service for deploying Quantum applications"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, quantum_root: str = None):
|
|
82
|
+
"""
|
|
83
|
+
Initialize deploy service
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
quantum_root: Root directory of Quantum installation
|
|
87
|
+
"""
|
|
88
|
+
self.quantum_root = quantum_root or os.environ.get(
|
|
89
|
+
"QUANTUM_ROOT",
|
|
90
|
+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
91
|
+
)
|
|
92
|
+
self._deployments: Dict[str, Deployment] = {}
|
|
93
|
+
self._lock = threading.Lock()
|
|
94
|
+
|
|
95
|
+
def _log(self, deployment: Deployment, message: str):
|
|
96
|
+
"""Add log message to deployment"""
|
|
97
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
98
|
+
deployment.logs += f"[{timestamp}] {message}\n"
|
|
99
|
+
logger.info(f"[Deploy {deployment.id}] {message}")
|
|
100
|
+
|
|
101
|
+
def _update_step(self, deployment: Deployment, step_name: str, status: str, message: str = ""):
|
|
102
|
+
"""Update step status"""
|
|
103
|
+
for step in deployment.steps:
|
|
104
|
+
if step.name == step_name:
|
|
105
|
+
step.status = status
|
|
106
|
+
step.message = message
|
|
107
|
+
if status == "running":
|
|
108
|
+
step.started_at = datetime.now()
|
|
109
|
+
elif status in ("completed", "failed", "skipped"):
|
|
110
|
+
step.completed_at = datetime.now()
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# A name that matches no step wrote nowhere and said nothing — a
|
|
114
|
+
# typo here made a whole step's status silently vanish from the
|
|
115
|
+
# screen, which then showed it as still pending.
|
|
116
|
+
logger.warning(
|
|
117
|
+
"deployment %s has no step named %r; the %r update was dropped",
|
|
118
|
+
deployment.id, step_name, status)
|
|
119
|
+
|
|
120
|
+
# =========================================================================
|
|
121
|
+
# Deployment Operations
|
|
122
|
+
# =========================================================================
|
|
123
|
+
|
|
124
|
+
def create_deployment(
|
|
125
|
+
self,
|
|
126
|
+
project_id: int,
|
|
127
|
+
project_name: str,
|
|
128
|
+
environment: str,
|
|
129
|
+
branch: str = "main",
|
|
130
|
+
strategy: str = "rolling",
|
|
131
|
+
triggered_by: str = "user"
|
|
132
|
+
) -> Deployment:
|
|
133
|
+
"""
|
|
134
|
+
Create a new deployment
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
project_id: Project database ID
|
|
138
|
+
project_name: Project name
|
|
139
|
+
environment: Target environment (dev, staging, production)
|
|
140
|
+
branch: Git branch to deploy
|
|
141
|
+
strategy: Deployment strategy (rolling, blue-green, recreate)
|
|
142
|
+
triggered_by: Who triggered the deployment
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Deployment object
|
|
146
|
+
"""
|
|
147
|
+
deploy_id = str(uuid.uuid4())[:8]
|
|
148
|
+
|
|
149
|
+
deployment = Deployment(
|
|
150
|
+
id=deploy_id,
|
|
151
|
+
project_id=project_id,
|
|
152
|
+
project_name=project_name,
|
|
153
|
+
environment=environment,
|
|
154
|
+
branch=branch,
|
|
155
|
+
strategy=strategy,
|
|
156
|
+
status=DeployStatus.PENDING,
|
|
157
|
+
steps=[
|
|
158
|
+
StepResult(name="prepare", status="pending"),
|
|
159
|
+
StepResult(name="build", status="pending"),
|
|
160
|
+
StepResult(name="push", status="pending"),
|
|
161
|
+
StepResult(name="deploy", status="pending"),
|
|
162
|
+
StepResult(name="health", status="pending"),
|
|
163
|
+
],
|
|
164
|
+
started_at=datetime.now(),
|
|
165
|
+
triggered_by=triggered_by
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
with self._lock:
|
|
169
|
+
self._deployments[deploy_id] = deployment
|
|
170
|
+
|
|
171
|
+
return deployment
|
|
172
|
+
|
|
173
|
+
def start_deployment(self, deploy_id: str, run_async: bool = True) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Start a deployment
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
deploy_id: Deployment ID
|
|
179
|
+
run_async: Run in background thread
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if started successfully
|
|
183
|
+
"""
|
|
184
|
+
deployment = self._deployments.get(deploy_id)
|
|
185
|
+
if not deployment:
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
if deployment.status != DeployStatus.PENDING:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
if run_async:
|
|
192
|
+
thread = threading.Thread(target=self._run_deployment, args=(deployment,))
|
|
193
|
+
thread.daemon = True
|
|
194
|
+
thread.start()
|
|
195
|
+
else:
|
|
196
|
+
self._run_deployment(deployment)
|
|
197
|
+
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
def _mark_failed(self, deployment: Deployment, step_name: str):
|
|
201
|
+
"""Registra a falha do deploy no proprio objeto.
|
|
202
|
+
|
|
203
|
+
Um passo que devolvia False fazia `return` daqui e mais nada: o
|
|
204
|
+
status continuava RUNNING para sempre. E a rota que a interface le
|
|
205
|
+
(`main.py`) responde
|
|
206
|
+
|
|
207
|
+
"completed": status in ("completed", "failed", "cancelled", ...)
|
|
208
|
+
"failed": status == "failed"
|
|
209
|
+
|
|
210
|
+
entao um build que falhou aparecia como `completed=false,
|
|
211
|
+
failed=false` — um spinner que nunca para, sem erro nenhum na tela,
|
|
212
|
+
para um deploy que ja tinha terminado e dado errado. So o
|
|
213
|
+
`_step_prepare` marcava FAILED por conta propria; os outros quatro
|
|
214
|
+
passos, nao.
|
|
215
|
+
"""
|
|
216
|
+
deployment.status = DeployStatus.FAILED
|
|
217
|
+
deployment.completed_at = datetime.now()
|
|
218
|
+
if not deployment.error_message:
|
|
219
|
+
passo = next(
|
|
220
|
+
(s for s in deployment.steps if s.name == step_name), None)
|
|
221
|
+
deployment.error_message = (
|
|
222
|
+
getattr(passo, 'message', None)
|
|
223
|
+
or f"the '{step_name}' step failed"
|
|
224
|
+
)
|
|
225
|
+
self._log(deployment,
|
|
226
|
+
f"Deployment failed at the '{step_name}' step: "
|
|
227
|
+
f"{deployment.error_message}")
|
|
228
|
+
|
|
229
|
+
def _run_deployment(self, deployment: Deployment):
|
|
230
|
+
"""Execute deployment steps"""
|
|
231
|
+
deployment.status = DeployStatus.RUNNING
|
|
232
|
+
self._log(deployment, f"Starting deployment to {deployment.environment}")
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# Step 1: Prepare
|
|
236
|
+
if not self._step_prepare(deployment):
|
|
237
|
+
return self._mark_failed(deployment, "prepare")
|
|
238
|
+
|
|
239
|
+
# Step 2: Build
|
|
240
|
+
if not self._step_build(deployment):
|
|
241
|
+
return self._mark_failed(deployment, "build")
|
|
242
|
+
|
|
243
|
+
# Step 3: Push (skip for local deployments)
|
|
244
|
+
if deployment.environment == "local":
|
|
245
|
+
self._update_step(deployment, "push", "skipped", "Skipped for local deployment")
|
|
246
|
+
self._log(deployment, "Push step skipped for local deployment")
|
|
247
|
+
else:
|
|
248
|
+
if not self._step_push(deployment):
|
|
249
|
+
return self._mark_failed(deployment, "push")
|
|
250
|
+
|
|
251
|
+
# Step 4: Deploy
|
|
252
|
+
if not self._step_deploy(deployment):
|
|
253
|
+
return self._mark_failed(deployment, "deploy")
|
|
254
|
+
|
|
255
|
+
# Step 5: Health Check
|
|
256
|
+
if not self._step_health(deployment):
|
|
257
|
+
return self._mark_failed(deployment, "health")
|
|
258
|
+
|
|
259
|
+
# Success!
|
|
260
|
+
deployment.status = DeployStatus.COMPLETED
|
|
261
|
+
deployment.completed_at = datetime.now()
|
|
262
|
+
self._log(deployment, "Deployment completed successfully!")
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
deployment.status = DeployStatus.FAILED
|
|
266
|
+
deployment.error_message = str(e)
|
|
267
|
+
deployment.completed_at = datetime.now()
|
|
268
|
+
self._log(deployment, f"Deployment failed: {e}")
|
|
269
|
+
logger.exception(f"Deployment {deployment.id} failed")
|
|
270
|
+
|
|
271
|
+
def _step_prepare(self, deployment: Deployment) -> bool:
|
|
272
|
+
"""Prepare step: validate configuration and environment"""
|
|
273
|
+
self._update_step(deployment, "prepare", "running", "Validating configuration...")
|
|
274
|
+
self._log(deployment, "Preparing deployment...")
|
|
275
|
+
|
|
276
|
+
# Check project directory exists
|
|
277
|
+
project_dir = os.path.join(self.quantum_root, "projects", deployment.project_name)
|
|
278
|
+
|
|
279
|
+
# For now, we'll use the examples directory as a fallback
|
|
280
|
+
if not os.path.exists(project_dir):
|
|
281
|
+
project_dir = os.path.join(self.quantum_root, "examples")
|
|
282
|
+
|
|
283
|
+
if not os.path.exists(project_dir):
|
|
284
|
+
self._update_step(deployment, "prepare", "failed", "Project directory not found")
|
|
285
|
+
self._log(deployment, f"ERROR: Project directory not found: {project_dir}")
|
|
286
|
+
deployment.status = DeployStatus.FAILED
|
|
287
|
+
deployment.error_message = "Project directory not found"
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
self._update_step(deployment, "prepare", "completed", "Configuration validated")
|
|
291
|
+
self._log(deployment, f"Project directory: {project_dir}")
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
def _step_build(self, deployment: Deployment) -> bool:
|
|
295
|
+
"""Build step: compile Quantum files"""
|
|
296
|
+
self._update_step(deployment, "build", "running", "Compiling Quantum files...")
|
|
297
|
+
self._log(deployment, "Building application...")
|
|
298
|
+
|
|
299
|
+
# The Quantum CLI moved from src/cli/runner.py to the installed
|
|
300
|
+
# package (FRAMEWORK_PLAN Fase 1.1), so this always took the
|
|
301
|
+
# "not found" branch.
|
|
302
|
+
cli_path = os.path.join(self.quantum_root, "quantum", "cli", "runner.py")
|
|
303
|
+
|
|
304
|
+
if not os.path.exists(cli_path):
|
|
305
|
+
# It used to log "Quantum CLI not found, skipping compilation" and
|
|
306
|
+
# then mark the step "completed / Build successful" anyway. A
|
|
307
|
+
# deployment that never compiled anything reported a green build.
|
|
308
|
+
self._log(deployment, f"Quantum CLI not found at {cli_path}")
|
|
309
|
+
self._update_step(
|
|
310
|
+
deployment, "build", "failed",
|
|
311
|
+
"Quantum CLI not found — nothing was compiled"
|
|
312
|
+
)
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
# Really parse the project's .q files with the real parser, instead of
|
|
316
|
+
# the time.sleep(1) + "Build completed: 0 errors, 0 warnings" this used
|
|
317
|
+
# to print without invoking anything.
|
|
318
|
+
#
|
|
319
|
+
# Parsing is what "build" means for a .q project — there is no compile
|
|
320
|
+
# step to a binary — so this is the whole check, not a stand-in for
|
|
321
|
+
# one. A file that does not parse is a 400 in production, which is
|
|
322
|
+
# exactly what a deploy gate should stop.
|
|
323
|
+
project_dir = os.path.join(self.quantum_root, "projects", deployment.project_name)
|
|
324
|
+
if not os.path.exists(project_dir):
|
|
325
|
+
project_dir = os.path.join(self.quantum_root, "examples")
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
from quantum.core.parser import QuantumParser
|
|
329
|
+
except ImportError as e:
|
|
330
|
+
self._log(deployment, f"ERROR: cannot import the Quantum parser: {e}")
|
|
331
|
+
self._update_step(deployment, "build", "failed",
|
|
332
|
+
"Quantum parser not importable")
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
parser = QuantumParser()
|
|
336
|
+
ok, failures = 0, []
|
|
337
|
+
for root, _dirs, files in os.walk(project_dir):
|
|
338
|
+
for fname in files:
|
|
339
|
+
if not fname.endswith('.q'):
|
|
340
|
+
continue
|
|
341
|
+
path = os.path.join(root, fname)
|
|
342
|
+
try:
|
|
343
|
+
with open(path, encoding='utf-8', errors='ignore') as fh:
|
|
344
|
+
parser.parse(fh.read())
|
|
345
|
+
ok += 1
|
|
346
|
+
except Exception as e:
|
|
347
|
+
failures.append((os.path.relpath(path, project_dir), str(e).split('\n')[0]))
|
|
348
|
+
|
|
349
|
+
for rel, err in failures:
|
|
350
|
+
self._log(deployment, f" FAIL {rel}: {err[:160]}")
|
|
351
|
+
|
|
352
|
+
if failures:
|
|
353
|
+
self._log(deployment,
|
|
354
|
+
f"Build failed: {ok} file(s) parsed, {len(failures)} failed")
|
|
355
|
+
self._update_step(
|
|
356
|
+
deployment, "build", "failed",
|
|
357
|
+
f"{len(failures)} file(s) do not parse"
|
|
358
|
+
)
|
|
359
|
+
deployment.status = DeployStatus.FAILED
|
|
360
|
+
deployment.error_message = f"{len(failures)} .q file(s) failed to parse"
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
if ok == 0:
|
|
364
|
+
self._log(deployment, f"No .q files found under {project_dir}")
|
|
365
|
+
self._update_step(deployment, "build", "failed",
|
|
366
|
+
"No .q files to build")
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
self._log(deployment, f"Build completed: {ok} file(s) parsed, 0 errors")
|
|
370
|
+
self._update_step(deployment, "build", "completed",
|
|
371
|
+
f"{ok} file(s) parsed")
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
# A note that covers this step and the deploy ones below.
|
|
375
|
+
#
|
|
376
|
+
# These slept for half a second and then reported "Image pushed to
|
|
377
|
+
# registry", "Deployment successful", "Local server started on
|
|
378
|
+
# http://localhost:8000" and "Health checks passed: Application is
|
|
379
|
+
# healthy". Nothing was pushed, deployed or checked. The comment inside
|
|
380
|
+
# each one said "in a real implementation, we would…" — invisible from
|
|
381
|
+
# the screen, which showed a green pipeline.
|
|
382
|
+
#
|
|
383
|
+
# Telling someone their application is deployed and healthy when nothing
|
|
384
|
+
# ran is the most expensive lie this codebase told. So: the health check
|
|
385
|
+
# is implemented for real below, and every step that cannot do its work
|
|
386
|
+
# now FAILS and says why. A deploy button that reports failure is
|
|
387
|
+
# useless; one that reports success is dangerous.
|
|
388
|
+
_NOT_IMPLEMENTED = (
|
|
389
|
+
"not implemented: this step does no work. It used to report success "
|
|
390
|
+
"anyway. See PRODUCTION_READINESS.md."
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def _step_push(self, deployment: Deployment) -> bool:
|
|
394
|
+
"""Push step: push a Docker image to the registry. Not implemented."""
|
|
395
|
+
self._update_step(deployment, "push", "running", "Pushing to registry...")
|
|
396
|
+
|
|
397
|
+
self._update_step(deployment, "push", "failed", self._NOT_IMPLEMENTED)
|
|
398
|
+
self._log(deployment,
|
|
399
|
+
"push: no image is built or pushed — step not implemented")
|
|
400
|
+
return False
|
|
401
|
+
|
|
402
|
+
def _step_deploy(self, deployment: Deployment) -> bool:
|
|
403
|
+
"""Deploy step: deploy to target environment"""
|
|
404
|
+
self._update_step(deployment, "deploy", "running", f"Deploying to {deployment.environment}...")
|
|
405
|
+
self._log(deployment, f"Deploying to {deployment.environment} using {deployment.strategy} strategy...")
|
|
406
|
+
|
|
407
|
+
# Deployment logic based on environment
|
|
408
|
+
if deployment.environment == "local":
|
|
409
|
+
return self._deploy_local(deployment)
|
|
410
|
+
elif deployment.environment == "docker":
|
|
411
|
+
return self._deploy_docker(deployment)
|
|
412
|
+
elif deployment.environment == "ssh":
|
|
413
|
+
return self._deploy_ssh(deployment)
|
|
414
|
+
else:
|
|
415
|
+
self._update_step(
|
|
416
|
+
deployment, "deploy", "failed",
|
|
417
|
+
f"unknown environment {deployment.environment!r}")
|
|
418
|
+
self._log(deployment,
|
|
419
|
+
f"deploy: no handler for environment "
|
|
420
|
+
f"{deployment.environment!r}")
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
def _deploy_local(self, deployment: Deployment) -> bool:
|
|
424
|
+
"""Deploy to the local environment. Not implemented."""
|
|
425
|
+
self._update_step(deployment, "deploy", "failed", self._NOT_IMPLEMENTED)
|
|
426
|
+
self._log(deployment,
|
|
427
|
+
"deploy(local): no server is started — step not implemented. "
|
|
428
|
+
"Run the app yourself with `quantum start`.")
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
def _deploy_docker(self, deployment: Deployment) -> bool:
|
|
432
|
+
"""Deploy with Docker. Not implemented.
|
|
433
|
+
|
|
434
|
+
DockerService can pull, create, start and stop containers, but there
|
|
435
|
+
is no image build anywhere in the admin, so there is nothing to run.
|
|
436
|
+
"""
|
|
437
|
+
self._update_step(deployment, "deploy", "failed", self._NOT_IMPLEMENTED)
|
|
438
|
+
self._log(deployment,
|
|
439
|
+
"deploy(docker): no image is built and no container is "
|
|
440
|
+
"started — step not implemented")
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
def _deploy_ssh(self, deployment: Deployment) -> bool:
|
|
444
|
+
"""Deploy over SSH. Not implemented."""
|
|
445
|
+
self._update_step(deployment, "deploy", "failed", self._NOT_IMPLEMENTED)
|
|
446
|
+
self._log(deployment,
|
|
447
|
+
"deploy(ssh): nothing is copied and nothing is restarted — "
|
|
448
|
+
"step not implemented")
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
def _step_health(self, deployment: Deployment, timeout: float = 10.0) -> bool:
|
|
452
|
+
"""Health check step: actually request the health endpoint.
|
|
453
|
+
|
|
454
|
+
This one CAN be done honestly — it is an HTTP GET — so it is. It was
|
|
455
|
+
a sleep followed by "Health checks passed: Application is healthy".
|
|
456
|
+
"""
|
|
457
|
+
self._update_step(deployment, "health", "running", "Running health checks...")
|
|
458
|
+
|
|
459
|
+
url = self._health_url(deployment)
|
|
460
|
+
if not url:
|
|
461
|
+
self._update_step(
|
|
462
|
+
deployment, "health", "skipped",
|
|
463
|
+
"no health URL configured for this environment")
|
|
464
|
+
self._log(deployment,
|
|
465
|
+
"health: no URL configured — nothing was checked")
|
|
466
|
+
return True
|
|
467
|
+
|
|
468
|
+
self._log(deployment, f"health: GET {url}")
|
|
469
|
+
try:
|
|
470
|
+
import urllib.request
|
|
471
|
+
with urllib.request.urlopen(url, timeout=timeout) as response:
|
|
472
|
+
code = response.getcode()
|
|
473
|
+
except Exception as exc:
|
|
474
|
+
self._update_step(deployment, "health", "failed",
|
|
475
|
+
f"{url} did not answer: {exc}")
|
|
476
|
+
self._log(deployment, f"health: FAILED — {exc}")
|
|
477
|
+
return False
|
|
478
|
+
|
|
479
|
+
if 200 <= code < 400:
|
|
480
|
+
self._update_step(deployment, "health", "completed",
|
|
481
|
+
f"{url} answered {code}")
|
|
482
|
+
self._log(deployment, f"health: {url} answered {code}")
|
|
483
|
+
return True
|
|
484
|
+
|
|
485
|
+
self._update_step(deployment, "health", "failed",
|
|
486
|
+
f"{url} answered {code}")
|
|
487
|
+
self._log(deployment, f"health: FAILED — {url} answered {code}")
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
def _health_url(self, deployment: Deployment) -> Optional[str]:
|
|
491
|
+
"""The health URL for this deployment's environment, if configured."""
|
|
492
|
+
try:
|
|
493
|
+
from .settings_service import get_settings_service
|
|
494
|
+
except ImportError:
|
|
495
|
+
from settings_service import get_settings_service
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
settings = get_settings_service().get_global_settings()
|
|
499
|
+
except Exception:
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
deployment_cfg = settings.get("deployment") or {}
|
|
503
|
+
for env in deployment_cfg.get("environments") or []:
|
|
504
|
+
if isinstance(env, dict) and env.get("name") == deployment.environment:
|
|
505
|
+
if env.get("health_url"):
|
|
506
|
+
return env["health_url"]
|
|
507
|
+
host = env.get("host")
|
|
508
|
+
if host:
|
|
509
|
+
path = deployment_cfg.get("health_check_path", "/health")
|
|
510
|
+
port = deployment_cfg.get("default_port", 8080)
|
|
511
|
+
return f"http://{host}:{port}{path}"
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
# =========================================================================
|
|
515
|
+
# Deployment Management
|
|
516
|
+
# =========================================================================
|
|
517
|
+
|
|
518
|
+
def get_deployment(self, deploy_id: str) -> Optional[Deployment]:
|
|
519
|
+
"""Get deployment by ID"""
|
|
520
|
+
return self._deployments.get(deploy_id)
|
|
521
|
+
|
|
522
|
+
def list_deployments(self, project_id: int = None, limit: int = 20) -> List[Deployment]:
|
|
523
|
+
"""List deployments, optionally filtered by project"""
|
|
524
|
+
deployments = list(self._deployments.values())
|
|
525
|
+
|
|
526
|
+
if project_id:
|
|
527
|
+
deployments = [d for d in deployments if d.project_id == project_id]
|
|
528
|
+
|
|
529
|
+
# Sort by started_at descending
|
|
530
|
+
deployments.sort(key=lambda d: d.started_at or datetime.min, reverse=True)
|
|
531
|
+
|
|
532
|
+
return deployments[:limit]
|
|
533
|
+
|
|
534
|
+
def cancel_deployment(self, deploy_id: str) -> bool:
|
|
535
|
+
"""Cancel a running deployment"""
|
|
536
|
+
deployment = self._deployments.get(deploy_id)
|
|
537
|
+
if not deployment:
|
|
538
|
+
return False
|
|
539
|
+
|
|
540
|
+
if deployment.status not in (DeployStatus.PENDING, DeployStatus.RUNNING):
|
|
541
|
+
return False
|
|
542
|
+
|
|
543
|
+
deployment.status = DeployStatus.CANCELLED
|
|
544
|
+
deployment.completed_at = datetime.now()
|
|
545
|
+
self._log(deployment, "Deployment cancelled by user")
|
|
546
|
+
return True
|
|
547
|
+
|
|
548
|
+
def rollback_deployment(self, deploy_id: str) -> Optional[Deployment]:
|
|
549
|
+
"""
|
|
550
|
+
Rollback a deployment
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
New deployment object for rollback, or None if failed
|
|
554
|
+
"""
|
|
555
|
+
deployment = self._deployments.get(deploy_id)
|
|
556
|
+
if not deployment:
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
# Create rollback deployment
|
|
560
|
+
rollback = self.create_deployment(
|
|
561
|
+
project_id=deployment.project_id,
|
|
562
|
+
project_name=deployment.project_name,
|
|
563
|
+
environment=deployment.environment,
|
|
564
|
+
branch=deployment.branch,
|
|
565
|
+
strategy="recreate", # Rollbacks use recreate strategy
|
|
566
|
+
triggered_by="rollback"
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
self._log(rollback, f"Rolling back deployment {deploy_id}")
|
|
570
|
+
|
|
571
|
+
# Mark original as rolled back
|
|
572
|
+
deployment.status = DeployStatus.ROLLED_BACK
|
|
573
|
+
|
|
574
|
+
# Start rollback
|
|
575
|
+
self.start_deployment(rollback.id)
|
|
576
|
+
|
|
577
|
+
return rollback
|
|
578
|
+
|
|
579
|
+
def rollback_to_version(
|
|
580
|
+
self,
|
|
581
|
+
project_id: int,
|
|
582
|
+
project_name: str,
|
|
583
|
+
environment: str,
|
|
584
|
+
target_version_id: int,
|
|
585
|
+
docker_client=None,
|
|
586
|
+
docker_image: str = None,
|
|
587
|
+
docker_tag: str = None
|
|
588
|
+
) -> Optional[Deployment]:
|
|
589
|
+
"""
|
|
590
|
+
Rollback to a specific deployment version.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
project_id: Project ID
|
|
594
|
+
project_name: Project name for logging
|
|
595
|
+
environment: Target environment
|
|
596
|
+
target_version_id: DeploymentVersion ID to rollback to
|
|
597
|
+
docker_client: Optional Docker client
|
|
598
|
+
docker_image: Docker image to use
|
|
599
|
+
docker_tag: Docker tag to use
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
New deployment object for rollback
|
|
603
|
+
"""
|
|
604
|
+
rollback = self.create_deployment(
|
|
605
|
+
project_id=project_id,
|
|
606
|
+
project_name=project_name,
|
|
607
|
+
environment=environment,
|
|
608
|
+
branch="rollback",
|
|
609
|
+
strategy="recreate",
|
|
610
|
+
triggered_by=f"rollback_to_v{target_version_id}"
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
self._log(rollback, f"Rolling back to version {target_version_id}")
|
|
614
|
+
|
|
615
|
+
# If we have docker info, we can do the actual rollback
|
|
616
|
+
if docker_client and docker_image and docker_tag:
|
|
617
|
+
try:
|
|
618
|
+
# Step 1: Stop current container
|
|
619
|
+
self._update_step(rollback, "prepare", "running")
|
|
620
|
+
container_name = f"{project_name.lower().replace(' ', '-')}-{environment}"
|
|
621
|
+
self._log(rollback, f"Stopping current container: {container_name}")
|
|
622
|
+
|
|
623
|
+
try:
|
|
624
|
+
current = docker_client.containers.get(container_name)
|
|
625
|
+
current.stop(timeout=30)
|
|
626
|
+
current.remove()
|
|
627
|
+
self._log(rollback, f"Stopped and removed: {container_name}")
|
|
628
|
+
except Exception as e:
|
|
629
|
+
self._log(rollback, f"No current container found: {e}")
|
|
630
|
+
|
|
631
|
+
self._update_step(rollback, "prepare", "completed")
|
|
632
|
+
|
|
633
|
+
# Step 2: Start container with old image
|
|
634
|
+
self._update_step(rollback, "deploy", "running")
|
|
635
|
+
full_image = f"{docker_image}:{docker_tag}"
|
|
636
|
+
self._log(rollback, f"Starting container with image: {full_image}")
|
|
637
|
+
|
|
638
|
+
container = docker_client.containers.run(
|
|
639
|
+
full_image,
|
|
640
|
+
name=container_name,
|
|
641
|
+
detach=True,
|
|
642
|
+
ports={'8000/tcp': None}, # Auto-assign port
|
|
643
|
+
restart_policy={"Name": "unless-stopped"}
|
|
644
|
+
)
|
|
645
|
+
self._log(rollback, f"Started container: {container.short_id}")
|
|
646
|
+
self._update_step(rollback, "deploy", "completed")
|
|
647
|
+
|
|
648
|
+
# Step 3: Health check
|
|
649
|
+
self._update_step(rollback, "health", "running")
|
|
650
|
+
import time
|
|
651
|
+
time.sleep(3) # Wait for startup
|
|
652
|
+
|
|
653
|
+
container.reload()
|
|
654
|
+
if container.status == "running":
|
|
655
|
+
self._update_step(rollback, "health", "completed")
|
|
656
|
+
rollback.status = DeployStatus.COMPLETED
|
|
657
|
+
self._log(rollback, "Rollback successful - container is running")
|
|
658
|
+
else:
|
|
659
|
+
self._update_step(rollback, "health", "failed", f"Container status: {container.status}")
|
|
660
|
+
rollback.status = DeployStatus.FAILED
|
|
661
|
+
rollback.error_message = f"Container not running: {container.status}"
|
|
662
|
+
|
|
663
|
+
except Exception as e:
|
|
664
|
+
logger.error(f"Rollback failed: {e}")
|
|
665
|
+
rollback.status = DeployStatus.FAILED
|
|
666
|
+
rollback.error_message = str(e)
|
|
667
|
+
self._log(rollback, f"Rollback failed: {e}")
|
|
668
|
+
else:
|
|
669
|
+
# No Docker client - just mark as completed (dry run)
|
|
670
|
+
self._log(rollback, "Dry run mode - no Docker client provided")
|
|
671
|
+
for step in ["prepare", "build", "push", "deploy", "health"]:
|
|
672
|
+
self._update_step(rollback, step, "skipped", "Dry run")
|
|
673
|
+
rollback.status = DeployStatus.COMPLETED
|
|
674
|
+
|
|
675
|
+
rollback.completed_at = datetime.now()
|
|
676
|
+
return rollback
|
|
677
|
+
|
|
678
|
+
def get_rollback_targets(self, project_id: int, environment_id: int, limit: int = 10) -> List[Dict]:
|
|
679
|
+
"""
|
|
680
|
+
Get available rollback targets for a project/environment.
|
|
681
|
+
|
|
682
|
+
This should be called with database versions, not in-memory deployments.
|
|
683
|
+
"""
|
|
684
|
+
# This is a placeholder - actual implementation needs DB access
|
|
685
|
+
# which should be done in the API endpoint
|
|
686
|
+
return []
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# Singleton instance
|
|
690
|
+
_deploy_service = None
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def get_deploy_service() -> DeployService:
|
|
694
|
+
"""Get singleton instance of DeployService"""
|
|
695
|
+
global _deploy_service
|
|
696
|
+
if _deploy_service is None:
|
|
697
|
+
_deploy_service = DeployService()
|
|
698
|
+
return _deploy_service
|