use std::borrow::Cow;
use anyhow::Context;
use clap::command;
use log::*;
use mx_tester::*;
const CONFIG_PATH_AUTOTEST: &str = "[empty]";
#[derive(Debug)]
enum Command {
Build,
Up,
Run,
Down,
}
#[tokio::main]
async fn main() {
use clap::Arg;
env_logger::init();
let matches = command!()
.version(std::env!("CARGO_PKG_VERSION"))
.about("Command-line tool to simplify testing Matrix bots and Synapse modules")
.arg(
Arg::new("config")
.short('c')
.long("config")
.global(true)
.default_value("mx-tester.yml")
.help("The file containing the test configuration. Pass `[empty]` to run an empty mx-tester.yml, for self-testing."),
)
.arg(
#[allow(deprecated)]
Arg::new("command")
.action(clap::ArgAction::Append)
.takes_value(false)
.multiple_occurrences(true)
.value_parser(["up", "run", "down", "build"])
.help("The list of commands to run. Order matters and the same command may be repeated."),
)
.arg(
Arg::new("username")
.short('u')
.long("username")
.global(true)
.takes_value(true)
.required(false)
.help("A username for logging to the Docker registry")
)
.arg(
Arg::new("password")
.short('p')
.long("password")
.global(true)
.takes_value(true)
.required(false)
.help("A password for logging to the Docker registry")
)
.arg(
Arg::new("server")
.long("server")
.global(true)
.takes_value(true)
.required(false)
.help("A server name for the Docker registry")
)
.arg(
Arg::new("root_dir")
.long("root")
.global(true)
.value_name("PATH")
.takes_value(true)
.required(false)
.help("Write all files in subdirectories of this directory (default: /tmp)")
)
.arg(
Arg::new("workers")
.long("workers")
.global(true)
.takes_value(false)
.required(false)
.help("If specified, use workerized Synapse (default: no workers). If you have run `build` with `--workers`, make sure that `up` and `build` are also run with `--workers`.")
)
.arg(
Arg::new("synapse-tag")
.long("synapse-tag")
.global(true)
.value_name("TAG")
.takes_value(true)
.required(false)
.help("If specified, use the Docker image published with TAG (default: use mx-tester.yml or tag `latest`)")
)
.arg(
Arg::new("no-autoclean-on-error")
.long("no-autoclean-on-error")
.global(true)
.takes_value(false)
.help("If specified, do NOT clean up containers in case of error")
)
.arg(
Arg::new("docker-ssl")
.long("docker-ssl")
.global(true)
.default_value("detect")
.value_parser(["always", "never", "detect"])
.help("If `detect`, attempt to auto-detect a SSL configuration and fallback tp HTTP otherwise. This may be broken in your CI. If `always`, fail if there is no Docker SSL configuration. If `never`, ignore any Docker SSL configuration.")
)
.get_matches();
let config_path: &String = matches
.get_one("config")
.expect("Missing value for `config`");
let is_self_test = config_path == CONFIG_PATH_AUTOTEST;
let commands = match matches.get_many::<String>("command") {
None if is_self_test => vec![],
None => vec![Command::Up, Command::Run, Command::Down],
Some(values) => values
.map(|command| match command.as_ref() {
"up" => Command::Up,
"down" => Command::Down,
"run" => Command::Run,
"build" => Command::Build,
_ => panic!("Invalid command `{}`", command),
})
.collect(),
};
debug!("Running {:?}", commands);
let mut config = {
if is_self_test {
Config::builder()
.name("mx-tester-autotest".to_string())
.build()
} else {
let config_file = std::fs::File::open(config_path).unwrap_or_else(|err| {
panic!("Could not open config file `{}`: {}", config_path, err)
});
serde_yaml::from_reader(config_file)
.unwrap_or_else(|err| panic!("Invalid config file `{}`: {}", config_path, err))
}
};
debug!("Config: {:2?}", config);
for (key, value) in std::env::vars().filter(|(key, _)| key.starts_with("DOCKER_")) {
debug!("{}={}", key, value);
}
debug!("Root: {:?}", config.test_root());
if let Some(server) = matches.get_one::<String>("server") {
config.credentials.serveraddress = Some(server.to_string());
}
if let Some(password) = matches.get_one::<String>("password") {
config.credentials.password = Some(password.to_string());
}
if let Some(username) = matches.get_one::<String>("username") {
config.credentials.username = Some(username.to_string());
}
if let Some(root) = matches.get_one::<String>("root_dir") {
config.directories.root = std::path::Path::new(root).to_path_buf()
}
let workers = matches.contains_id("workers");
config.workers.enabled = workers;
if let Some(synapse_tag) = matches.get_one::<String>("synapse-tag") {
config.synapse = SynapseVersion::Docker {
tag: format!("matrixdotorg/synapse:{}", synapse_tag),
};
}
enum ShouldSsl {
Never,
Detect,
Always,
}
let should_ssl = match matches.get_one::<String>("docker-ssl").unwrap().as_ref() {
"never" => ShouldSsl::Never,
"detect" => ShouldSsl::Detect,
"always" => ShouldSsl::Always,
_ => panic!(), };
if !is_self_test && commands.is_empty() {
return;
}
println!(
"mx-tester {version} starting. Logs will be stored at {logs_dir:?}",
version = env!("CARGO_PKG_VERSION"),
logs_dir = config.logs_dir()
);
let has_docker_cert_path = std::env::var("DOCKER_CERT_PATH").is_ok();
let mut docker = match (should_ssl, &config.credentials.serveraddress, has_docker_cert_path) {
(ShouldSsl::Never, None, _) | (ShouldSsl::Detect, None, _) => {
info!("Using local docker repository");
bollard::Docker::connect_with_local_defaults().context("Connecting with local defaults")
}
(ShouldSsl::Always, None, _) => {
panic!("Option conflict: `--docker-ssl=always` requires option `--server` or an server address in mx-tester.yml")
}
(ShouldSsl::Never, &Some(ref server), _) | (ShouldSsl::Detect, &Some(ref server), false) => {
info!("Using docker repository with HTTP {}", server);
bollard::Docker::connect_with_http_defaults().context("Connecting with HTTP")
},
(ShouldSsl::Always, &Some(ref server), _) | (ShouldSsl::Detect, &Some(ref server), true) => {
info!("Using docker repository with SSL {}", server);
bollard::Docker::connect_with_ssl_defaults().context("Connecting with SSL")
}
}.expect("Failed to connect to the Docker daemon");
docker.set_timeout(std::time::Duration::from_secs(600));
let version = docker
.version()
.await
.expect("Checking connection to docker daemon");
println!(
"Using docker {}",
version.version.map(Cow::from).unwrap_or_else(|| "?".into())
);
let mut result_run = None;
for command in commands {
match command {
Command::Build => {
info!("mx-tester build...");
build(&docker, &config).await.expect("Error in `build`");
}
Command::Up => {
info!("mx-tester up...");
up(&docker, &config).await.expect("Error in `up`");
}
Command::Run => {
info!("mx-tester run...");
result_run = Some(run(&docker, &config).await);
}
Command::Down => {
info!("mx-tester down...");
let status = match result_run {
None => Status::Manual,
Some(Ok(_)) => Status::Success,
Some(Err(_)) => Status::Failure,
};
let result_down = down(&docker, &config, status).await;
if let Some(result_run) = result_run.take() {
result_run.expect("Error in `run`");
}
result_down.expect("Error during teardown");
}
}
}
if let Some(result) = result_run {
result.expect("Error in `run`");
}
println!("* mx-tester success");
}