cargo-rustapi 0.1.443

The official CLI tool for the RustAPI framework. Scaffold new projects, run development servers, and manage database migrations.
//! Deployment Commands
//!
//! Generate deployment configurations and deploy to various platforms.

use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use std::fs;
use std::path::PathBuf;

/// Arguments for deployment commands
#[derive(Subcommand, Debug)]
pub enum DeployArgs {
    /// Generate a Dockerfile for the project
    Docker(DockerArgs),

    /// Deploy to Fly.io
    Fly(FlyArgs),

    /// Deploy to Railway
    Railway(RailwayArgs),

    /// Deploy to Shuttle.rs
    Shuttle(ShuttleArgs),
}

#[derive(Args, Debug)]
pub struct DockerArgs {
    /// Output path for Dockerfile
    #[arg(short, long, default_value = "./Dockerfile")]
    pub output: PathBuf,

    /// Rust toolchain version
    #[arg(long, default_value = "1.78")]
    pub rust_version: String,

    /// Binary name (defaults to package name)
    #[arg(short, long)]
    pub binary: Option<String>,

    /// Port to expose
    #[arg(short, long, default_value = "8080")]
    pub port: u16,
}

#[derive(Args, Debug)]
pub struct FlyArgs {
    /// Application name
    #[arg(short, long)]
    pub app: Option<String>,

    /// Region to deploy to
    #[arg(short, long, default_value = "iad")]
    pub region: String,

    /// Initialize only (don't deploy)
    #[arg(long)]
    pub init_only: bool,
}

#[derive(Args, Debug)]
pub struct RailwayArgs {
    /// Project name
    #[arg(short, long)]
    pub project: Option<String>,

    /// Environment (production, staging)
    #[arg(short, long, default_value = "production")]
    pub environment: String,
}

#[derive(Args, Debug)]
pub struct ShuttleArgs {
    /// Project name
    #[arg(short, long)]
    pub project: Option<String>,

    /// Initialize only
    #[arg(long)]
    pub init_only: bool,
}

/// Execute deployment command
pub async fn deploy(args: DeployArgs) -> Result<()> {
    match args {
        DeployArgs::Docker(docker_args) => generate_dockerfile(docker_args).await,
        DeployArgs::Fly(fly_args) => deploy_fly(fly_args).await,
        DeployArgs::Railway(railway_args) => deploy_railway(railway_args).await,
        DeployArgs::Shuttle(shuttle_args) => deploy_shuttle(shuttle_args).await,
    }
}

async fn generate_dockerfile(args: DockerArgs) -> Result<()> {
    println!("🐳 Generating Dockerfile...");

    // Try to get package name from Cargo.toml
    let binary_name = args
        .binary
        .unwrap_or_else(|| get_package_name().unwrap_or_else(|_| "app".to_string()));

    let dockerfile = format!(
        r#"# Build stage
FROM rust:{rust_version}-slim as builder

# Install build dependencies
RUN apt-get update && apt-get install -y \
    pkg-config \
    libssl-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy manifests first for dependency caching
COPY Cargo.toml Cargo.lock* ./
COPY crates ./crates

# Build dependencies (this layer will be cached)
RUN mkdir src && echo "fn main() {{}}" > src/main.rs
RUN cargo build --release
RUN rm -rf src

# Copy actual source code
COPY . .

# Build the application
RUN cargo build --release

# Runtime stage
FROM debian:bookworm-slim

# Install runtime dependencies
RUN apt-get update && apt-get install -y \
    ca-certificates \
    libssl3 \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy the binary from builder
COPY --from=builder /app/target/release/{binary_name} /usr/local/bin/app

# Expose port
EXPOSE {port}

# Set environment variables
ENV RUST_LOG=info
ENV PORT={port}

# Run the application
CMD ["app"]
"#,
        rust_version = args.rust_version,
        binary_name = binary_name,
        port = args.port
    );

    fs::write(&args.output, dockerfile).context("Failed to write Dockerfile")?;

    println!("✅ Dockerfile generated at: {}", args.output.display());
    println!();
    println!("Build and run with:");
    println!("  docker build -t myapp .");
    println!("  docker run -p {}:{} myapp", args.port, args.port);

    Ok(())
}

async fn deploy_fly(args: FlyArgs) -> Result<()> {
    println!("✈️  Deploying to Fly.io...");

    let app_name = args
        .app
        .unwrap_or_else(|| get_package_name().unwrap_or_else(|_| "rustapi-app".to_string()));

    // Generate fly.toml
    let fly_toml = format!(
        r#"# Fly.io configuration
# Generated by RustAPI CLI

app = "{app_name}"
primary_region = "{region}"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0

[[vm]]
  memory = "256mb"
  cpu_kind = "shared"
  cpus = 1
"#,
        app_name = app_name,
        region = args.region
    );

    fs::write("fly.toml", &fly_toml).context("Failed to write fly.toml")?;

    println!("✅ fly.toml generated");

    if args.init_only {
        println!();
        println!("To deploy, run:");
        println!("  fly launch");
        println!("  fly deploy");
    } else {
        println!();
        println!("To complete deployment:");
        println!("  1. Install flyctl: curl -L https://fly.io/install.sh | sh");
        println!("  2. Login: fly auth login");
        println!("  3. Launch: fly launch");
        println!("  4. Deploy: fly deploy");
    }

    Ok(())
}

async fn deploy_railway(args: RailwayArgs) -> Result<()> {
    println!("🚂 Deploying to Railway...");

    let project_name = args
        .project
        .unwrap_or_else(|| get_package_name().unwrap_or_else(|_| "rustapi-app".to_string()));

    // Generate railway.toml
    let railway_toml = r#"# Railway configuration
# Generated by RustAPI CLI

[build]
builder = "dockerfile"
dockerfilePath = "Dockerfile"

[deploy]
numReplicas = 1
healthcheckPath = "/health"
healthcheckTimeout = 100
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 3
"#
    .to_string();

    fs::write("railway.toml", &railway_toml).context("Failed to write railway.toml")?;

    println!("✅ railway.toml generated for: {}", project_name);
    println!();
    println!("To deploy:");
    println!("  1. Install Railway CLI: npm i -g @railway/cli");
    println!("  2. Login: railway login");
    println!("  3. Link project: railway link");
    println!("  4. Deploy: railway up");

    Ok(())
}

async fn deploy_shuttle(args: ShuttleArgs) -> Result<()> {
    println!("🚀 Setting up Shuttle.rs...");

    let project_name = args
        .project
        .unwrap_or_else(|| get_package_name().unwrap_or_else(|_| "rustapi-app".to_string()));

    // Generate Shuttle.toml
    let shuttle_toml = format!(
        r#"# Shuttle configuration
# Generated by RustAPI CLI

name = "{project_name}"
"#
    );

    fs::write("Shuttle.toml", &shuttle_toml).context("Failed to write Shuttle.toml")?;

    println!("✅ Shuttle.toml generated");
    println!();
    println!("⚠️  Note: Shuttle requires code modifications to use their runtime.");
    println!();
    println!("To deploy:");
    println!("  1. Install Shuttle CLI: cargo install cargo-shuttle");
    println!("  2. Login: cargo shuttle login");
    println!("  3. Init: cargo shuttle init");
    println!("  4. Deploy: cargo shuttle deploy");

    Ok(())
}

fn get_package_name() -> Result<String> {
    let cargo_toml = fs::read_to_string("Cargo.toml").context("Failed to read Cargo.toml")?;

    for line in cargo_toml.lines() {
        if line.starts_with("name") {
            if let Some(name) = line.split('=').nth(1) {
                return Ok(name.trim().trim_matches('"').to_string());
            }
        }
    }

    anyhow::bail!("Could not find package name in Cargo.toml")
}