Add scalable deployment planning and guided releases
This commit is contained in:
@@ -68,9 +68,7 @@ class DeploymentPlan:
|
||||
"installation_id": self.installation_id,
|
||||
"desired_spec_sha256": self.desired_spec_sha256,
|
||||
"desired_compose_sha256": self.desired_compose_sha256,
|
||||
"desired_environment_fingerprint": (
|
||||
self.desired_environment_fingerprint
|
||||
),
|
||||
"desired_environment_fingerprint": (self.desired_environment_fingerprint),
|
||||
"blocked": self.blocked,
|
||||
"actions": [action.to_dict() for action in self.actions],
|
||||
"checks": [check.to_dict() for check in self.checks],
|
||||
@@ -151,7 +149,9 @@ def build_plan(
|
||||
and previous_services == desired_services
|
||||
):
|
||||
actions.append(
|
||||
PlanAction("noop", "installation", "Desired state matches the last receipt.")
|
||||
PlanAction(
|
||||
"noop", "installation", "Desired state matches the last receipt."
|
||||
)
|
||||
)
|
||||
|
||||
checks = list(static_checks(spec, paths))
|
||||
@@ -179,6 +179,9 @@ def static_checks(spec: InstallationSpec, paths: BundlePaths) -> tuple[Check, ..
|
||||
images["components.redis.image"] = spec.components.redis.image
|
||||
if spec.components.mail.mode == "test-mail":
|
||||
images["components.mail.image"] = spec.components.mail.image
|
||||
if spec.components.storage.mode == "garage":
|
||||
images["components.storage.image"] = spec.components.storage.image
|
||||
images["components.load_balancer.image"] = spec.components.load_balancer.image
|
||||
|
||||
for label, image in images.items():
|
||||
if image_is_unpublished(image):
|
||||
@@ -248,7 +251,7 @@ def static_checks(spec: InstallationSpec, paths: BundlePaths) -> tuple[Check, ..
|
||||
required = {"MASTER_KEY_B64", "DATABASE_URL"}
|
||||
if spec.components.redis.mode != "disabled":
|
||||
required.add("REDIS_URL")
|
||||
if spec.components.storage.mode == "s3":
|
||||
if spec.components.storage.mode in {"s3", "garage"}:
|
||||
required.update(
|
||||
{
|
||||
"FILE_STORAGE_S3_ENDPOINT_URL",
|
||||
@@ -258,6 +261,17 @@ def static_checks(spec: InstallationSpec, paths: BundlePaths) -> tuple[Check, ..
|
||||
"FILE_STORAGE_S3_BUCKET",
|
||||
}
|
||||
)
|
||||
if spec.components.storage.mode == "garage":
|
||||
required.update(
|
||||
{
|
||||
"GARAGE_DEFAULT_ACCESS_KEY",
|
||||
"GARAGE_DEFAULT_SECRET_KEY",
|
||||
"GARAGE_DEFAULT_BUCKET",
|
||||
"GARAGE_RPC_SECRET",
|
||||
"GARAGE_ADMIN_TOKEN",
|
||||
"GARAGE_METRICS_TOKEN",
|
||||
}
|
||||
)
|
||||
missing = sorted(name for name in required if not values.get(name))
|
||||
checks.append(
|
||||
Check(
|
||||
@@ -268,7 +282,9 @@ def static_checks(spec: InstallationSpec, paths: BundlePaths) -> tuple[Check, ..
|
||||
if missing
|
||||
else "Required runtime secret references are populated."
|
||||
),
|
||||
"Re-run configure with the required external service values." if missing else "",
|
||||
"Re-run configure with the required external service values."
|
||||
if missing
|
||||
else "",
|
||||
)
|
||||
)
|
||||
if paths.env.exists():
|
||||
@@ -312,6 +328,39 @@ def static_checks(spec: InstallationSpec, paths: BundlePaths) -> tuple[Check, ..
|
||||
"Include files-data in backup/restore drills or configure S3 storage.",
|
||||
)
|
||||
)
|
||||
if spec.components.storage.mode == "garage":
|
||||
checks.append(
|
||||
Check(
|
||||
"storage.garage.single_node",
|
||||
"warning",
|
||||
(
|
||||
"Managed Garage is persistent S3-compatible storage, but "
|
||||
"this Compose profile runs one Garage node without data redundancy."
|
||||
),
|
||||
(
|
||||
"Use an external multi-node Garage/S3 service and tested "
|
||||
"backup/restore for an availability-sensitive installation."
|
||||
),
|
||||
)
|
||||
)
|
||||
checks.append(
|
||||
Check(
|
||||
"topology.load_balancing",
|
||||
"ok",
|
||||
(
|
||||
"Managed HAProxy balances "
|
||||
f"{spec.replicas.web} WebUI and {spec.replicas.api} API replica(s)."
|
||||
),
|
||||
)
|
||||
)
|
||||
if spec.replicas.worker > 1:
|
||||
checks.append(
|
||||
Check(
|
||||
"topology.worker_scaling",
|
||||
"ok",
|
||||
f"{spec.replicas.worker} workers share the configured Redis queues.",
|
||||
)
|
||||
)
|
||||
return tuple(checks)
|
||||
|
||||
|
||||
@@ -566,9 +615,7 @@ def _endpoint_check(
|
||||
)
|
||||
|
||||
|
||||
def _run_command(
|
||||
argv: Sequence[str], cwd: Path
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
def _run_command(argv: Sequence[str], cwd: Path) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
list(argv),
|
||||
cwd=cwd,
|
||||
|
||||
Reference in New Issue
Block a user