use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::infra::cluster::config::ClusterRole;
use crate::platform::boot;
use crate::platform::build_router;
use super::client::Instance;
use super::context::{self, ClientContext};
const BOOTSTRAP_PASSWORD_REL: &str = ".bootstrap/superadmin-password";
const CATALOG_REL: &str = "platform/catalog.db";
const PROBE_TIMEOUT: Duration = Duration::from_millis(1500);
pub async fn open(stored: &mut ClientContext) -> Result<Instance, io::Error> {
let config = boot::load_platform_config(ClusterRole::Standalone)?;
let data_root = config.data_root.clone();
let owner = config.default_owner.clone();
let configured_password = config.default_password.clone();
let url = boot::local_instance_url();
let existed = data_root.join(CATALOG_REL).exists();
let instance = if listening(&url).await {
Instance::new(&url, stored.token.trim())?
} else {
let router = build_router(config)
.await
.map_err(|err| io::Error::other(err.message))?;
Instance::local(&url, stored.token.trim(), router)
};
let instance = authenticated(
instance,
stored,
&url,
&owner,
&data_root,
&configured_password,
)
.await?;
if !existed {
announce_provisioning(&data_root, &owner);
}
Ok(instance)
}
async fn authenticated(
instance: Instance,
stored: &mut ClientContext,
url: &str,
owner: &str,
data_root: &Path,
configured_password: &str,
) -> Result<Instance, io::Error> {
let usable = !stored.token.trim().is_empty()
&& stored.instance == url
&& instance.get_json("/api/profile").await.is_ok();
if usable {
return Ok(instance);
}
let password = superadmin_password(data_root, configured_password)?;
let token = instance.authenticate(owner, &password).await?;
stored.instance = url.to_string();
stored.token = token.clone();
stored.owner = owner.to_string();
remember(url, &token, owner)?;
Ok(instance.with_token(&token))
}
fn announce_provisioning(data_root: &Path, owner: &str) {
println!(
"Created a new Zebflow instance at {} and signed in as {owner}.",
data_root.display()
);
let password_path = data_root.join(BOOTSTRAP_PASSWORD_REL);
if password_path.is_file() {
println!(
"Its generated password is in {} (mode 0600). Changing it from the web UI's \
change-password screen deletes this file; set ZEBFLOW_PLATFORM_DEFAULT_PASSWORD \
before an instance's first start to choose one instead.",
password_path.display()
);
}
}
pub(crate) async fn listening(url: &str) -> bool {
let Ok(client) = reqwest::Client::builder().timeout(PROBE_TIMEOUT).build() else {
return false;
};
client
.get(format!("{url}/health"))
.send()
.await
.map(|response| response.status().is_success())
.unwrap_or(false)
}
fn superadmin_password(data_root: &Path, configured: &str) -> Result<String, io::Error> {
if !configured.trim().is_empty() {
return Ok(configured.to_string());
}
let path: PathBuf = data_root.join(BOOTSTRAP_PASSWORD_REL);
let raw = fs::read_to_string(&path).map_err(|err| {
io::Error::new(
err.kind(),
format!(
"cannot read the generated password at {}: {err}. The file is deleted when the \
password is changed, so the zero-ceremony window has likely closed. Sign in \
once with `{} login {}` and this command will use the stored session.",
path.display(),
super::program(),
boot::local_instance_url()
),
)
})?;
let password = raw.trim().to_string();
if password.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("the generated password at {} is empty", path.display()),
));
}
Ok(password)
}
fn remember(url: &str, token: &str, owner: &str) -> Result<(), io::Error> {
let path = context::default_context_path()?;
let mut stored = context::load(&path)?;
stored.instance = url.to_string();
stored.token = token.to_string();
stored.owner = owner.to_string();
context::save(&path, &stored)
}