use crate::BoxError;
use asimov_env::paths::asimov_root;
use asimov_module::{ConfigurationVariable, ModuleManifest, ModuleName};
use clientele::{
StandardOptions,
SysexitsError::*,
crates::clap::{Subcommand, builder::PossibleValuesParser},
};
use color_print::ceprintln;
use std::{path::PathBuf, string::String, vec::Vec};
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
#[clap(alias = "list")]
Show {
name: ModuleName,
#[arg(value_name = "FORMAT", short = 'o', long)]
#[arg(value_parser = PossibleValuesParser::new(["cli", "json"]), hide_possible_values = true)]
output: Option<String>,
},
Get {
name: ModuleName,
key: String,
#[arg(long)]
stored: bool,
},
Set {
name: ModuleName,
#[arg(value_name = "KEY=VALUE", required_unless_present = "from_json")]
assignments: Vec<String>,
#[arg(long, conflicts_with = "from_json")]
stdin: bool,
#[arg(long, conflicts_with = "assignments")]
from_json: bool,
},
Unset {
name: ModuleName,
#[arg(required_unless_present = "all")]
keys: Vec<String>,
#[arg(long, conflicts_with = "keys")]
all: bool,
},
Setup {
name: ModuleName,
},
}
impl ConfigCommand {
pub async fn run(&self, flags: &StandardOptions) -> Result<(), BoxError> {
use ConfigCommand::*;
match self {
Show { name, output } => {
show::show(name, output.as_deref().unwrap_or("cli"), flags).await
},
Get { name, key, stored } => get::get(name, key, *stored, flags).await,
Set {
name,
assignments,
stdin,
from_json,
} => set::set(name, assignments, *stdin, *from_json, flags).await,
Unset { name, keys, all } => unset::unset(name, keys, *all, flags).await,
Setup { name } => setup::setup(name, flags).await,
}
}
}
mod get;
mod set;
mod setup;
mod show;
mod unset;
pub(super) const MASK: &str = "******";
pub(super) struct Module {
pub name: ModuleName,
pub manifest: ModuleManifest,
pub profile: &'static str,
pub conf_dir: PathBuf,
}
pub(super) async fn open(module_name: &ModuleName) -> Result<Module, BoxError> {
let manifest = asimov_registry::Registry::default()
.read_manifest(module_name)
.await
.map_err(|e| {
tracing::error!("failed to read manifest for module `{module_name}`: {e}");
if let asimov_registry::error::ManifestError::NotInstalled = e {
ceprintln!(
"<s,dim>hint:</> Check if the module is installed with: <s>asimov module list</>"
);
}
EX_UNAVAILABLE
})?
.manifest;
let is_valid_name = |name: &str| {
!name.is_empty()
&& !name.starts_with('.')
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
};
let variables = manifest
.config
.as_ref()
.map(|c| c.variables.as_slice())
.unwrap_or_default();
if let Some(var) = variables.iter().find(|var| !is_valid_name(&var.name)) {
ceprintln!(
"<s,r>error:</> module <s>{module_name}</> declares an invalid configuration variable name: `{}`",
var.name
);
return Err(EX_DATAERR.into());
}
let profile = "default"; let conf_dir = asimov_root()
.join("configs")
.join(profile)
.join(module_name.as_str());
Ok(Module {
name: module_name.clone(),
manifest,
profile,
conf_dir,
})
}
impl Module {
pub fn variables(&self) -> &[ConfigurationVariable] {
self.manifest
.config
.as_ref()
.map(|c| c.variables.as_slice())
.unwrap_or_default()
}
pub fn variable(&self, key: &str) -> Result<&ConfigurationVariable, BoxError> {
self.variables()
.iter()
.find(|var| var.name == key)
.ok_or_else(|| {
ceprintln!(
"<s,r>error:</> `{key}` is not the name of a configuration variable for <s>{}</> module",
self.name
);
EX_USAGE.into()
})
}
pub fn require_variables(&self) -> Result<&[ConfigurationVariable], BoxError> {
let variables = self.variables();
if variables.is_empty() {
ceprintln!(
"<s,r>error:</> module <s>{}</> has no configuration variables",
self.name
);
return Err(EX_USAGE.into());
}
Ok(variables)
}
pub fn var_file(&self, key: &str) -> PathBuf {
self.conf_dir.join(key)
}
pub async fn source(&self, var: &ConfigurationVariable) -> Source {
if let Some(env_name) = var.environment.as_deref()
&& std::env::var(env_name).is_ok()
{
return Source::Environment;
}
if tokio::fs::try_exists(self.var_file(&var.name))
.await
.unwrap_or(false)
{
return Source::Stored;
}
if var.default_value.is_some() {
return Source::Default;
}
Source::Unset
}
pub async fn create_conf_dir(&self) -> tokio::io::Result<()> {
tokio::fs::create_dir_all(&self.conf_dir)
.await
.inspect_err(|e| {
tracing::error!(
"failed to create configuration directory for module `{}`: {e}",
self.name
)
})
}
#[cfg(not(unix))]
pub async fn set_permissions(&self) -> tokio::io::Result<()> {
Ok(())
}
#[cfg(unix)]
pub async fn set_permissions(&self) -> tokio::io::Result<()> {
async {
use std::os::unix::fs::PermissionsExt;
let metadata = match tokio::fs::symlink_metadata(&self.conf_dir).await {
Ok(metadata) => metadata,
Err(e) if e.kind() == tokio::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
if metadata.is_symlink() {
return Ok(());
}
let mut directories = vec![self.conf_dir.clone()];
while let Some(directory) = directories.pop() {
tokio::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o700))
.await?;
let mut entries = tokio::fs::read_dir(&directory).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let metadata = tokio::fs::symlink_metadata(&path).await?;
if metadata.is_dir() {
directories.push(path);
} else if metadata.is_file() {
tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.await?;
}
}
}
Ok(())
}
.await
.inspect_err(|e| {
tracing::error!(
"failed to set configuration permissions for module `{}`: {e}",
self.name
)
})
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Source {
Environment,
Stored,
Default,
Unset,
}
impl Source {
pub fn as_str(self) -> &'static str {
match self {
Source::Environment => "environment",
Source::Stored => "stored",
Source::Default => "default",
Source::Unset => "unset",
}
}
}
pub(super) fn prompt_for_value(prompt: String, secret: bool) -> Result<String, BoxError> {
let input = if secret {
dialoguer::Password::new()
.with_prompt(prompt)
.allow_empty_password(true)
.interact()
} else {
dialoguer::Input::<String>::new()
.with_prompt(prompt)
.allow_empty(true)
.interact_text()
};
input.map_err(|e| {
tracing::error!("failed to read a value from the terminal: {e}");
EX_IOERR.into()
})
}