use super::config::DeploymentConfig;
pub fn generate_dockerfile(project_name: &str, config: &DeploymentConfig) -> String {
let port = config.port.unwrap_or(8080);
format!(
r#"# Multi-stage Dockerfile for {project_name}
# Generated by AuDB
# Stage 1: Build
FROM rust:latest as builder
WORKDIR /build
# Copy manifests
COPY Cargo.toml Cargo.lock ./
COPY build.rs ./
# Copy source code
COPY src ./src
COPY gold ./gold
# Build release binary
RUN cargo build --release
# Stage 2: Runtime
FROM debian:bookworm-slim
# Install runtime dependencies
RUN apt-get update && \
apt-get install -y ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Create app user
RUN useradd -m -u 1000 app
WORKDIR /app
# Copy binary from builder
COPY --from=builder /build/target/release/{project_name} /app/{project_name}
# Create data directory
RUN mkdir -p /app/data && chown -R app:app /app
# Switch to app user
USER app
# Expose port
EXPOSE {port}
# Set environment
ENV RUST_LOG=info
# Run the application
CMD ["/app/{project_name}"]
"#,
project_name = project_name,
port = port
)
}
pub fn generate_docker_compose(project_name: &str, config: &DeploymentConfig) -> String {
let port = config.port.unwrap_or(8080);
let restart_policy = &config.restart;
let mut compose = format!(
r#"version: '3.8'
services:
{project_name}:
build: .
image: {project_name}:latest
container_name: {project_name}
restart: {restart_policy}
ports:
- "{port}:{port}"
"#,
project_name = project_name,
port = port,
restart_policy = restart_policy
);
if !config.environment.is_empty() {
compose.push_str(" environment:\n");
for (key, value) in &config.environment {
compose.push_str(&format!(" {}: {}\n", key, value));
}
}
if !config.volumes.is_empty() {
compose.push_str(" volumes:\n");
for (host, container) in &config.volumes {
compose.push_str(&format!(" - {}:{}\n", host, container));
}
}
if let Some(ref healthcheck) = config.healthcheck {
compose.push_str(" healthcheck:\n");
compose.push_str(&format!(
" test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:{}{}\"])\n",
port, healthcheck.endpoint
));
compose.push_str(&format!(" interval: {}\n", healthcheck.interval));
compose.push_str(&format!(" timeout: {}\n", healthcheck.timeout));
compose.push_str(&format!(" retries: {}\n", healthcheck.retries));
}
compose
}
pub fn generate_systemd_service(
project_name: &str,
binary_path: &str,
config: &DeploymentConfig,
) -> String {
let port = config.port.unwrap_or(8080);
let restart_policy = match config.restart.as_str() {
"always" => "always",
"on-failure" => "on-failure",
"unless-stopped" => "always",
_ => "on-failure",
};
let mut service = format!(
r#"[Unit]
Description={project_name} - AuDB Application
After=network.target
[Service]
Type=simple
User=app
WorkingDirectory={working_dir}
ExecStart={binary_path}
Restart={restart_policy}
RestartSec=10
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/app/data
# Environment
Environment=RUST_LOG=info
Environment=PORT={port}
"#,
project_name = project_name,
working_dir = std::env::current_dir()
.unwrap_or_default()
.display()
.to_string(),
binary_path = binary_path,
restart_policy = restart_policy,
port = port
);
for (key, value) in &config.environment {
service.push_str(&format!("Environment={}={}\n", key, value));
}
service.push_str(
r#"
[Install]
WantedBy=multi-user.target
"#,
);
service
}
pub fn generate_dockerignore() -> String {
r#"# Generated by AuDB
target/
.git/
.gitignore
*.log
*.md
.DS_Store
.audb/
"#
.to_string()
}
pub fn generate_healthcheck_script(port: u16, endpoint: &str) -> String {
format!(
r#"#!/bin/sh
# Health check script for Docker
curl -f http://localhost:{}{} || exit 1
"#,
port, endpoint
)
}
pub fn generate_launchd_plist(
service_label: &str,
binary_path: &std::path::Path,
config: &DeploymentConfig,
) -> String {
let port = config.port.unwrap_or(8080);
let mut env_vars = String::new();
if !config.environment.is_empty() {
env_vars.push_str(" <key>EnvironmentVariables</key>\n");
env_vars.push_str(" <dict>\n");
for (key, value) in &config.environment {
env_vars.push_str(&format!(" <key>{}</key>\n", key));
env_vars.push_str(&format!(" <string>{}</string>\n", value));
}
env_vars.push_str(&format!(" <key>PORT</key>\n"));
env_vars.push_str(&format!(" <string>{}</string>\n", port));
env_vars.push_str(" </dict>\n");
} else {
env_vars.push_str(" <key>EnvironmentVariables</key>\n");
env_vars.push_str(" <dict>\n");
env_vars.push_str(&format!(" <key>PORT</key>\n"));
env_vars.push_str(&format!(" <string>{}</string>\n", port));
env_vars.push_str(" </dict>\n");
}
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{service_label}</string>
<key>ProgramArguments</key>
<array>
<string>{binary_path}</string>
</array>
<key>RunAtLoad</key>
<{run_at_load}/>
<key>KeepAlive</key>
<{keep_alive}/>
{env_vars}
<key>StandardOutPath</key>
<string>/tmp/{service_label}.log</string>
<key>StandardErrorPath</key>
<string>/tmp/{service_label}.err</string>
<key>WorkingDirectory</key>
<string>{working_dir}</string>
</dict>
</plist>
"#,
service_label = service_label,
binary_path = binary_path.display(),
run_at_load = if config.persist { "true" } else { "false" },
keep_alive = if config.persist { "true" } else { "false" },
env_vars = env_vars.trim_end(),
working_dir = binary_path
.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
.map(|p| p.display().to_string())
.unwrap_or_else(|| "/tmp".to_string())
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_generate_dockerfile() {
let config = DeploymentConfig {
port: Some(3000),
..Default::default()
};
let dockerfile = generate_dockerfile("my-app", &config);
assert!(dockerfile.contains("my-app"));
assert!(dockerfile.contains("EXPOSE 3000"));
assert!(dockerfile.contains("rust:latest"));
assert!(dockerfile.contains("debian:bookworm-slim"));
}
#[test]
fn test_generate_docker_compose() {
let mut config = DeploymentConfig::default();
config.port = Some(8080);
config.restart = "always".to_string();
let compose = generate_docker_compose("test-app", &config);
assert!(compose.contains("test-app"));
assert!(compose.contains("8080:8080"));
assert!(compose.contains("restart: always"));
}
#[test]
fn test_generate_docker_compose_with_env() {
let mut config = DeploymentConfig::default();
config
.environment
.insert("DB_URL".to_string(), "postgres://localhost".to_string());
let compose = generate_docker_compose("test-app", &config);
assert!(compose.contains("environment:"));
assert!(compose.contains("DB_URL: postgres://localhost"));
}
#[test]
fn test_generate_systemd_service() {
let config = DeploymentConfig {
port: Some(8080),
restart: "on-failure".to_string(),
..Default::default()
};
let service = generate_systemd_service("my-service", "/usr/local/bin/my-service", &config);
assert!(service.contains("my-service"));
assert!(service.contains("ExecStart=/usr/local/bin/my-service"));
assert!(service.contains("Restart=on-failure"));
assert!(service.contains("PORT=8080"));
}
#[test]
fn test_generate_dockerignore() {
let ignore = generate_dockerignore();
assert!(ignore.contains("target/"));
assert!(ignore.contains(".git/"));
assert!(ignore.contains(".audb/"));
}
#[test]
fn test_generate_launchd_plist() {
use std::path::PathBuf;
let config = DeploymentConfig {
port: Some(8080),
persist: true,
..Default::default()
};
let plist = generate_launchd_plist(
"com.audb.test",
&PathBuf::from("/usr/local/bin/test-app"),
&config,
);
assert!(plist.contains("com.audb.test"));
assert!(plist.contains("/usr/local/bin/test-app"));
assert!(plist.contains("<key>RunAtLoad</key>"));
assert!(plist.contains("<key>KeepAlive</key>"));
assert!(plist.contains("<key>PORT</key>"));
assert!(plist.contains("<string>8080</string>"));
assert!(plist.contains("/tmp/com.audb.test.log"));
}
}