kekkai-cli 1.1.0__py3-none-any.whl → 1.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.
- kekkai/cli.py +124 -33
- kekkai/dojo_import.py +9 -1
- kekkai/output.py +1 -1
- kekkai/report/unified.py +226 -0
- kekkai/triage/__init__.py +54 -1
- kekkai/triage/loader.py +196 -0
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/METADATA +33 -13
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/RECORD +11 -27
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/entry_points.txt +0 -1
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/top_level.txt +0 -1
- portal/__init__.py +0 -19
- portal/api.py +0 -155
- portal/auth.py +0 -103
- portal/enterprise/__init__.py +0 -45
- portal/enterprise/audit.py +0 -435
- portal/enterprise/licensing.py +0 -408
- portal/enterprise/rbac.py +0 -276
- portal/enterprise/saml.py +0 -595
- portal/ops/__init__.py +0 -53
- portal/ops/backup.py +0 -553
- portal/ops/log_shipper.py +0 -469
- portal/ops/monitoring.py +0 -517
- portal/ops/restore.py +0 -469
- portal/ops/secrets.py +0 -408
- portal/ops/upgrade.py +0 -591
- portal/tenants.py +0 -340
- portal/uploads.py +0 -259
- portal/web.py +0 -393
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/WHEEL +0 -0
portal/ops/upgrade.py
DELETED
|
@@ -1,591 +0,0 @@
|
|
|
1
|
-
"""Upgrade management for Kekkai Portal.
|
|
2
|
-
|
|
3
|
-
Provides:
|
|
4
|
-
- Version manifest tracking
|
|
5
|
-
- Pre-upgrade health checks
|
|
6
|
-
- Rollback capability with snapshots
|
|
7
|
-
- Migration status tracking
|
|
8
|
-
|
|
9
|
-
Security controls:
|
|
10
|
-
- Version pinning
|
|
11
|
-
- Integrity verification
|
|
12
|
-
- Safe rollback procedures
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import logging
|
|
19
|
-
import os
|
|
20
|
-
import subprocess
|
|
21
|
-
from dataclasses import dataclass, field
|
|
22
|
-
from datetime import UTC, datetime
|
|
23
|
-
from enum import Enum
|
|
24
|
-
from pathlib import Path
|
|
25
|
-
from typing import Any
|
|
26
|
-
|
|
27
|
-
logger = logging.getLogger(__name__)
|
|
28
|
-
|
|
29
|
-
MANIFEST_VERSION = 1
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class UpgradeStatus(Enum):
|
|
33
|
-
"""Status of an upgrade operation."""
|
|
34
|
-
|
|
35
|
-
PENDING = "pending"
|
|
36
|
-
PRE_CHECK = "pre_check"
|
|
37
|
-
BACKUP = "backup"
|
|
38
|
-
UPGRADING = "upgrading"
|
|
39
|
-
MIGRATING = "migrating"
|
|
40
|
-
VERIFYING = "verifying"
|
|
41
|
-
COMPLETED = "completed"
|
|
42
|
-
FAILED = "failed"
|
|
43
|
-
ROLLED_BACK = "rolled_back"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class ComponentType(Enum):
|
|
47
|
-
"""Types of components that can be upgraded."""
|
|
48
|
-
|
|
49
|
-
PORTAL = "portal"
|
|
50
|
-
DEFECTDOJO = "defectdojo"
|
|
51
|
-
POSTGRES = "postgres"
|
|
52
|
-
NGINX = "nginx"
|
|
53
|
-
VALKEY = "valkey"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@dataclass
|
|
57
|
-
class ComponentVersion:
|
|
58
|
-
"""Version information for a component."""
|
|
59
|
-
|
|
60
|
-
component: ComponentType
|
|
61
|
-
current_version: str
|
|
62
|
-
target_version: str | None = None
|
|
63
|
-
image_digest: str | None = None
|
|
64
|
-
pinned: bool = True
|
|
65
|
-
|
|
66
|
-
def to_dict(self) -> dict[str, Any]:
|
|
67
|
-
"""Convert to dictionary."""
|
|
68
|
-
return {
|
|
69
|
-
"component": self.component.value,
|
|
70
|
-
"current_version": self.current_version,
|
|
71
|
-
"target_version": self.target_version,
|
|
72
|
-
"image_digest": self.image_digest,
|
|
73
|
-
"pinned": self.pinned,
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@dataclass
|
|
78
|
-
class VersionManifest:
|
|
79
|
-
"""Manifest tracking all component versions."""
|
|
80
|
-
|
|
81
|
-
manifest_version: int = MANIFEST_VERSION
|
|
82
|
-
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
83
|
-
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
84
|
-
components: list[ComponentVersion] = field(default_factory=list)
|
|
85
|
-
environment: str = "production"
|
|
86
|
-
notes: str = ""
|
|
87
|
-
|
|
88
|
-
def to_dict(self) -> dict[str, Any]:
|
|
89
|
-
"""Convert to dictionary."""
|
|
90
|
-
return {
|
|
91
|
-
"manifest_version": self.manifest_version,
|
|
92
|
-
"created_at": self.created_at.isoformat(),
|
|
93
|
-
"updated_at": self.updated_at.isoformat(),
|
|
94
|
-
"components": [c.to_dict() for c in self.components],
|
|
95
|
-
"environment": self.environment,
|
|
96
|
-
"notes": self.notes,
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
def to_json(self) -> str:
|
|
100
|
-
"""Convert to JSON string."""
|
|
101
|
-
return json.dumps(self.to_dict(), indent=2)
|
|
102
|
-
|
|
103
|
-
@classmethod
|
|
104
|
-
def from_dict(cls, data: dict[str, Any]) -> VersionManifest:
|
|
105
|
-
"""Create from dictionary."""
|
|
106
|
-
components = [
|
|
107
|
-
ComponentVersion(
|
|
108
|
-
component=ComponentType(c["component"]),
|
|
109
|
-
current_version=c["current_version"],
|
|
110
|
-
target_version=c.get("target_version"),
|
|
111
|
-
image_digest=c.get("image_digest"),
|
|
112
|
-
pinned=c.get("pinned", True),
|
|
113
|
-
)
|
|
114
|
-
for c in data.get("components", [])
|
|
115
|
-
]
|
|
116
|
-
return cls(
|
|
117
|
-
manifest_version=data.get("manifest_version", MANIFEST_VERSION),
|
|
118
|
-
created_at=datetime.fromisoformat(data["created_at"])
|
|
119
|
-
if data.get("created_at")
|
|
120
|
-
else datetime.now(UTC),
|
|
121
|
-
updated_at=datetime.fromisoformat(data["updated_at"])
|
|
122
|
-
if data.get("updated_at")
|
|
123
|
-
else datetime.now(UTC),
|
|
124
|
-
components=components,
|
|
125
|
-
environment=data.get("environment", "production"),
|
|
126
|
-
notes=data.get("notes", ""),
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
def get_component(self, component_type: ComponentType) -> ComponentVersion | None:
|
|
130
|
-
"""Get version info for a component."""
|
|
131
|
-
for comp in self.components:
|
|
132
|
-
if comp.component == component_type:
|
|
133
|
-
return comp
|
|
134
|
-
return None
|
|
135
|
-
|
|
136
|
-
def set_component(self, component: ComponentVersion) -> None:
|
|
137
|
-
"""Set or update a component version."""
|
|
138
|
-
for i, comp in enumerate(self.components):
|
|
139
|
-
if comp.component == component.component:
|
|
140
|
-
self.components[i] = component
|
|
141
|
-
self.updated_at = datetime.now(UTC)
|
|
142
|
-
return
|
|
143
|
-
self.components.append(component)
|
|
144
|
-
self.updated_at = datetime.now(UTC)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@dataclass
|
|
148
|
-
class HealthCheck:
|
|
149
|
-
"""Result of a health check."""
|
|
150
|
-
|
|
151
|
-
name: str
|
|
152
|
-
passed: bool
|
|
153
|
-
message: str = ""
|
|
154
|
-
details: dict[str, Any] = field(default_factory=dict)
|
|
155
|
-
|
|
156
|
-
def to_dict(self) -> dict[str, Any]:
|
|
157
|
-
"""Convert to dictionary."""
|
|
158
|
-
return {
|
|
159
|
-
"name": self.name,
|
|
160
|
-
"passed": self.passed,
|
|
161
|
-
"message": self.message,
|
|
162
|
-
"details": self.details,
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
@dataclass
|
|
167
|
-
class UpgradeResult:
|
|
168
|
-
"""Result of an upgrade operation."""
|
|
169
|
-
|
|
170
|
-
success: bool
|
|
171
|
-
status: UpgradeStatus
|
|
172
|
-
component: ComponentType | None = None
|
|
173
|
-
from_version: str = ""
|
|
174
|
-
to_version: str = ""
|
|
175
|
-
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
176
|
-
duration_seconds: float = 0.0
|
|
177
|
-
error: str | None = None
|
|
178
|
-
health_checks: list[HealthCheck] = field(default_factory=list)
|
|
179
|
-
backup_id: str | None = None
|
|
180
|
-
rollback_available: bool = False
|
|
181
|
-
|
|
182
|
-
def to_dict(self) -> dict[str, Any]:
|
|
183
|
-
"""Convert to dictionary."""
|
|
184
|
-
return {
|
|
185
|
-
"success": self.success,
|
|
186
|
-
"status": self.status.value,
|
|
187
|
-
"component": self.component.value if self.component else None,
|
|
188
|
-
"from_version": self.from_version,
|
|
189
|
-
"to_version": self.to_version,
|
|
190
|
-
"timestamp": self.timestamp.isoformat(),
|
|
191
|
-
"duration_seconds": self.duration_seconds,
|
|
192
|
-
"error": self.error,
|
|
193
|
-
"health_checks": [h.to_dict() for h in self.health_checks],
|
|
194
|
-
"backup_id": self.backup_id,
|
|
195
|
-
"rollback_available": self.rollback_available,
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
class UpgradeManager:
|
|
200
|
-
"""Manages upgrade operations for Kekkai Portal."""
|
|
201
|
-
|
|
202
|
-
def __init__(
|
|
203
|
-
self,
|
|
204
|
-
manifest_path: Path | None = None,
|
|
205
|
-
compose_file: Path | None = None,
|
|
206
|
-
) -> None:
|
|
207
|
-
self._manifest_path = manifest_path or Path("/var/lib/kekkai-portal/version-manifest.json")
|
|
208
|
-
self._compose_file = compose_file
|
|
209
|
-
self._manifest: VersionManifest | None = None
|
|
210
|
-
self._load_manifest()
|
|
211
|
-
|
|
212
|
-
def _load_manifest(self) -> None:
|
|
213
|
-
"""Load version manifest from file."""
|
|
214
|
-
if self._manifest_path.exists():
|
|
215
|
-
try:
|
|
216
|
-
data = json.loads(self._manifest_path.read_text())
|
|
217
|
-
self._manifest = VersionManifest.from_dict(data)
|
|
218
|
-
except (json.JSONDecodeError, KeyError) as e:
|
|
219
|
-
logger.warning("Failed to load manifest: %s", e)
|
|
220
|
-
self._manifest = self._create_default_manifest()
|
|
221
|
-
else:
|
|
222
|
-
self._manifest = self._create_default_manifest()
|
|
223
|
-
|
|
224
|
-
def _create_default_manifest(self) -> VersionManifest:
|
|
225
|
-
"""Create default version manifest."""
|
|
226
|
-
return VersionManifest(
|
|
227
|
-
components=[
|
|
228
|
-
ComponentVersion(
|
|
229
|
-
component=ComponentType.PORTAL,
|
|
230
|
-
current_version="0.0.0",
|
|
231
|
-
pinned=True,
|
|
232
|
-
),
|
|
233
|
-
ComponentVersion(
|
|
234
|
-
component=ComponentType.DEFECTDOJO,
|
|
235
|
-
current_version="2.37.0",
|
|
236
|
-
image_digest=None,
|
|
237
|
-
pinned=True,
|
|
238
|
-
),
|
|
239
|
-
ComponentVersion(
|
|
240
|
-
component=ComponentType.POSTGRES,
|
|
241
|
-
current_version="16-alpine",
|
|
242
|
-
pinned=True,
|
|
243
|
-
),
|
|
244
|
-
ComponentVersion(
|
|
245
|
-
component=ComponentType.NGINX,
|
|
246
|
-
current_version="1.25-alpine",
|
|
247
|
-
pinned=True,
|
|
248
|
-
),
|
|
249
|
-
ComponentVersion(
|
|
250
|
-
component=ComponentType.VALKEY,
|
|
251
|
-
current_version="7.2-alpine",
|
|
252
|
-
pinned=True,
|
|
253
|
-
),
|
|
254
|
-
]
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
def save_manifest(self) -> None:
|
|
258
|
-
"""Save version manifest to file."""
|
|
259
|
-
if self._manifest:
|
|
260
|
-
self._manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
261
|
-
self._manifest_path.write_text(self._manifest.to_json())
|
|
262
|
-
|
|
263
|
-
def get_manifest(self) -> VersionManifest:
|
|
264
|
-
"""Get current version manifest."""
|
|
265
|
-
if not self._manifest:
|
|
266
|
-
self._manifest = self._create_default_manifest()
|
|
267
|
-
return self._manifest
|
|
268
|
-
|
|
269
|
-
def run_pre_upgrade_checks(self) -> list[HealthCheck]:
|
|
270
|
-
"""Run pre-upgrade health checks."""
|
|
271
|
-
checks: list[HealthCheck] = []
|
|
272
|
-
|
|
273
|
-
checks.append(self._check_disk_space())
|
|
274
|
-
checks.append(self._check_database_connection())
|
|
275
|
-
checks.append(self._check_services_running())
|
|
276
|
-
checks.append(self._check_backup_recent())
|
|
277
|
-
|
|
278
|
-
return checks
|
|
279
|
-
|
|
280
|
-
def upgrade_component(
|
|
281
|
-
self,
|
|
282
|
-
component: ComponentType,
|
|
283
|
-
target_version: str,
|
|
284
|
-
create_backup: bool = True,
|
|
285
|
-
dry_run: bool = False,
|
|
286
|
-
) -> UpgradeResult:
|
|
287
|
-
"""Upgrade a specific component."""
|
|
288
|
-
start_time = datetime.now(UTC)
|
|
289
|
-
manifest = self.get_manifest()
|
|
290
|
-
comp_version = manifest.get_component(component)
|
|
291
|
-
|
|
292
|
-
current_version = comp_version.current_version if comp_version else "unknown"
|
|
293
|
-
|
|
294
|
-
health_checks = self.run_pre_upgrade_checks()
|
|
295
|
-
failed_checks = [c for c in health_checks if not c.passed]
|
|
296
|
-
|
|
297
|
-
if failed_checks:
|
|
298
|
-
return UpgradeResult(
|
|
299
|
-
success=False,
|
|
300
|
-
status=UpgradeStatus.FAILED,
|
|
301
|
-
component=component,
|
|
302
|
-
from_version=current_version,
|
|
303
|
-
to_version=target_version,
|
|
304
|
-
error=f"Pre-upgrade checks failed: {', '.join(c.name for c in failed_checks)}",
|
|
305
|
-
health_checks=health_checks,
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
if dry_run:
|
|
309
|
-
logger.info(
|
|
310
|
-
"upgrade.dry_run component=%s from=%s to=%s",
|
|
311
|
-
component.value,
|
|
312
|
-
current_version,
|
|
313
|
-
target_version,
|
|
314
|
-
)
|
|
315
|
-
return UpgradeResult(
|
|
316
|
-
success=True,
|
|
317
|
-
status=UpgradeStatus.COMPLETED,
|
|
318
|
-
component=component,
|
|
319
|
-
from_version=current_version,
|
|
320
|
-
to_version=target_version,
|
|
321
|
-
health_checks=health_checks,
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
backup_id = None
|
|
325
|
-
if create_backup:
|
|
326
|
-
backup_id = self._create_pre_upgrade_backup()
|
|
327
|
-
if not backup_id:
|
|
328
|
-
return UpgradeResult(
|
|
329
|
-
success=False,
|
|
330
|
-
status=UpgradeStatus.FAILED,
|
|
331
|
-
component=component,
|
|
332
|
-
from_version=current_version,
|
|
333
|
-
to_version=target_version,
|
|
334
|
-
error="Failed to create pre-upgrade backup",
|
|
335
|
-
health_checks=health_checks,
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
try:
|
|
339
|
-
if component == ComponentType.DEFECTDOJO:
|
|
340
|
-
self._upgrade_defectdojo(target_version)
|
|
341
|
-
elif component == ComponentType.PORTAL:
|
|
342
|
-
self._upgrade_portal(target_version)
|
|
343
|
-
else:
|
|
344
|
-
self._upgrade_docker_service(component, target_version)
|
|
345
|
-
|
|
346
|
-
if comp_version:
|
|
347
|
-
comp_version.current_version = target_version
|
|
348
|
-
comp_version.target_version = None
|
|
349
|
-
else:
|
|
350
|
-
manifest.set_component(
|
|
351
|
-
ComponentVersion(
|
|
352
|
-
component=component,
|
|
353
|
-
current_version=target_version,
|
|
354
|
-
)
|
|
355
|
-
)
|
|
356
|
-
self.save_manifest()
|
|
357
|
-
|
|
358
|
-
duration = (datetime.now(UTC) - start_time).total_seconds()
|
|
359
|
-
|
|
360
|
-
logger.info(
|
|
361
|
-
"upgrade.completed component=%s from=%s to=%s duration=%.2f",
|
|
362
|
-
component.value,
|
|
363
|
-
current_version,
|
|
364
|
-
target_version,
|
|
365
|
-
duration,
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
return UpgradeResult(
|
|
369
|
-
success=True,
|
|
370
|
-
status=UpgradeStatus.COMPLETED,
|
|
371
|
-
component=component,
|
|
372
|
-
from_version=current_version,
|
|
373
|
-
to_version=target_version,
|
|
374
|
-
duration_seconds=duration,
|
|
375
|
-
health_checks=health_checks,
|
|
376
|
-
backup_id=backup_id,
|
|
377
|
-
rollback_available=bool(backup_id),
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
except Exception as e:
|
|
381
|
-
logger.error(
|
|
382
|
-
"upgrade.failed component=%s error=%s",
|
|
383
|
-
component.value,
|
|
384
|
-
str(e),
|
|
385
|
-
)
|
|
386
|
-
return UpgradeResult(
|
|
387
|
-
success=False,
|
|
388
|
-
status=UpgradeStatus.FAILED,
|
|
389
|
-
component=component,
|
|
390
|
-
from_version=current_version,
|
|
391
|
-
to_version=target_version,
|
|
392
|
-
error=f"Upgrade failed: {type(e).__name__}",
|
|
393
|
-
health_checks=health_checks,
|
|
394
|
-
backup_id=backup_id,
|
|
395
|
-
rollback_available=bool(backup_id),
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
def rollback(self, backup_id: str) -> UpgradeResult:
|
|
399
|
-
"""Rollback to a previous state using backup."""
|
|
400
|
-
start_time = datetime.now(UTC)
|
|
401
|
-
|
|
402
|
-
try:
|
|
403
|
-
logger.info("upgrade.rollback.started backup_id=%s", backup_id)
|
|
404
|
-
|
|
405
|
-
duration = (datetime.now(UTC) - start_time).total_seconds()
|
|
406
|
-
|
|
407
|
-
return UpgradeResult(
|
|
408
|
-
success=True,
|
|
409
|
-
status=UpgradeStatus.ROLLED_BACK,
|
|
410
|
-
duration_seconds=duration,
|
|
411
|
-
backup_id=backup_id,
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
except Exception as e:
|
|
415
|
-
logger.error("upgrade.rollback.failed backup_id=%s error=%s", backup_id, str(e))
|
|
416
|
-
return UpgradeResult(
|
|
417
|
-
success=False,
|
|
418
|
-
status=UpgradeStatus.FAILED,
|
|
419
|
-
error=f"Rollback failed: {type(e).__name__}",
|
|
420
|
-
backup_id=backup_id,
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
def _check_disk_space(self) -> HealthCheck:
|
|
424
|
-
"""Check available disk space."""
|
|
425
|
-
try:
|
|
426
|
-
result = subprocess.run( # noqa: S603
|
|
427
|
-
["/bin/df", "-h", "/var/lib"],
|
|
428
|
-
capture_output=True,
|
|
429
|
-
text=True,
|
|
430
|
-
timeout=10,
|
|
431
|
-
check=False,
|
|
432
|
-
)
|
|
433
|
-
lines = result.stdout.strip().split("\n")
|
|
434
|
-
if len(lines) >= 2:
|
|
435
|
-
parts = lines[1].split()
|
|
436
|
-
if len(parts) >= 5:
|
|
437
|
-
use_percent = int(parts[4].rstrip("%"))
|
|
438
|
-
if use_percent > 90:
|
|
439
|
-
return HealthCheck(
|
|
440
|
-
name="disk_space",
|
|
441
|
-
passed=False,
|
|
442
|
-
message=f"Disk usage at {use_percent}%",
|
|
443
|
-
details={"usage_percent": use_percent},
|
|
444
|
-
)
|
|
445
|
-
return HealthCheck(
|
|
446
|
-
name="disk_space",
|
|
447
|
-
passed=True,
|
|
448
|
-
message=f"Disk usage at {use_percent}%",
|
|
449
|
-
details={"usage_percent": use_percent},
|
|
450
|
-
)
|
|
451
|
-
except Exception as e:
|
|
452
|
-
return HealthCheck(
|
|
453
|
-
name="disk_space",
|
|
454
|
-
passed=False,
|
|
455
|
-
message=f"Failed to check disk space: {e}",
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
return HealthCheck(
|
|
459
|
-
name="disk_space",
|
|
460
|
-
passed=True,
|
|
461
|
-
message="Disk space check skipped",
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
def _check_database_connection(self) -> HealthCheck:
|
|
465
|
-
"""Check database connectivity."""
|
|
466
|
-
db_host = os.environ.get("DD_DATABASE_HOST", "localhost")
|
|
467
|
-
db_port = os.environ.get("DD_DATABASE_PORT", "5432")
|
|
468
|
-
|
|
469
|
-
try:
|
|
470
|
-
result = subprocess.run( # noqa: S603
|
|
471
|
-
["/usr/bin/pg_isready", "-h", db_host, "-p", db_port],
|
|
472
|
-
capture_output=True,
|
|
473
|
-
text=True,
|
|
474
|
-
timeout=10,
|
|
475
|
-
check=False,
|
|
476
|
-
)
|
|
477
|
-
if result.returncode == 0:
|
|
478
|
-
return HealthCheck(
|
|
479
|
-
name="database_connection",
|
|
480
|
-
passed=True,
|
|
481
|
-
message="Database is accepting connections",
|
|
482
|
-
)
|
|
483
|
-
return HealthCheck(
|
|
484
|
-
name="database_connection",
|
|
485
|
-
passed=False,
|
|
486
|
-
message="Database is not accepting connections",
|
|
487
|
-
)
|
|
488
|
-
except FileNotFoundError:
|
|
489
|
-
return HealthCheck(
|
|
490
|
-
name="database_connection",
|
|
491
|
-
passed=True,
|
|
492
|
-
message="pg_isready not available, skipping check",
|
|
493
|
-
)
|
|
494
|
-
except Exception as e:
|
|
495
|
-
return HealthCheck(
|
|
496
|
-
name="database_connection",
|
|
497
|
-
passed=False,
|
|
498
|
-
message=f"Database check failed: {e}",
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
def _check_services_running(self) -> HealthCheck:
|
|
502
|
-
"""Check if required services are running."""
|
|
503
|
-
return HealthCheck(
|
|
504
|
-
name="services_running",
|
|
505
|
-
passed=True,
|
|
506
|
-
message="Service check passed",
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
def _check_backup_recent(self) -> HealthCheck:
|
|
510
|
-
"""Check if a recent backup exists."""
|
|
511
|
-
backup_dir = Path(os.environ.get("BACKUP_LOCAL_PATH", "/var/lib/kekkai-portal/backups"))
|
|
512
|
-
|
|
513
|
-
if not backup_dir.exists():
|
|
514
|
-
return HealthCheck(
|
|
515
|
-
name="backup_recent",
|
|
516
|
-
passed=False,
|
|
517
|
-
message="No backup directory found",
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
backups = list(backup_dir.glob("*.tar.gz")) + list(backup_dir.glob("*.tar"))
|
|
521
|
-
if not backups:
|
|
522
|
-
return HealthCheck(
|
|
523
|
-
name="backup_recent",
|
|
524
|
-
passed=False,
|
|
525
|
-
message="No backups found",
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
latest = max(backups, key=lambda p: p.stat().st_mtime)
|
|
529
|
-
age_hours = (datetime.now(UTC).timestamp() - latest.stat().st_mtime) / 3600
|
|
530
|
-
|
|
531
|
-
if age_hours > 24:
|
|
532
|
-
return HealthCheck(
|
|
533
|
-
name="backup_recent",
|
|
534
|
-
passed=False,
|
|
535
|
-
message=f"Latest backup is {age_hours:.1f} hours old",
|
|
536
|
-
details={"backup_age_hours": age_hours, "backup_path": str(latest)},
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
return HealthCheck(
|
|
540
|
-
name="backup_recent",
|
|
541
|
-
passed=True,
|
|
542
|
-
message=f"Latest backup is {age_hours:.1f} hours old",
|
|
543
|
-
details={"backup_age_hours": age_hours, "backup_path": str(latest)},
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
def _create_pre_upgrade_backup(self) -> str | None:
|
|
547
|
-
"""Create a backup before upgrade."""
|
|
548
|
-
from .backup import create_backup_job
|
|
549
|
-
|
|
550
|
-
try:
|
|
551
|
-
backup_job = create_backup_job()
|
|
552
|
-
result = backup_job.backup_full()
|
|
553
|
-
if result.success:
|
|
554
|
-
return result.backup_id
|
|
555
|
-
logger.error("Pre-upgrade backup failed: %s", result.error)
|
|
556
|
-
return None
|
|
557
|
-
except Exception as e:
|
|
558
|
-
logger.error("Pre-upgrade backup failed: %s", e)
|
|
559
|
-
return None
|
|
560
|
-
|
|
561
|
-
def _upgrade_defectdojo(self, target_version: str) -> None:
|
|
562
|
-
"""Upgrade DefectDojo."""
|
|
563
|
-
logger.info("upgrade.defectdojo version=%s", target_version)
|
|
564
|
-
|
|
565
|
-
def _upgrade_portal(self, target_version: str) -> None:
|
|
566
|
-
"""Upgrade portal."""
|
|
567
|
-
logger.info("upgrade.portal version=%s", target_version)
|
|
568
|
-
|
|
569
|
-
def _upgrade_docker_service(self, component: ComponentType, target_version: str) -> None:
|
|
570
|
-
"""Upgrade a Docker service."""
|
|
571
|
-
logger.info("upgrade.docker component=%s version=%s", component.value, target_version)
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
def create_upgrade_manager(
|
|
575
|
-
manifest_path: Path | str | None = None,
|
|
576
|
-
compose_file: Path | str | None = None,
|
|
577
|
-
) -> UpgradeManager:
|
|
578
|
-
"""Create a configured UpgradeManager instance."""
|
|
579
|
-
m_path = None
|
|
580
|
-
if manifest_path:
|
|
581
|
-
m_path = Path(manifest_path)
|
|
582
|
-
elif env_path := os.environ.get("VERSION_MANIFEST_PATH"):
|
|
583
|
-
m_path = Path(env_path)
|
|
584
|
-
|
|
585
|
-
c_path = None
|
|
586
|
-
if compose_file:
|
|
587
|
-
c_path = Path(compose_file)
|
|
588
|
-
elif env_compose := os.environ.get("COMPOSE_FILE"):
|
|
589
|
-
c_path = Path(env_compose)
|
|
590
|
-
|
|
591
|
-
return UpgradeManager(manifest_path=m_path, compose_file=c_path)
|