use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use std::fs;
use std::path::PathBuf;
#[derive(Subcommand, Debug)]
pub enum DeployArgs {
Docker(DockerArgs),
Fly(FlyArgs),
Railway(RailwayArgs),
Shuttle(ShuttleArgs),
}
#[derive(Args, Debug)]
pub struct DockerArgs {
#[arg(short, long, default_value = "./Dockerfile")]
pub output: PathBuf,
#[arg(long, default_value = "1.78")]
pub rust_version: String,
#[arg(short, long)]
pub binary: Option<String>,
#[arg(short, long, default_value = "8080")]
pub port: u16,
}
#[derive(Args, Debug)]
pub struct FlyArgs {
#[arg(short, long)]
pub app: Option<String>,
#[arg(short, long, default_value = "iad")]
pub region: String,
#[arg(long)]
pub init_only: bool,
}
#[derive(Args, Debug)]
pub struct RailwayArgs {
#[arg(short, long)]
pub project: Option<String>,
#[arg(short, long, default_value = "production")]
pub environment: String,
}
#[derive(Args, Debug)]
pub struct ShuttleArgs {
#[arg(short, long)]
pub project: Option<String>,
#[arg(long)]
pub init_only: bool,
}
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...");
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()));
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()));
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()));
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")
}