feat(delivery): production deploy over SSH/SFTP/FTP/FTPS (v2 phase A3-5)
The one path that touches a customer's server, and the one the platform cannot undo. It is human-triggered, confirmed and audited; there is deliberately no code path from a pipeline run into it (D13). - deploy_ssh.py: SshDeployer. Release lands in releases/<ts>-<sha7>/, the post-deploy command runs with cwd there, and only then does `current` move — atomically (relative symlink + mv -T). A failed hook or extraction never moves it, so the previous release keeps serving. Host key is pinned on first success (TOFU); a mismatch is a hard failure. Prunes to the newest five. - deploy_ftp.py: FtpDeployer. FTP has no atomic switch and no rollback except a backup, so a backup is mandatory: every file about to be overwritten is archived under ARTIFACTS_ROOT before anything is uploaded. The artifact is extracted, then the build dir (dist/build/public/out) is chosen — a static deploy must not publish the repository. Archive members are sanitised: absolute paths, `..` and escaping links are refused. - deploy_target.py: the tester that dispatches on `kind`. - deploy_service.py + production_deploys (migration a3b4c5d6e7f4) + 4 routes. One deploy per project at a time; the org slug must be typed back; the target summary reports has_secret, never the secret. Tests: 499 API (+26) with fake SSH/FTP transports; ruff, mypy, alembic clean. Report: docs/plan/reports/agent-3-phase-5.md
This commit is contained in:
@@ -19,9 +19,10 @@ import logging
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.ports.delivery_hooks import DeliveryHooks
|
||||
from app.application.ports.deploy_target import DeployTargetTester, FakeDeployTargetTester
|
||||
from app.application.ports.deploy_target import DeployTargetTester
|
||||
from app.application.ports.repo_workspace import FakeRepoWorkspace, RepoWorkspacePort
|
||||
from app.application.services.delivery_service import DeliveryService
|
||||
from app.application.services.deploy_service import DeployService
|
||||
from app.core.config import settings
|
||||
from app.core.crypto import decrypt
|
||||
from app.infrastructure.adapters.git_workspace import (
|
||||
@@ -35,10 +36,12 @@ from app.infrastructure.repositories.delivery import (
|
||||
AppRuntimeRepository,
|
||||
PipelineRunRepository,
|
||||
PipelineStepRepository,
|
||||
ProductionDeployRepository,
|
||||
ReleaseRepository,
|
||||
)
|
||||
from app.infrastructure.repositories.identity import OrganizationRepository
|
||||
from app.infrastructure.repositories.projects import (
|
||||
DeployTargetRepository,
|
||||
PreviewDomainRepository,
|
||||
ProjectExternalGitRepository,
|
||||
ProjectGitCredentialRepository,
|
||||
@@ -49,7 +52,6 @@ from app.infrastructure.repositories.projects import (
|
||||
logger = logging.getLogger("agy.container.delivery")
|
||||
|
||||
_fake_workspace = FakeRepoWorkspace()
|
||||
_fake_deploy_tester = FakeDeployTargetTester()
|
||||
|
||||
|
||||
def build_repo_workspace(session: AsyncSession) -> RepoWorkspacePort:
|
||||
@@ -186,4 +188,26 @@ class _UnavailableContainerRuntime:
|
||||
|
||||
|
||||
def build_deploy_target_tester(session: AsyncSession) -> DeployTargetTester:
|
||||
return _fake_deploy_tester
|
||||
"""The real tester: SSH/SFTP over asyncssh, FTP/FTPS over aioftp.
|
||||
|
||||
The wizard calls this to validate a user-supplied production target, and it
|
||||
runs the same SSRF check the git adapter does before a byte leaves the host.
|
||||
"""
|
||||
from app.infrastructure.adapters.deploy_target import DispatchingDeployTargetTester
|
||||
|
||||
return DispatchingDeployTargetTester()
|
||||
|
||||
|
||||
def build_deploy_service(session: AsyncSession) -> DeployService:
|
||||
"""Production deploys: human-triggered, confirmed and audited."""
|
||||
from app.infrastructure.adapters.deploy_target import DispatchingDeployTargetTester
|
||||
|
||||
return DeployService(
|
||||
projects=ProjectRepository(session),
|
||||
organizations=OrganizationRepository(session),
|
||||
targets=DeployTargetRepository(session),
|
||||
deploys=ProductionDeployRepository(session),
|
||||
releases=ReleaseRepository(session),
|
||||
runtimes=AppRuntimeRepository(session),
|
||||
tester=DispatchingDeployTargetTester(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user