use std::{path::PathBuf, process::Command as StdCommand};
use anyhow::Context;
use dialoguer::*;
use email_address_parser::EmailAddress;
use sanity_api::*;
use structopt::StructOpt;
use url::Url;
#[derive(StructOpt)]
struct CliOptions {
#[structopt(
long,
parse(try_from_str),
default_value = "https://web-tb546tjmbq-uc.a.run.app/api/v0/"
)]
base_api_url: Url,
#[structopt(
long,
parse(try_from_str),
default_value = "docker-registry-tb546tjmbq-uc.a.run.app"
)]
push_host: String,
#[structopt(subcommand)]
command: Command,
}
#[derive(StructOpt)]
enum Command {
Login {
#[structopt(long)]
token: Option<String>,
},
CreateProject,
Push {
docker_image: String,
project_id: String,
task_id: String,
},
}
fn main() -> anyhow::Result<()> {
stderrlog::new().verbosity(log::Level::Info).init()?;
let options = CliOptions::from_args();
let api = HttpApi::new(&options.base_api_url)?;
if let Command::Login { token } = &options.command {
let token = if let Some(token) = token {
token.clone()
} else {
do_login(&api)?
};
let token_path = token_path()?;
std::fs::create_dir_all(token_path.parent().expect("did join"))?;
std::fs::write(&token_path, token)?;
log::info!("Wrote token to {}", token_path.display());
return Ok(());
}
let token = read_token().context("Reading token")?;
match options.command {
Command::Login { .. } => unreachable!(),
Command::CreateProject => create_project(&api, token)?,
Command::Push {
docker_image,
project_id,
task_id,
} => {
let full_tag = format!(
"{}/{project_id}/{task_id}",
options.push_host.trim_end_matches("/")
);
log::info!("Tagging image as {full_tag:?}");
let tag_status = StdCommand::new("docker")
.args(["tag", &docker_image, &full_tag])
.status()?;
anyhow::ensure!(tag_status.success(), "docker tag failed");
let push_status = StdCommand::new("docker")
.args(["push", &full_tag])
.status()?;
anyhow::ensure!(push_status.success(), "docker push failed");
let response = async_std::task::block_on(api.register_task_version(
CreateTaskVersionRequest {
project_id,
task_id,
},
token,
))?;
log::info!("Created task version: {response:#?}");
}
}
Ok(())
}
fn sanity_dir() -> anyhow::Result<PathBuf> {
Ok(dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine HOME directory"))?
.join(".sanity"))
}
fn token_path() -> anyhow::Result<PathBuf> {
Ok(sanity_dir()?.join("token.txt"))
}
fn read_token() -> anyhow::Result<String> {
let path = token_path().context("Determining token path")?;
Ok(std::fs::read_to_string(&path)
.context(format!("Reading {}", path.display()))?
.trim()
.to_string())
}
fn do_login(api: &dyn Api) -> anyhow::Result<String> {
let email: String = Input::new().with_prompt("Email").interact_text()?;
let email =
EmailAddress::parse(&email, None).ok_or_else(|| anyhow::anyhow!("Invalid email"))?;
let password = Password::new().with_prompt("Password").interact()?;
let login_response = async_std::task::block_on(api.login(LoginRequest {
email: email.to_string(),
password,
}))?;
let token = match login_response {
LoginResponse::CreatedAccount { email, token } => {
log::info!("Created an account with email {email}");
token
}
LoginResponse::LoggedIn { email, token } => {
log::info!("Logged in as {email}");
token
}
};
println!("{token}");
Ok(token)
}
fn create_project(api: &dyn Api, token: String) -> anyhow::Result<()> {
let nickname: String = Input::<String>::new()
.with_prompt("Project nickname (optional)")
.allow_empty(true)
.interact_text()?
.trim()
.to_string();
let nickname = if nickname.is_empty() {
None
} else {
Some(nickname)
};
let create_response =
async_std::task::block_on(api.create_project(CreateProjectRequest { nickname }, token))?;
println!("{}", create_response.id);
Ok(())
}