scalo 2.10.12

Self-regulating runtime for Rust data-plane services. Backpressure, load shedding and adaptive scaling are on by default.
// Project:   scalo
// File:      src/deployment/generate/compose.rs
// Purpose:   Docker Compose fragment generation
// Language:  Rust
//
// License:   Apache-2.0
// Copyright: (c) 2026 HYPERI PTY LIMITED

#![allow(clippy::format_push_string)]

use crate::deployment::contract::DeploymentContract;

// ============================================================================
// Docker Compose fragment
// ============================================================================

/// Generate a Docker Compose service fragment from the deployment contract.
#[must_use]
pub fn generate_compose_fragment(contract: &DeploymentContract) -> String {
    let binary = contract.binary();
    let mut out = String::with_capacity(512);

    // Compose interpolates ${NAME}; the contract's nested `__` separator is not
    // valid there, so flatten it once and reuse for every override below.
    let env_prefix = contract.env_prefix.replace("__", "_");

    // Service definition
    out.push_str(&format!(
        "# Generated by scalo deployment module\nservices:\n  {}:\n",
        contract.app_name
    ));

    // Image
    out.push_str(&format!(
        "    image: {}/{}:${{{env_prefix}_VERSION:-latest}}\n",
        contract.image_registry, contract.app_name,
    ));

    // depends_on
    if !contract.depends_on.is_empty() {
        out.push_str("    depends_on:\n");
        for dep in &contract.depends_on {
            out.push_str(&format!(
                "      {dep}:\n        condition: service_healthy\n"
            ));
        }
    }

    // Ports
    out.push_str("    ports:\n");
    out.push_str(&format!(
        "      - \"{}:{}\"\n",
        contract.metrics_port, contract.metrics_port
    ));
    for p in &contract.extra_ports {
        out.push_str(&format!("      - \"{}:{}\"\n", p.port, p.port));
    }

    // Volumes -- config file mount
    out.push_str("    volumes:\n");
    out.push_str(&format!(
        "      - ./config/{}:{}:ro\n",
        contract.config_filename(),
        contract.config_mount_path,
    ));

    // Healthcheck
    out.push_str(&format!(
        "    healthcheck:\n\
         \x20     test: [\"CMD\", \"curl\", \"-sf\", \"http://localhost:{}{}\"]
      interval: 10s\n\
         \x20     timeout: 3s\n\
         \x20     retries: 5\n",
        contract.metrics_port, contract.health.liveness_path,
    ));

    // Resource limits -- conservative local-development default so a stray
    // loop can't starve the host running `docker compose up`. The generated
    // Helm chart's `resources.limits` is the production knob and supersedes
    // this once the app runs in Kubernetes. Overridable from the environment
    // like the image tag above, so nobody has to hand-edit an AUTOGENERATED
    // file to give a local run more headroom.
    out.push_str("    deploy:\n");
    out.push_str("      resources:\n");
    out.push_str("        limits:\n");
    out.push_str(&format!(
        "          cpus: \"${{{env_prefix}_CPU_LIMIT:-2}}\"\n"
    ));
    out.push_str(&format!(
        "          memory: ${{{env_prefix}_MEM_LIMIT:-1G}}\n"
    ));

    // Entrypoint args
    if !contract.entrypoint_args.is_empty() {
        out.push_str(&format!("    command: [\"{binary}\""));
        for arg in &contract.entrypoint_args {
            out.push_str(&format!(", \"{arg}\""));
        }
        out.push_str("]\n");
    }

    out
}