pub mod types;
use std::{
collections::HashMap,
fs::{self, File},
io::{Read, Write},
path::PathBuf,
};
use anyhow::Context;
use config::Config;
use dialoguer::{Input, Select};
use directories::ProjectDirs;
use manta_backend_dispatcher::types::{K8sAuth, K8sDetails};
use toml_edit::DocumentMut;
use types::{MantaConfiguration, Site};
use crate::common::{
audit::Auditor,
check_network_connectivity::check_network_connectivity_to_backend,
kafka::Kafka,
};
fn get_project_dirs() -> Result<ProjectDirs, anyhow::Error> {
ProjectDirs::from(
"local",
"cscs",
"manta",
)
.context(
"Could not determine project directories \
(home directory may not be set)",
)
}
pub(crate) fn get_default_config_path() -> Result<PathBuf, anyhow::Error> {
Ok(PathBuf::from(get_project_dirs()?.config_dir()))
}
pub(crate) fn get_default_manta_config_file_path()
-> Result<PathBuf, anyhow::Error> {
let mut path = get_default_config_path()?;
path.push("config.toml");
Ok(path)
}
pub(crate) fn get_default_cache_path() -> Result<PathBuf, anyhow::Error> {
Ok(PathBuf::from(get_project_dirs()?.cache_dir()))
}
pub(crate) fn read_config_toml() -> Result<(PathBuf, DocumentMut), anyhow::Error>
{
let path = get_default_manta_config_file_path()?;
log::debug!(
"Reading manta configuration from {}",
path.to_string_lossy()
);
let content =
fs::read_to_string(&path).context("Error reading configuration file")?;
let doc = content
.parse::<DocumentMut>()
.context("Could not parse configuration file as TOML")?;
Ok((path, doc))
}
pub(crate) fn write_config_toml(
path: &std::path::Path,
doc: &DocumentMut,
) -> Result<(), anyhow::Error> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(path)
.context("Failed to open configuration file for writing")?;
file
.write_all(doc.to_string().as_bytes())
.context("Failed to write configuration file")?;
file.flush().context("Failed to flush configuration file")?;
Ok(())
}
pub fn get_csm_root_cert_content(
file_path: &str,
) -> Result<Vec<u8>, anyhow::Error> {
let mut buf = Vec::new();
let root_cert_file_rslt = File::open(file_path);
let file_rslt = if root_cert_file_rslt.is_err() {
let mut config_path = get_default_config_path()?;
config_path.push(file_path);
File::open(config_path)
} else {
root_cert_file_rslt
};
match file_rslt {
Ok(mut file) => {
file
.read_to_end(&mut buf)
.context("Failed to read CA root certificate file")?;
Ok(buf)
}
Err(_) => Err(anyhow::anyhow!("CA public root file could not be found")),
}
}
fn get_default_manta_audit_file_path() -> Result<PathBuf, anyhow::Error> {
let mut log_file_path = PathBuf::from(get_project_dirs()?.data_dir());
log_file_path.push("manta.log");
Ok(log_file_path)
}
fn get_default_mgmt_plane_ca_cert_file_path() -> Result<PathBuf, anyhow::Error>
{
let mut ca_cert_file_path = get_default_config_path()?;
ca_cert_file_path.push("alps_root_cert.pem");
Ok(ca_cert_file_path)
}
pub async fn get_config_file_path() -> Result<PathBuf, anyhow::Error> {
if let Ok(env_config_file_name) = std::env::var("MANTA_CONFIG") {
let mut env_config_file = std::path::PathBuf::new();
env_config_file.push(env_config_file_name);
Ok(env_config_file)
} else {
get_default_manta_config_file_path()
}
}
pub async fn get_configuration() -> Result<Config, anyhow::Error> {
let config_file_path = get_config_file_path().await?;
if !config_file_path.exists() {
log::info!(
"Configuration file '{}' not found. Creating a new one.",
config_file_path.to_string_lossy()
);
create_new_config_file(Some(&config_file_path)).await?;
};
let config_file_path_str = config_file_path.to_str().ok_or_else(|| {
anyhow::anyhow!("Configuration file path contains invalid UTF-8")
})?;
let config_file =
config::File::new(config_file_path_str, config::FileFormat::Toml);
::config::Config::builder()
.add_source(config_file)
.add_source(
::config::Environment::with_prefix("MANTA")
.try_parsing(true)
.prefix_separator("_"),
)
.build()
.context("Could not process manta configuration file")
}
fn prompt_string(
prompt: &str,
default: &str,
) -> Result<String, anyhow::Error> {
Input::new()
.with_prompt(prompt)
.default(default.to_string())
.show_default(true)
.interact_text()
.with_context(|| format!("Failed to read: {}", prompt))
}
fn prompt_string_allow_empty(
prompt: &str,
default: &str,
) -> Result<String, anyhow::Error> {
Input::new()
.with_prompt(prompt)
.default(default.to_string())
.show_default(true)
.allow_empty(true)
.interact_text()
.with_context(|| format!("Failed to read: {}", prompt))
}
async fn create_new_config_file(
config_file_path_opt: Option<&PathBuf>,
) -> Result<(), anyhow::Error> {
eprintln!("Configuration file not found. Please introduce values below:");
let log_level_values = vec!["error", "info", "warn", "debug", "trace"];
let log_selection = Select::new()
.with_prompt("Please select 'log verbosity' level from the list below")
.items(&log_level_values)
.default(0)
.interact()
.context("Failed to read log level selection")?;
let log = log_level_values[log_selection].to_string();
let parent_hsm_group = String::new();
let audit_file: String = prompt_string_allow_empty(
"Please type full path for the audit file",
&get_default_manta_audit_file_path()?
.to_string_lossy()
.to_string(),
)?;
let site: String = prompt_string(
"Please type site name",
"alps",
)?;
let shasta_base_url: String = prompt_string(
"Please type site management plane URL",
"https://api.cmn.alps.cscs.ch",
)?;
let k8s_api_url: String = prompt_string(
"Please type kubernetes api URL",
"https://10.252.1.12:6442",
)?;
let vault_base_url: String = prompt_string(
"Please type Hashicorp Vault URL",
"https://hashicorp-vault.cscs.ch:8200",
)?;
let vault_secret_path: String = prompt_string(
"Please type Hashicorp Vault secret path",
"shasta",
)?;
let root_ca_cert_file: String = prompt_string(
"Please type full path for the CA public certificate file",
&get_default_mgmt_plane_ca_cert_file_path()?
.to_string_lossy()
.to_string(),
)?;
let backend_options = vec!["csm", "ochami"];
let backend_selection = Select::new()
.with_prompt("Please select 'backend' technology from the list below")
.items(&backend_options)
.default(0)
.interact()
.context("Failed to read backend selection")?;
let backend = backend_options[backend_selection].to_string();
let audit_kafka_brokers: String = prompt_string_allow_empty(
"Please type kafka broker to send audit logs",
"kafka.o11y.cscs.ch:9095",
)
.unwrap_or_default();
let audit_kafka_topic: String = if !audit_kafka_brokers.is_empty() {
prompt_string(
"Please type kafka topic to send audit logs",
"test-topic",
)
.unwrap_or_default()
} else {
String::new()
};
let auditor =
if !audit_kafka_brokers.is_empty() && !audit_kafka_topic.is_empty() {
let kafka = Kafka::new(vec![audit_kafka_brokers], audit_kafka_topic);
Some(Auditor { kafka })
} else {
None
};
println!("Testing connectivity to CSM backend, please wait ...");
let test_backend_api =
check_network_connectivity_to_backend(&shasta_base_url).await;
let socks5_proxy = if test_backend_api.is_ok() {
println!("This machine can access CSM API, no need to setup SOCKS5 proxy");
None
} else {
println!("This machine cannot access CSM API, configuring SOCKS5 proxy");
Some(prompt_string_allow_empty(
"Please type socks5 proxy URL",
"socks5h://127.0.0.1:1080",
)?)
};
let k8s_auth = K8sAuth::Native {
certificate_authority_data: String::new(),
client_certificate_data: String::new(),
client_key_data: String::new(),
};
let k8s_details = K8sDetails {
api_url: k8s_api_url.clone(),
authentication: k8s_auth,
};
let site_details = Site {
socks5_proxy,
shasta_base_url,
vault_base_url: Some(vault_base_url),
vault_secret_path: Some(vault_secret_path),
root_ca_cert_file,
k8s: Some(k8s_details),
backend,
};
let mut site_hashmap = HashMap::new();
site_hashmap.insert(site.clone(), site_details);
let config_toml = MantaConfiguration {
log,
site,
parent_hsm_group,
audit_file,
sites: site_hashmap,
auditor,
};
let config_file_content = toml::to_string(&config_toml)
.context("Failed to serialize configuration to TOML")?;
let config_file_path = if let Some(config_file_path) = config_file_path_opt {
PathBuf::from(config_file_path)
} else {
get_default_manta_config_file_path()?
};
let parent_dir = config_file_path
.parent()
.context("Configuration file path has no parent directory")?;
std::fs::create_dir_all(parent_dir).with_context(|| {
format!(
"Failed to create config directory '{}'",
parent_dir.display()
)
})?;
let mut config_file = File::create(&config_file_path).with_context(|| {
format!(
"Failed to create config file '{}'",
config_file_path.display()
)
})?;
config_file
.write_all(config_file_content.as_bytes())
.with_context(|| {
format!(
"Failed to write config file '{}'",
config_file_path.display()
)
})?;
log::info!(
"Configuration file '{}' created",
config_file_path.to_string_lossy()
);
Ok(())
}