use crate::cli::GlobalArgs;
use elasticctl_core::{
Capabilities, Config, Credential, Error, ErrorKind, Overrides, Resolved, Result, Transport,
};
use tokio::sync::OnceCell;
pub fn config_path(global: &GlobalArgs) -> std::path::PathBuf {
global.config.clone().unwrap_or_else(Config::default_path)
}
pub struct Context {
pub resolved: Resolved,
pub global: GlobalArgs,
transport: OnceCell<Transport>,
}
impl Context {
pub fn build(global: &GlobalArgs) -> Result<Context> {
let path = config_path(global);
let config = Config::load(&path)?;
let flags = Overrides {
space: global.space.clone(),
timeout_secs: global.timeout,
..Default::default()
};
let env = Overrides::try_from_env_with_flags(&flags)?;
let overrides = flags.merge_over(env);
let resolved = config.resolve(global.profile.as_deref(), &overrides)?;
Ok(Context {
resolved,
global: global.clone(),
transport: OnceCell::new(),
})
}
pub async fn transport(&self) -> Result<&Transport> {
self.transport
.get_or_try_init(|| async {
Transport::with_debug(&self.resolved.profile, self.global.debug)
})
.await
}
pub async fn capabilities(&self) -> Result<&Capabilities> {
let transport = self.transport().await?;
transport.capabilities().await
}
pub fn require_credential(&self) -> Result<()> {
if !Credential::is_configured(&self.resolved.profile) {
return Err(Error::new(
ErrorKind::Auth,
format!(
"Profile '{}' has no credential. Run: elasticctl config init --profile {}",
self.resolved.name, self.resolved.name
),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use elasticctl_core::{Profile, Source};
fn profile_with(
api_key: Option<&str>,
username: Option<&str>,
password: Option<&str>,
) -> Profile {
Profile {
kibana_url: "https://kb.example.com".into(),
es_url: None,
api_key: api_key.map(String::from),
username: username.map(String::from),
password: password.map(String::from),
space: "default".into(),
verify: true,
timeout_secs: 30,
}
}
fn context_with(profile: Profile) -> Context {
Context {
resolved: Resolved {
profile,
name: "test".into(),
source: Source::Profile,
},
global: GlobalArgs::default(),
transport: OnceCell::new(),
}
}
#[test]
fn require_credential_rejects_an_empty_api_key() {
let ctx = context_with(profile_with(Some(""), None, None));
let err = ctx.require_credential().unwrap_err();
assert_eq!(err.kind, ErrorKind::Auth);
assert!(err.message.contains("test"), "{}", err.message);
}
#[test]
fn require_credential_rejects_a_username_without_a_password() {
let ctx = context_with(profile_with(None, Some("elastic"), None));
assert_eq!(ctx.require_credential().unwrap_err().kind, ErrorKind::Auth);
}
#[test]
fn require_credential_accepts_a_configured_api_key() {
let ctx = context_with(profile_with(Some("essu_abc"), None, None));
assert!(ctx.require_credential().is_ok());
}
#[tokio::test]
async fn build_succeeds_for_a_profile_with_no_credential() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
r#"
current = "nocreds"
[profiles.nocreds]
kibana_url = "https://kb.example.com"
space = "default"
verify = true
timeout_secs = 30
"#,
)
.unwrap();
let global = GlobalArgs {
config: Some(path),
..GlobalArgs::default()
};
let ctx = Context::build(&global).expect(
"Context::build must succeed without a credential — that requirement belongs to \
transport()/require_credential(), not to loading and resolving config",
);
assert!(ctx.require_credential().is_err());
}
}