use silicon_iam_client::{Error as ClientError, models};
use crate::{
cli::EnvCommand,
context::Context,
error::{CliError, Result},
output::{Format, Table, json, label, next_cursor, or_dash, timestamp, timestamp_or_dash},
};
#[allow(
clippy::too_many_lines,
reason = "one arm per verb; splitting the match would only move it elsewhere"
)]
pub async fn run(context: &Context, command: EnvCommand) -> Result<()> {
if let EnvCommand::Current = command {
let environment_id = context.require_test()?;
let described = context
.anonymous()
.environments()
.current()
.await
.map_err(|error| environment_error(environment_id, error))?;
return match context.format {
Format::Json => json(&described),
Format::Text => {
let mut table = Table::new(["field", "value"]);
table.row(["id", &described.id.to_string()]);
table.row(["name", &described.name]);
table.row(["description", &or_dash(described.description.as_deref())]);
table.row(["key_generation", &described.key_generation.to_string()]);
table.row(["created", ×tamp(described.created_at)]);
table.print();
Ok(())
}
};
}
if let EnvCommand::Clean {
environment_id: None,
} = command
{
let environment_id = context.require_test()?;
let cleaned = context
.anonymous()
.environments()
.clean_current(&context.mutation())
.await
.map_err(|error| environment_error(environment_id, error))?;
return report_cleaning(context, &cleaned);
}
let client = context.authenticated().await?;
let org = context.organization()?;
match command {
EnvCommand::Current
| EnvCommand::Clean {
environment_id: None,
} => unreachable!(),
EnvCommand::List { status, page } => {
let listed = client
.environments()
.list(org, status.as_deref(), &page.paging())
.await?;
match context.format {
Format::Json => json(&listed),
Format::Text => {
let mut table =
Table::new(["id", "name", "status", "key gen", "last activity"]);
for entry in &listed.items {
table.row([
entry.id.to_string(),
entry.name.clone(),
label(&entry.status),
entry.key_generation.to_string(),
timestamp(entry.last_activity_at),
]);
}
table.print();
next_cursor(
listed
.page
.get("has_more")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
listed
.page
.get("next_cursor")
.and_then(serde_json::Value::as_str),
);
Ok(())
}
}
}
EnvCommand::Create { name, description } => {
let created = client
.environments()
.create(
org,
&models::TestingEnvironmentCreate { name, description },
&context.mutation(),
)
.await?;
context.remember_testing_environment(created.id, created.key.clone())?;
match context.format {
Format::Json => json(&created),
Format::Text => {
println!("Created {} ({}).", created.name, created.id);
println!("Key: {}", created.key);
println!(
"This device stored the key. Enter it with: iam --test {} <command>",
created.id
);
Ok(())
}
}
}
EnvCommand::Show { environment_id } => {
let environment = client.environments().get(org, environment_id).await?;
report(context, &environment)
}
EnvCommand::Update {
environment_id,
name,
description,
clear_description,
} => {
let current = client.environments().get(org, environment_id).await?;
let updated = client
.environments()
.update(
org,
environment_id,
current.version,
&models::TestingEnvironmentPatch {
name,
description: nullable_patch(description, clear_description),
},
&context.mutation(),
)
.await?;
report(context, &updated)
}
EnvCommand::Delete { environment_id } => {
let retired = client
.environments()
.delete(org, environment_id, &context.mutation())
.await?;
match context.format {
Format::Json => json(&retired),
Format::Text => {
println!("Retired {}.", retired.name);
if let Some(purge_after) = retired.purge_after {
println!(
"Recoverable with `iam env restore {}` until {}.",
retired.id,
timestamp(purge_after)
);
}
Ok(())
}
}
}
EnvCommand::Restore { environment_id } => {
let restored = client
.environments()
.restore(org, environment_id, &context.mutation())
.await?;
report(context, &restored)
}
EnvCommand::Key { environment_id } => {
let key = client.environments().key(org, environment_id).await?;
context.remember_testing_environment(environment_id, key.key.clone())?;
match context.format {
Format::Json => json(&key),
Format::Text => {
println!("{}", key.key);
Ok(())
}
}
}
EnvCommand::RotateKey { environment_id } => {
let rotated = client
.environments()
.rotate_key(org, environment_id, &context.mutation())
.await?;
context.remember_testing_environment(environment_id, rotated.key.clone())?;
match context.format {
Format::Json => json(&rotated),
Format::Text => {
println!("Key: {}", rotated.key);
println!("The previous key stopped working.");
Ok(())
}
}
}
EnvCommand::Clean {
environment_id: Some(environment_id),
} => {
let cleaned = client
.environments()
.clean(org, environment_id, &context.mutation())
.await?;
report_cleaning(context, &cleaned)
}
}
}
fn environment_error(environment_id: uuid::Uuid, error: ClientError) -> CliError {
if error
.api()
.is_some_and(silicon_iam_client::ApiError::is_unauthenticated)
{
CliError::TestingEnvironmentUnavailable {
environment_id,
source: error,
}
} else {
CliError::Client(error)
}
}
#[allow(
clippy::option_option,
reason = "JSON Merge Patch has distinct omitted, null, and value states"
)]
fn nullable_patch<T>(value: Option<T>, clear: bool) -> Option<Option<T>> {
if clear { Some(None) } else { value.map(Some) }
}
fn report(context: &Context, environment: &models::TestingEnvironment) -> Result<()> {
match context.format {
Format::Json => json(environment),
Format::Text => {
let mut table = Table::new(["field", "value"]);
table.row(["id", &environment.id.to_string()]);
table.row(["name", &environment.name]);
table.row(["description", &or_dash(environment.description.as_deref())]);
table.row(["status", &label(&environment.status)]);
table.row(["key_generation", &environment.key_generation.to_string()]);
table.row(["last_activity", ×tamp(environment.last_activity_at)]);
table.row(["purge_after", ×tamp_or_dash(environment.purge_after)]);
table.row(["version", &environment.version.to_string()]);
table.print();
Ok(())
}
}
}
fn report_cleaning(context: &Context, cleaning: &models::TestingEnvironmentCleaning) -> Result<()> {
match context.format {
Format::Json => json(cleaning),
Format::Text => {
println!("Erased {} rows.", cleaning.erased_rows);
Ok(())
}
}
}