use crate::paths;
use anyhow::{Context, Result};
use std::path::Path;
async fn read_project_template(assets_root: &Path, name: &str) -> Result<String> {
let template_path = assets_root.join("project").join(name);
tokio::fs::read_to_string(&template_path)
.await
.with_context(|| format!("Failed to read {:?} from downloaded mecha10-assets", template_path))
}
pub struct InfraTemplates;
impl InfraTemplates {
pub fn new() -> Self {
Self
}
async fn ensure_docker_dir(&self, path: &Path) -> Result<()> {
let docker_dir = path.join(paths::docker::DIR);
if !docker_dir.exists() {
tokio::fs::create_dir_all(&docker_dir).await?;
}
Ok(())
}
pub async fn create_docker_compose(&self, path: &Path, project_name: &str, assets_root: &Path) -> Result<()> {
self.ensure_docker_dir(path).await?;
let template = read_project_template(assets_root, "docker-compose.yml.template").await?;
let content = template.replace("{{project_name}}", project_name);
tokio::fs::write(path.join(paths::docker::COMPOSE_FILE), content).await?;
Ok(())
}
pub async fn create_docker_compose_remote(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.ensure_docker_dir(path).await?;
let framework_version = env!("CARGO_PKG_VERSION");
let template = read_project_template(assets_root, "docker/docker-compose.remote.yml.template").await?;
let content = template.replace("{{framework_version}}", framework_version);
tokio::fs::write(path.join(paths::docker::COMPOSE_REMOTE_FILE), content).await?;
Ok(())
}
pub async fn create_dockerfile_remote(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.ensure_docker_dir(path).await?;
let framework_version = env!("CARGO_PKG_VERSION");
let template = read_project_template(assets_root, "docker/Dockerfile.remote.template").await?;
let content = template.replace("{{framework_version}}", framework_version);
tokio::fs::write(path.join(paths::docker::DOCKERFILE_REMOTE), content).await?;
Ok(())
}
pub async fn create_entrypoint_remote(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.ensure_docker_dir(path).await?;
let content = read_project_template(assets_root, "docker/entrypoint.remote.sh.template").await?;
tokio::fs::write(path.join(paths::docker::ENTRYPOINT_REMOTE), content).await?;
Ok(())
}
pub async fn create_remote_docker_files(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.ensure_docker_dir(path).await?;
self.create_docker_compose_remote(path, assets_root).await?;
self.create_dockerfile_remote(path, assets_root).await?;
self.create_entrypoint_remote(path, assets_root).await?;
Ok(())
}
pub async fn create_docker_compose_rl(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.ensure_docker_dir(path).await?;
let framework_version = env!("CARGO_PKG_VERSION");
let template = read_project_template(assets_root, "docker/docker-compose.rl.yml.template").await?;
let content = template.replace("{{framework_version}}", framework_version);
tokio::fs::write(path.join(paths::docker::COMPOSE_RL_FILE), content).await?;
Ok(())
}
pub async fn create_dockerfile_rl(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.ensure_docker_dir(path).await?;
let framework_version = env!("CARGO_PKG_VERSION");
let template = read_project_template(assets_root, "docker/Dockerfile.rl.template").await?;
let content = template.replace("{{framework_version}}", framework_version);
tokio::fs::write(path.join(paths::docker::DOCKERFILE_RL), content).await?;
Ok(())
}
pub async fn create_entrypoint_rl(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.ensure_docker_dir(path).await?;
let content = read_project_template(assets_root, "docker/entrypoint-rl.sh.template").await?;
tokio::fs::write(path.join(paths::docker::ENTRYPOINT_RL), content).await?;
Ok(())
}
pub async fn create_rl_docker_files(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.ensure_docker_dir(path).await?;
self.create_docker_compose_rl(path, assets_root).await?;
self.create_dockerfile_rl(path, assets_root).await?;
self.create_entrypoint_rl(path, assets_root).await?;
Ok(())
}
pub async fn create_env_example(&self, path: &Path, project_name: &str, assets_root: &Path) -> Result<()> {
let robot_id = project_name.replace('-', "_");
let template = read_project_template(assets_root, ".env.example.template").await?;
let content = template
.replace("{{robot_id}}", &robot_id)
.replace("{{project_name}}", project_name);
tokio::fs::write(path.join(paths::env::EXAMPLE), &content).await?;
tokio::fs::write(path.join(paths::env::FILE), &content).await?;
Ok(())
}
}
impl Default for InfraTemplates {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::{tempdir, TempDir};
fn stage_assets_root() -> TempDir {
let assets_root = TempDir::new().unwrap();
let docker_dir = assets_root.path().join("project/docker");
std::fs::create_dir_all(&docker_dir).unwrap();
std::fs::write(
assets_root.path().join("project/docker-compose.yml.template"),
"services:\n app:\n container_name: {{project_name}}\n",
)
.unwrap();
std::fs::write(
docker_dir.join("docker-compose.remote.yml.template"),
"# framework {{framework_version}}\n",
)
.unwrap();
std::fs::write(
docker_dir.join("Dockerfile.remote.template"),
format!(
"# framework {{{{framework_version}}}}\nCOPY {} /entrypoint.sh\n",
paths::docker::ENTRYPOINT_REMOTE
),
)
.unwrap();
std::fs::write(
docker_dir.join("entrypoint.remote.sh.template"),
"#!/bin/sh\nexec mecha10-launcher start --target remote \"$@\"\n",
)
.unwrap();
std::fs::write(
docker_dir.join("docker-compose.rl.yml.template"),
"# framework {{framework_version}}\n",
)
.unwrap();
std::fs::write(
docker_dir.join("Dockerfile.rl.template"),
format!(
"# framework {{{{framework_version}}}}\nCOPY {} /entrypoint.sh\n",
paths::docker::ENTRYPOINT_RL
),
)
.unwrap();
std::fs::write(
docker_dir.join("entrypoint-rl.sh.template"),
"#!/bin/sh\nexec mecha10-launcher start --target rl \"$@\"\n",
)
.unwrap();
std::fs::write(
assets_root.path().join("project/.env.example.template"),
"ROBOT_ID={{robot_id}}\nMECHA10_PROJECT_NAME={{project_name}}\n",
)
.unwrap();
assets_root
}
#[tokio::test]
async fn create_remote_docker_files_writes_compose_dockerfile_and_entrypoint() {
let dir = tempdir().unwrap();
let assets_root = stage_assets_root();
let infra = InfraTemplates::new();
infra
.create_remote_docker_files(dir.path(), assets_root.path())
.await
.unwrap();
assert!(dir.path().join(paths::docker::COMPOSE_REMOTE_FILE).exists());
assert!(dir.path().join(paths::docker::DOCKERFILE_REMOTE).exists());
assert!(dir.path().join(paths::docker::ENTRYPOINT_REMOTE).exists());
}
#[tokio::test]
async fn dockerfile_remote_copies_entrypoint_from_the_path_it_actually_writes_it_to() {
let dir = tempdir().unwrap();
let assets_root = stage_assets_root();
let infra = InfraTemplates::new();
infra
.create_remote_docker_files(dir.path(), assets_root.path())
.await
.unwrap();
let dockerfile = tokio::fs::read_to_string(dir.path().join(paths::docker::DOCKERFILE_REMOTE))
.await
.unwrap();
assert!(
dockerfile.contains(&format!("COPY {} /entrypoint.sh", paths::docker::ENTRYPOINT_REMOTE)),
"Dockerfile.remote must COPY from {}, got:\n{dockerfile}",
paths::docker::ENTRYPOINT_REMOTE
);
}
#[tokio::test]
async fn create_rl_docker_files_writes_compose_dockerfile_and_entrypoint() {
let dir = tempdir().unwrap();
let assets_root = stage_assets_root();
let infra = InfraTemplates::new();
infra
.create_rl_docker_files(dir.path(), assets_root.path())
.await
.unwrap();
assert!(dir.path().join(paths::docker::COMPOSE_RL_FILE).exists());
assert!(dir.path().join(paths::docker::DOCKERFILE_RL).exists());
assert!(dir.path().join(paths::docker::ENTRYPOINT_RL).exists());
}
#[tokio::test]
async fn dockerfile_rl_copies_entrypoint_from_the_path_it_actually_writes_it_to() {
let dir = tempdir().unwrap();
let assets_root = stage_assets_root();
let infra = InfraTemplates::new();
infra
.create_rl_docker_files(dir.path(), assets_root.path())
.await
.unwrap();
let dockerfile = tokio::fs::read_to_string(dir.path().join(paths::docker::DOCKERFILE_RL))
.await
.unwrap();
assert!(
dockerfile.contains(&format!("COPY {} /entrypoint.sh", paths::docker::ENTRYPOINT_RL)),
"Dockerfile.rl must COPY from {}, got:\n{dockerfile}",
paths::docker::ENTRYPOINT_RL
);
}
#[tokio::test]
async fn rl_and_remote_docker_files_coexist_without_overwriting_each_other() {
let dir = tempdir().unwrap();
let assets_root = stage_assets_root();
let infra = InfraTemplates::new();
infra
.create_remote_docker_files(dir.path(), assets_root.path())
.await
.unwrap();
infra
.create_rl_docker_files(dir.path(), assets_root.path())
.await
.unwrap();
assert!(dir.path().join(paths::docker::ENTRYPOINT_REMOTE).exists());
assert!(dir.path().join(paths::docker::ENTRYPOINT_RL).exists());
assert_ne!(paths::docker::ENTRYPOINT_REMOTE, paths::docker::ENTRYPOINT_RL);
let rl_entrypoint = tokio::fs::read_to_string(dir.path().join(paths::docker::ENTRYPOINT_RL))
.await
.unwrap();
assert!(rl_entrypoint.contains("--target rl"));
let remote_entrypoint = tokio::fs::read_to_string(dir.path().join(paths::docker::ENTRYPOINT_REMOTE))
.await
.unwrap();
assert!(remote_entrypoint.contains("--target remote"));
}
#[tokio::test]
async fn env_example_includes_project_name_distinct_from_robot_id() {
let dir = tempdir().unwrap();
let assets_root = stage_assets_root();
let infra = InfraTemplates::new();
infra
.create_env_example(dir.path(), "my-robot", assets_root.path())
.await
.unwrap();
let content = tokio::fs::read_to_string(dir.path().join(paths::env::EXAMPLE))
.await
.unwrap();
assert!(content.contains("ROBOT_ID=my_robot"));
assert!(content.contains("MECHA10_PROJECT_NAME=my-robot"));
}
}