redock-vps 0.1.1__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.
Files changed (61) hide show
  1. redock/__init__.py +0 -0
  2. redock/application/__init__.py +0 -0
  3. redock/application/add_catalog_item.py +26 -0
  4. redock/application/catalog_commands.py +67 -0
  5. redock/application/check_catalog_item.py +20 -0
  6. redock/application/clean_dns.py +10 -0
  7. redock/application/commands.py +60 -0
  8. redock/application/delete_catalog_item.py +13 -0
  9. redock/application/deploy_app.py +91 -0
  10. redock/application/enrich_catalog_item.py +28 -0
  11. redock/application/ensure_dns.py +17 -0
  12. redock/application/ensure_vps.py +26 -0
  13. redock/application/exceptions.py +78 -0
  14. redock/application/get_catalog_item.py +15 -0
  15. redock/application/list_apps.py +10 -0
  16. redock/application/list_catalog_items.py +14 -0
  17. redock/application/prepare_host.py +21 -0
  18. redock/application/scan_catalog_item.py +74 -0
  19. redock/application/search_docker_hub.py +11 -0
  20. redock/application/stop_app.py +30 -0
  21. redock/application/up.py +94 -0
  22. redock/application/update_catalog_item.py +37 -0
  23. redock/domain/__init__.py +0 -0
  24. redock/domain/catalog_item.py +56 -0
  25. redock/domain/catalog_suggestion.py +16 -0
  26. redock/domain/deployment.py +37 -0
  27. redock/domain/dns_record.py +11 -0
  28. redock/domain/docker_hub.py +11 -0
  29. redock/domain/docker_hub_image.py +12 -0
  30. redock/domain/ports.py +137 -0
  31. redock/domain/scan_result.py +25 -0
  32. redock/domain/value_objects.py +81 -0
  33. redock/domain/vps.py +22 -0
  34. redock/domain/vuln_scorer.py +52 -0
  35. redock/infrastructure/__init__.py +0 -0
  36. redock/infrastructure/caddy_router.py +10 -0
  37. redock/infrastructure/config.py +26 -0
  38. redock/infrastructure/docker_hub_search.py +25 -0
  39. redock/infrastructure/docker_scout_scanner.py +78 -0
  40. redock/infrastructure/hetzner_dns_provider.py +120 -0
  41. redock/infrastructure/hetzner_provisioner.py +71 -0
  42. redock/infrastructure/infomaniak_metadata_enricher.py +97 -0
  43. redock/infrastructure/json_repository.py +73 -0
  44. redock/infrastructure/printing_dns_provider.py +75 -0
  45. redock/infrastructure/public_dns_resolver.py +16 -0
  46. redock/infrastructure/remote_docker_runtime.py +59 -0
  47. redock/infrastructure/requests_image_details_fetcher.py +146 -0
  48. redock/infrastructure/sqlite_catalog_repository.py +129 -0
  49. redock/infrastructure/ssh_host_preparer.py +65 -0
  50. redock/infrastructure/trivy_scanner.py +45 -0
  51. redock/interfaces/__init__.py +0 -0
  52. redock/interfaces/cli.py +814 -0
  53. redock/interfaces/composition.py +133 -0
  54. redock/interfaces/rich_renderer.py +72 -0
  55. redock/server-assets/Caddyfile +3 -0
  56. redock/server-assets/docker-compose.caddy.yml +23 -0
  57. redock_vps-0.1.1.dist-info/METADATA +250 -0
  58. redock_vps-0.1.1.dist-info/RECORD +61 -0
  59. redock_vps-0.1.1.dist-info/WHEEL +4 -0
  60. redock_vps-0.1.1.dist-info/entry_points.txt +2 -0
  61. redock_vps-0.1.1.dist-info/licenses/LICENSE +21 -0
redock/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ from redock.application.catalog_commands import AddCatalogItemCommand
2
+ from redock.application.exceptions import CatalogItemAlreadyExists
3
+ from redock.domain.catalog_item import CatalogItem
4
+ from redock.domain.ports import CatalogRepository
5
+
6
+
7
+ class AddCatalogItemUseCase:
8
+ def __init__(self, repo: CatalogRepository) -> None:
9
+ self._repo = repo
10
+
11
+ def execute(self, cmd: AddCatalogItemCommand) -> CatalogItem:
12
+ item = CatalogItem.from_image(
13
+ cmd.image,
14
+ port=cmd.port,
15
+ volume_mount=cmd.volume_mount,
16
+ entrypoint=cmd.entrypoint,
17
+ user=cmd.user,
18
+ user_id=cmd.user_id,
19
+ hub_link=cmd.hub_link,
20
+ project_link=cmd.project_link,
21
+ env=cmd.env,
22
+ )
23
+ if self._repo.get(item.id) is not None:
24
+ raise CatalogItemAlreadyExists(item.id)
25
+ self._repo.add(item)
26
+ return item
@@ -0,0 +1,67 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class AddCatalogItemCommand:
6
+ image: str
7
+ port: int | None = None
8
+ volume_mount: str | None = None
9
+ entrypoint: str | None = None
10
+ user: str | None = None
11
+ user_id: int | None = None
12
+ hub_link: str | None = None
13
+ project_link: str | None = None
14
+ env: dict[str, str] = field(default_factory=dict)
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class GetCatalogItemCommand:
19
+ id: str
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ListCatalogItemsCommand:
24
+ checked_only: bool = False
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class UpdateCatalogItemCommand:
29
+ id: str
30
+ image: str | None = None
31
+ port: int | None = None
32
+ volume_mount: str | None = None
33
+ entrypoint: str | None = None
34
+ user: str | None = None
35
+ user_id: int | None = None
36
+ hub_link: str | None = None
37
+ project_link: str | None = None
38
+ env: dict[str, str] | None = None
39
+ unset_env: tuple[str, ...] = ()
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class DeleteCatalogItemCommand:
44
+ id: str
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class CheckCatalogItemCommand:
49
+ id: str
50
+ checked: bool = True
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class SearchDockerHubCommand:
55
+ query: str
56
+ page_size: int = 25
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class EnrichCatalogItemCommand:
61
+ image: str
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class ScanCatalogItemCommand:
66
+ id: str
67
+ dry_run: bool = False
@@ -0,0 +1,20 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from redock.application.catalog_commands import CheckCatalogItemCommand
4
+ from redock.application.exceptions import CatalogItemNotFound
5
+ from redock.domain.catalog_item import CatalogItem
6
+ from redock.domain.ports import CatalogRepository
7
+
8
+
9
+ class CheckCatalogItemUseCase:
10
+ def __init__(self, repo: CatalogRepository) -> None:
11
+ self._repo = repo
12
+
13
+ def execute(self, cmd: CheckCatalogItemCommand) -> CatalogItem:
14
+ item = self._repo.get(cmd.id)
15
+ if item is None:
16
+ raise CatalogItemNotFound(cmd.id)
17
+ item.checked = cmd.checked
18
+ item.updated_at = datetime.now(tz=timezone.utc)
19
+ self._repo.update(item)
20
+ return item
@@ -0,0 +1,10 @@
1
+ from redock.application.commands import CleanDnsCommand
2
+ from redock.domain.ports import DnsProvider
3
+
4
+
5
+ class CleanDnsUseCase:
6
+ def __init__(self, provider: DnsProvider) -> None:
7
+ self._provider = provider
8
+
9
+ def execute(self, cmd: CleanDnsCommand) -> None:
10
+ self._provider.delete_records(cmd.subdomain)
@@ -0,0 +1,60 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from redock.domain.value_objects import IPv4, Slug, Subdomain
4
+ from redock.domain.vps import Vps
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class EnsureVpsCommand:
9
+ region: str
10
+ server_type: str
11
+ ssh_key_name: str
12
+ label: str = "redock=true"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class PrepareHostCommand:
17
+ vps: Vps
18
+ base_domain: str
19
+ acme_email: str
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class EnsureDnsCommand:
24
+ subdomain: Subdomain
25
+ target_ip: IPv4
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class DeployAppCommand:
30
+ template_id: str
31
+ slug: Slug
32
+ subdomain: Subdomain
33
+ vps: Vps
34
+ env: dict[str, str] = field(default_factory=dict)
35
+ upgrade: bool = False
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class StopAppCommand:
40
+ slug: Slug
41
+ purge: bool = False
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class CleanDnsCommand:
46
+ subdomain: Subdomain
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class UpCommand:
51
+ template_id: str
52
+ slug: Slug
53
+ base_domain: str
54
+ acme_email: str
55
+ region: str
56
+ server_type: str
57
+ ssh_key_name: str
58
+ label: str = "redock=true"
59
+ env: dict[str, str] = field(default_factory=dict)
60
+ upgrade: bool = False
@@ -0,0 +1,13 @@
1
+ from redock.application.catalog_commands import DeleteCatalogItemCommand
2
+ from redock.application.exceptions import CatalogItemNotFound
3
+ from redock.domain.ports import CatalogRepository
4
+
5
+
6
+ class DeleteCatalogItemUseCase:
7
+ def __init__(self, repo: CatalogRepository) -> None:
8
+ self._repo = repo
9
+
10
+ def execute(self, cmd: DeleteCatalogItemCommand) -> None:
11
+ if self._repo.get(cmd.id) is None:
12
+ raise CatalogItemNotFound(cmd.id)
13
+ self._repo.delete(cmd.id)
@@ -0,0 +1,91 @@
1
+ import os
2
+ from datetime import datetime, timezone
3
+
4
+ from redock.application.commands import DeployAppCommand
5
+ from redock.application.exceptions import DeploymentFailed, MissingPort, UnknownTemplate, UnapprovedTemplate
6
+ from redock.domain.deployment import Deployment, DeploymentStatus
7
+ from redock.domain.ports import CatalogRepository, ContainerRuntime, ContainerSpec, DeploymentRepository, ProxyRouter
8
+ from redock.domain.value_objects import ContainerName, VolumeName
9
+
10
+
11
+ def _resolve_env(env: dict[str, str]) -> dict[str, str]:
12
+ resolved: dict[str, str] = {}
13
+ for k, v in env.items():
14
+ if v.startswith("$"):
15
+ var = v[1:]
16
+ if var not in os.environ:
17
+ raise DeploymentFailed(
18
+ f"env var ${var} referenced in catalog but not set on this host"
19
+ )
20
+ resolved[k] = os.environ[var]
21
+ else:
22
+ resolved[k] = v
23
+ return resolved
24
+
25
+
26
+ class DeployAppUseCase:
27
+ def __init__(
28
+ self,
29
+ runtime: ContainerRuntime,
30
+ router: ProxyRouter,
31
+ repo: DeploymentRepository,
32
+ catalog: CatalogRepository,
33
+ ) -> None:
34
+ self._runtime = runtime
35
+ self._router = router
36
+ self._repo = repo
37
+ self._catalog = catalog
38
+
39
+ def execute(self, cmd: DeployAppCommand) -> Deployment:
40
+ item = self._catalog.get(cmd.template_id)
41
+ if item is None:
42
+ raise UnknownTemplate(cmd.template_id)
43
+ if not item.checked:
44
+ raise UnapprovedTemplate(cmd.template_id)
45
+ if item.port is None:
46
+ raise MissingPort(cmd.template_id)
47
+
48
+ existing = self._repo.get(cmd.slug)
49
+ if existing is not None and existing.status == DeploymentStatus.RUNNING:
50
+ if self._runtime.is_running(cmd.vps, existing.container_name):
51
+ if not cmd.upgrade:
52
+ return existing
53
+ self._runtime.stop(cmd.vps, existing.container_name)
54
+ self._runtime.remove(cmd.vps, existing.container_name, remove_volume=False)
55
+
56
+ container_name = ContainerName.for_slug(cmd.slug)
57
+ volume_name = VolumeName.for_slug(cmd.slug) if item.volume_mount else None
58
+ labels = self._router.labels_for(cmd.subdomain, item.port)
59
+
60
+ spec = ContainerSpec(
61
+ image=item.image,
62
+ container_name=str(container_name),
63
+ volume_name=str(volume_name) if volume_name else None,
64
+ volume_mount=item.volume_mount,
65
+ labels=labels,
66
+ internal_port=item.port,
67
+ env=_resolve_env({**item.env, **cmd.env}),
68
+ )
69
+
70
+ deployment = Deployment(
71
+ slug=cmd.slug,
72
+ template_id=cmd.template_id,
73
+ subdomain=cmd.subdomain,
74
+ container_name=container_name,
75
+ volume_name=volume_name,
76
+ vps_id=cmd.vps.id,
77
+ status=DeploymentStatus.PENDING,
78
+ created_at=datetime.now(tz=timezone.utc),
79
+ )
80
+ self._repo.save(deployment)
81
+
82
+ try:
83
+ self._runtime.run(cmd.vps, spec)
84
+ except Exception as exc:
85
+ deployment.mark_failed(str(exc))
86
+ self._repo.save(deployment)
87
+ raise DeploymentFailed(str(exc)) from exc
88
+
89
+ deployment.mark_running()
90
+ self._repo.save(deployment)
91
+ return deployment
@@ -0,0 +1,28 @@
1
+ from redock.application.catalog_commands import EnrichCatalogItemCommand
2
+ from redock.domain.catalog_suggestion import CatalogSuggestion
3
+ from redock.domain.ports import ImageDetailsFetcher, MetadataEnricher
4
+
5
+
6
+ class EnrichCatalogItemUseCase:
7
+ def __init__(self, fetcher: ImageDetailsFetcher, enricher: MetadataEnricher) -> None:
8
+ self._fetcher = fetcher
9
+ self._enricher = enricher
10
+
11
+ def execute(self, cmd: EnrichCatalogItemCommand) -> CatalogSuggestion:
12
+ details = self._fetcher.fetch(cmd.image)
13
+ readme = details.full_description if details else ""
14
+ ai = self._enricher.enrich(cmd.image, readme)
15
+ registry_user = details.user if details else None
16
+ registry_user_id = details.user_id if details else None
17
+ # If the registry gave a named user but no numeric uid, the AI cannot
18
+ # know the uid either — suppress its guess rather than store a wrong value.
19
+ ai_user_id = ai.user_id if registry_user is None else None
20
+ return CatalogSuggestion(
21
+ port=ai.port,
22
+ volume_mount=ai.volume_mount,
23
+ entrypoint=ai.entrypoint,
24
+ user=registry_user if registry_user is not None else ai.user,
25
+ user_id=registry_user_id if registry_user_id is not None else ai_user_id,
26
+ hub_link=details.hub_link if details else None,
27
+ project_link=ai.project_link,
28
+ )
@@ -0,0 +1,17 @@
1
+ from redock.application.commands import EnsureDnsCommand
2
+ from redock.application.exceptions import ApplicationError
3
+ from redock.domain.ports import DnsProvider
4
+
5
+
6
+ class EnsureDnsUseCase:
7
+ def __init__(self, dns_provider: DnsProvider) -> None:
8
+ self._dns_provider = dns_provider
9
+
10
+ def execute(self, cmd: EnsureDnsCommand) -> None:
11
+ records = self._dns_provider.required_records(cmd.subdomain, cmd.target_ip)
12
+ try:
13
+ self._dns_provider.ensure(records)
14
+ except ApplicationError:
15
+ raise
16
+ except Exception as exc:
17
+ raise ApplicationError(f"DNS setup failed: {exc}") from exc
@@ -0,0 +1,26 @@
1
+ from redock.application.commands import EnsureVpsCommand
2
+ from redock.application.exceptions import VpsCreationFailed
3
+ from redock.domain.ports import VpsProvisioner
4
+ from redock.domain.vps import Vps
5
+
6
+
7
+ class EnsureVpsUseCase:
8
+ def __init__(self, provisioner: VpsProvisioner) -> None:
9
+ self._provisioner = provisioner
10
+
11
+ def execute(self, cmd: EnsureVpsCommand) -> Vps:
12
+ existing = self._provisioner.find_existing(cmd.label)
13
+ if existing is not None:
14
+ return existing
15
+
16
+ try:
17
+ vps = self._provisioner.create(
18
+ region=cmd.region,
19
+ server_type=cmd.server_type,
20
+ ssh_key_name=cmd.ssh_key_name,
21
+ label=cmd.label,
22
+ )
23
+ self._provisioner.wait_until_ssh_ready(vps)
24
+ return vps
25
+ except Exception as exc:
26
+ raise VpsCreationFailed(str(exc)) from exc
@@ -0,0 +1,78 @@
1
+ class ApplicationError(Exception):
2
+ """Base for all application-layer errors."""
3
+
4
+
5
+ class VpsCreationFailed(ApplicationError):
6
+ pass
7
+
8
+
9
+ class HostPreparationFailed(ApplicationError):
10
+ pass
11
+
12
+
13
+ class DnsTimeout(ApplicationError):
14
+ def __init__(self, hostname: str) -> None:
15
+ super().__init__(f"DNS did not resolve for {hostname!r} within the timeout")
16
+ self.hostname = hostname
17
+
18
+
19
+ class DeploymentFailed(ApplicationError):
20
+ pass
21
+
22
+
23
+ class UnknownTemplate(ApplicationError):
24
+ def __init__(self, template_id: str) -> None:
25
+ super().__init__(f"Unknown template: {template_id!r}. Check `redock catalog`.")
26
+ self.template_id = template_id
27
+
28
+
29
+ class AlreadyDeployed(ApplicationError):
30
+ def __init__(self, slug: str) -> None:
31
+ super().__init__(f"Deployment {slug!r} is already running.")
32
+ self.slug = slug
33
+
34
+
35
+ class HttpsTimeout(ApplicationError):
36
+ def __init__(self, url: str) -> None:
37
+ super().__init__(f"HTTPS endpoint {url!r} did not become available within the timeout")
38
+ self.url = url
39
+
40
+
41
+ class CatalogItemNotFound(ApplicationError):
42
+ def __init__(self, item_id: str) -> None:
43
+ super().__init__(f"Catalog item {item_id!r} not found.")
44
+ self.item_id = item_id
45
+
46
+
47
+ class CatalogItemAlreadyExists(ApplicationError):
48
+ def __init__(self, item_id: str) -> None:
49
+ super().__init__(
50
+ f"Catalog item {item_id!r} already exists. Use `redock catalog update` to modify it."
51
+ )
52
+ self.item_id = item_id
53
+
54
+
55
+ class UnapprovedTemplate(ApplicationError):
56
+ def __init__(self, item_id: str) -> None:
57
+ super().__init__(f"{item_id!r} is not checked. Run: redock catalog check {item_id}")
58
+ self.item_id = item_id
59
+
60
+
61
+ class MissingPort(ApplicationError):
62
+ def __init__(self, item_id: str) -> None:
63
+ super().__init__(
64
+ f"{item_id!r} has no port set. Run: redock catalog update {item_id} --port 80"
65
+ )
66
+ self.item_id = item_id
67
+
68
+
69
+ class EnrichmentError(ApplicationError):
70
+ pass
71
+
72
+
73
+ class ScannerNotAvailableError(ApplicationError):
74
+ pass
75
+
76
+
77
+ class ScanError(ApplicationError):
78
+ pass
@@ -0,0 +1,15 @@
1
+ from redock.application.catalog_commands import GetCatalogItemCommand
2
+ from redock.application.exceptions import CatalogItemNotFound
3
+ from redock.domain.catalog_item import CatalogItem
4
+ from redock.domain.ports import CatalogRepository
5
+
6
+
7
+ class GetCatalogItemUseCase:
8
+ def __init__(self, repo: CatalogRepository) -> None:
9
+ self._repo = repo
10
+
11
+ def execute(self, cmd: GetCatalogItemCommand) -> CatalogItem:
12
+ item = self._repo.get(cmd.id)
13
+ if item is None:
14
+ raise CatalogItemNotFound(cmd.id)
15
+ return item
@@ -0,0 +1,10 @@
1
+ from redock.domain.deployment import Deployment
2
+ from redock.domain.ports import DeploymentRepository
3
+
4
+
5
+ class ListAppsUseCase:
6
+ def __init__(self, repo: DeploymentRepository) -> None:
7
+ self._repo = repo
8
+
9
+ def execute(self) -> list[Deployment]:
10
+ return self._repo.list_all()
@@ -0,0 +1,14 @@
1
+ from redock.application.catalog_commands import ListCatalogItemsCommand
2
+ from redock.domain.catalog_item import CatalogItem
3
+ from redock.domain.ports import CatalogRepository
4
+
5
+
6
+ class ListCatalogItemsUseCase:
7
+ def __init__(self, repo: CatalogRepository) -> None:
8
+ self._repo = repo
9
+
10
+ def execute(self, cmd: ListCatalogItemsCommand) -> list[CatalogItem]:
11
+ items = self._repo.list_all()
12
+ if cmd.checked_only:
13
+ items = [i for i in items if i.checked]
14
+ return items
@@ -0,0 +1,21 @@
1
+ from redock.application.commands import PrepareHostCommand
2
+ from redock.application.exceptions import HostPreparationFailed
3
+ from redock.domain.ports import HostPreparer
4
+
5
+
6
+ class PrepareHostUseCase:
7
+ def __init__(self, preparer: HostPreparer) -> None:
8
+ self._preparer = preparer
9
+
10
+ def execute(self, cmd: PrepareHostCommand) -> None:
11
+ try:
12
+ if self._preparer.is_prepared(cmd.vps):
13
+ return
14
+ self._preparer.prepare(cmd.vps, cmd.base_domain, cmd.acme_email)
15
+ except FileNotFoundError as exc:
16
+ raise HostPreparationFailed(
17
+ f"SSH key file not found: {exc.filename!r} — "
18
+ "set REDOCK_SSH_KEY_PATH to your private key path"
19
+ ) from exc
20
+ except Exception as exc:
21
+ raise HostPreparationFailed(str(exc)) from exc
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+ from datetime import datetime, timezone
3
+
4
+ from redock.application.catalog_commands import ScanCatalogItemCommand
5
+ from redock.application.exceptions import CatalogItemNotFound, ScannerNotAvailableError
6
+ from redock.domain.ports import CatalogRepository, ImageScanner
7
+ from redock.domain.scan_result import ScanSummary, Vulnerability
8
+ from redock.domain.vuln_scorer import VulnScorer
9
+
10
+
11
+ class ScanCatalogItemUseCase:
12
+ def __init__(
13
+ self,
14
+ repo: CatalogRepository,
15
+ scanners: list[ImageScanner],
16
+ scorer: VulnScorer,
17
+ ) -> None:
18
+ self._repo = repo
19
+ self._scanners = scanners
20
+ self._scorer = scorer
21
+
22
+ def execute(self, cmd: ScanCatalogItemCommand) -> ScanSummary:
23
+ item = self._repo.get(cmd.id)
24
+ if item is None:
25
+ raise CatalogItemNotFound(cmd.id)
26
+
27
+ reports = []
28
+ errors: list[Exception] = []
29
+ for scanner in self._scanners:
30
+ try:
31
+ reports.append(scanner.scan(item.image))
32
+ except ScannerNotAvailableError as e:
33
+ errors.append(e)
34
+
35
+ if not reports:
36
+ raise ScannerNotAvailableError(
37
+ f"No scanners available. Errors: {'; '.join(str(e) for e in errors)}"
38
+ )
39
+
40
+ summary = self._scorer.score(reports)
41
+
42
+ if not cmd.dry_run:
43
+ item.score = summary.score
44
+ item.scan_result = _to_dict(summary)
45
+ item.updated_at = datetime.now(tz=timezone.utc)
46
+ self._repo.update(item)
47
+
48
+ return summary
49
+
50
+
51
+ def _to_dict(summary: ScanSummary) -> dict:
52
+ def _counts(vulns: list[Vulnerability]) -> dict:
53
+ c: dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "low": 0}
54
+ for v in vulns:
55
+ key = v.severity.lower() if v.severity.lower() in c else "low"
56
+ c[key] += 1
57
+ return c
58
+
59
+ return {
60
+ "scanned_at": datetime.now(tz=timezone.utc).isoformat(),
61
+ "scanners": {
62
+ r.scanner: {
63
+ "vuln_count": len(r.vulnerabilities),
64
+ **_counts(r.vulnerabilities),
65
+ "score": summary.per_scanner_scores[r.scanner],
66
+ }
67
+ for r in summary.reports
68
+ },
69
+ "merged": {
70
+ "vuln_count": len(summary.merged),
71
+ **_counts(summary.merged),
72
+ },
73
+ "score": summary.score,
74
+ }
@@ -0,0 +1,11 @@
1
+ from redock.application.catalog_commands import SearchDockerHubCommand
2
+ from redock.domain.docker_hub import DockerHubResult
3
+ from redock.domain.ports import DockerHubSearch
4
+
5
+
6
+ class SearchDockerHubUseCase:
7
+ def __init__(self, hub: DockerHubSearch) -> None:
8
+ self._hub = hub
9
+
10
+ def execute(self, cmd: SearchDockerHubCommand) -> list[DockerHubResult]:
11
+ return self._hub.search(cmd.query, cmd.page_size)
@@ -0,0 +1,30 @@
1
+ from redock.application.commands import StopAppCommand
2
+ from redock.application.exceptions import ApplicationError
3
+ from redock.domain.ports import ContainerRuntime, DeploymentRepository
4
+ from redock.domain.vps import Vps
5
+
6
+
7
+ class StopAppUseCase:
8
+ def __init__(self, runtime: ContainerRuntime, repo: DeploymentRepository) -> None:
9
+ self._runtime = runtime
10
+ self._repo = repo
11
+
12
+ def execute(self, cmd: StopAppCommand, vps: Vps) -> None:
13
+ deployment = self._repo.get(cmd.slug)
14
+ if deployment is None:
15
+ raise ApplicationError(f"No deployment found for slug {cmd.slug!r}")
16
+
17
+ try:
18
+ if self._runtime.is_running(vps, deployment.container_name):
19
+ self._runtime.stop(vps, deployment.container_name)
20
+
21
+ if cmd.purge:
22
+ self._runtime.remove(vps, deployment.container_name, remove_volume=True)
23
+ self._repo.delete(cmd.slug)
24
+ else:
25
+ deployment.mark_stopped()
26
+ self._repo.save(deployment)
27
+ except ApplicationError:
28
+ raise
29
+ except Exception as exc:
30
+ raise ApplicationError(f"Failed to stop deployment {cmd.slug!r}: {exc}") from exc