mod completions;
mod render;
#[cfg(feature = "tui")]
use std::io::IsTerminal;
use std::process::ExitCode;
use clap::Parser;
use ignition_core::actions;
use ignition_core::client::ReqwestGatewayApi;
use ignition_core::config::{self, AuthRef, Config, Credential, SecretStore};
use ignition_core::error::CoreError;
use crate::render::{RenderMode, render_error, render_log_entry_line, render_ok};
use ignition_cli::cli;
use ignition_cli::cli::{
BackupArgs, BackupCommand, Cli, Commands, EamArgs, EamCommand, EamTaskCommand, LintArgs,
LogLevel, LoggersCmd, LogsArgs, LogsCmd, ProfileArgs, ProfileCmd, ProjectArgs, ProjectCommand,
ResourceArgs, ResourceCommand, RigArgs, RigCommand, ScheduleMode, ScriptArgs, ScriptCommand,
SessionsArgs, SessionsCmd, TagsAlarmsCommand, TagsArgs, TagsCommand, TagsConfigCommand,
TagsHistoryCommand, TagsProviderCommand, TagsUdtCommand, WaitArgs, WaitCmd, WebdevArgs,
WebdevCommand,
};
enum ActionOutput {
Version(actions::version::VersionResult),
Status(actions::inspect::StatusResult),
Modules(actions::inspect::ModulesResult),
Metrics(actions::inspect::MetricsResult),
Sessions(actions::sessions::SessionsResult),
SessionsTerminate(actions::sessions::TerminateResult),
Connections(actions::connections::ConnectionsResult),
LogsList(actions::logs::LogPage),
LogsTail(actions::logs::TailResult),
LogsDownload(actions::logs::DownloadResult),
LoggersList(actions::logs::LoggersEnvelope),
LoggerSet(actions::logs::SetLevelResult),
LoggerReset(actions::logs::ResetResult),
Restart(actions::restart::RestartResult),
RestartWait(actions::restart::RestartWaitResult),
Wait(actions::restart::WaitResult),
Doctor(actions::doctor::DoctorResult),
Completions {
shell: clap_complete::aot::Shell,
},
ProfileAdd(actions::profile::ProfileAddResult),
ProfileList(actions::profile::ProfileListResult),
ProfileUse(actions::profile::ProfileUseResult),
ProjectsList(actions::projects::ProjectsResult),
ProjectNew(actions::projects::ProjectSummary),
ProjectCopy(actions::projects::ProjectCopyResult),
ProjectRename(actions::projects::ProjectRenameResult),
ProjectSet(actions::projects::ProjectSetResult),
ProjectDelete(actions::projects::ProjectDeleteResult),
ProjectExport(actions::projects::ExportResult),
ProjectExportDecoded(actions::projects::ExportDecodedResult),
ProjectImport(actions::projects::ImportResult),
ProjectDiff(actions::projects::ProjectDiffResult),
ProjectSync(actions::projects::ProjectSyncResult),
ResourcesList(actions::resources::ResourcesResult),
ResourceGet(actions::resources::ResourceGetResult),
ResourcePut(actions::resources::ResourcePutResult),
ResourceDelete(actions::resources::ResourceDeleteResult),
RigUp(actions::rig::RigUpResult),
RigDown(actions::rig::RigDownResult),
RigReset(actions::rig::RigResetResult),
RigStatus(actions::rig::RigStatusResult),
RigLogs(actions::rig::RigLogsResult),
RigSnapshot(actions::rig::SnapshotResult),
RigRestore(actions::rig::RestoreResult),
BackupDownload(actions::backup::BackupDownloadResult),
BackupRestore(actions::backup::BackupRestoreResult),
EamHistory(actions::eam::EamHistoryResult),
EamTasks(actions::eam::EamTasksResult),
EamTaskDetail(actions::eam::EamTaskDetailResult),
EamTaskCreate(actions::eam::EamTaskCreateResult),
EamTaskForce(actions::eam::EamTaskForceResult),
ScriptRun(actions::script::ScriptRunResult),
Lint(actions::lint::LintResult),
RigTrialStatus(actions::rig::TrialStatusResult),
RigTrialReset(actions::rig::TrialResetResult),
WebdevDeploy(actions::webdev::WebdevDeployResult),
WebdevStatus(actions::webdev::WebdevStatusResult),
TagProviders(actions::tags::TagProvidersResult),
TagProviderCreate(actions::tags::TagProviderCreateResult),
TagProviderDelete(actions::tags::TagProviderDeleteResult),
TagsBrowse(actions::tags::TagsBrowseResult),
TagsBrowseFromExport(actions::tags::TagBrowseFromExportResult),
TagsRead(actions::tags::TagsReadResult),
TagsWrite(actions::tags::TagsWriteResult),
TagsConfigGet(actions::tags::TagsConfigGetResult),
TagsConfigCreate(actions::tags::TagsConfigWriteResult),
TagsConfigEdit(actions::tags::TagsConfigWriteResult),
TagsConfigDelete(actions::tags::TagsConfigDeleteResult),
TagsUdtTypes(actions::tags::TagsUdtTypesResult),
TagsUdtDef(actions::tags::TagsUdtDefResult),
TagsExport(actions::tags::TagsExportResult),
TagsImport(actions::tags::TagsImportResult),
TagsAlarmsActive(actions::tags::TagsAlarmsActiveResult),
TagsAlarmsHistory(actions::tags::TagsAlarmsHistoryResult),
TagsAlarmsAck(actions::tags::TagsAlarmsAckResult),
TagsHistoryQuery(actions::tags::TagsHistoryQueryResult),
#[cfg(feature = "tui")]
TuiExited,
}
impl ActionOutput {
pub(crate) fn render_json(&self, profile: Option<&str>, compact: bool) -> String {
use ignition_core::output::render_success;
match self {
ActionOutput::Version(result) => render_success(profile, result, compact),
ActionOutput::Status(result) => render_success(profile, result, compact),
ActionOutput::Modules(result) => render_success(profile, result, compact),
ActionOutput::Metrics(result) => render_success(profile, result, compact),
ActionOutput::Sessions(result) => render_success(profile, result, compact),
ActionOutput::SessionsTerminate(result) => render_success(profile, result, compact),
ActionOutput::Connections(result) => render_success(profile, result, compact),
ActionOutput::LogsList(result) => render_success(profile, result, compact),
ActionOutput::LogsTail(result) => render_success(profile, result, compact),
ActionOutput::LogsDownload(result) => render_success(profile, result, compact),
ActionOutput::LoggersList(result) => render_success(profile, result, compact),
ActionOutput::LoggerSet(result) => render_success(profile, result, compact),
ActionOutput::LoggerReset(result) => render_success(profile, result, compact),
ActionOutput::Restart(result) => render_success(profile, result, compact),
ActionOutput::RestartWait(result) => render_success(profile, result, compact),
ActionOutput::Wait(result) => render_success(profile, result, compact),
ActionOutput::Doctor(result) => render_success(profile, result, compact),
ActionOutput::Completions { shell } => crate::completions::completions(*shell),
ActionOutput::ProfileAdd(result) => render_success(profile, result, compact),
ActionOutput::ProfileList(result) => render_success(profile, result, compact),
ActionOutput::ProfileUse(result) => render_success(profile, result, compact),
ActionOutput::ProjectsList(result) => render_success(profile, result, compact),
ActionOutput::ProjectNew(result) => render_success(profile, result, compact),
ActionOutput::ProjectCopy(result) => render_success(profile, result, compact),
ActionOutput::ProjectRename(result) => render_success(profile, result, compact),
ActionOutput::ProjectSet(result) => render_success(profile, result, compact),
ActionOutput::ProjectDelete(result) => render_success(profile, result, compact),
ActionOutput::ProjectExport(result) => render_success(profile, result, compact),
ActionOutput::ProjectExportDecoded(result) => render_success(profile, result, compact),
ActionOutput::ProjectImport(result) => render_success(profile, result, compact),
ActionOutput::ProjectDiff(result) => render_success(profile, result, compact),
ActionOutput::ProjectSync(result) => render_success(profile, result, compact),
ActionOutput::ResourcesList(result) => render_success(profile, result, compact),
ActionOutput::ResourceGet(result) => render_success(profile, result, compact),
ActionOutput::ResourcePut(result) => render_success(profile, result, compact),
ActionOutput::ResourceDelete(result) => render_success(profile, result, compact),
ActionOutput::RigUp(result) => render_success(profile, result, compact),
ActionOutput::RigDown(result) => render_success(profile, result, compact),
ActionOutput::RigReset(result) => render_success(profile, result, compact),
ActionOutput::RigStatus(result) => render_success(profile, result, compact),
ActionOutput::RigLogs(result) => render_success(profile, result, compact),
ActionOutput::RigSnapshot(result) => render_success(profile, result, compact),
ActionOutput::RigRestore(result) => render_success(profile, result, compact),
ActionOutput::BackupDownload(result) => render_success(profile, result, compact),
ActionOutput::BackupRestore(result) => render_success(profile, result, compact),
ActionOutput::EamHistory(result) => render_success(profile, result, compact),
ActionOutput::EamTasks(result) => render_success(profile, result, compact),
ActionOutput::EamTaskDetail(result) => render_success(profile, result, compact),
ActionOutput::EamTaskCreate(result) => render_success(profile, result, compact),
ActionOutput::EamTaskForce(result) => render_success(profile, result, compact),
ActionOutput::ScriptRun(result) => render_success(profile, result, compact),
ActionOutput::Lint(result) => render_success(profile, result, compact),
ActionOutput::RigTrialStatus(result) => render_success(profile, result, compact),
ActionOutput::RigTrialReset(result) => render_success(profile, result, compact),
ActionOutput::WebdevDeploy(result) => render_success(profile, result, compact),
ActionOutput::WebdevStatus(result) => render_success(profile, result, compact),
ActionOutput::TagProviders(result) => render_success(profile, result, compact),
ActionOutput::TagProviderCreate(result) => render_success(profile, result, compact),
ActionOutput::TagProviderDelete(result) => render_success(profile, result, compact),
ActionOutput::TagsBrowse(result) => render_success(profile, result, compact),
ActionOutput::TagsBrowseFromExport(result) => render_success(profile, result, compact),
ActionOutput::TagsRead(result) => render_success(profile, result, compact),
ActionOutput::TagsWrite(result) => render_success(profile, result, compact),
ActionOutput::TagsConfigGet(result) => render_success(profile, result, compact),
ActionOutput::TagsConfigCreate(result) => render_success(profile, result, compact),
ActionOutput::TagsConfigEdit(result) => render_success(profile, result, compact),
ActionOutput::TagsConfigDelete(result) => render_success(profile, result, compact),
ActionOutput::TagsUdtTypes(result) => render_success(profile, result, compact),
ActionOutput::TagsUdtDef(result) => render_success(profile, result, compact),
ActionOutput::TagsExport(result) => render_success(profile, result, compact),
ActionOutput::TagsImport(result) => render_success(profile, result, compact),
ActionOutput::TagsAlarmsActive(result) => render_success(profile, result, compact),
ActionOutput::TagsAlarmsHistory(result) => render_success(profile, result, compact),
ActionOutput::TagsAlarmsAck(result) => render_success(profile, result, compact),
ActionOutput::TagsHistoryQuery(result) => render_success(profile, result, compact),
#[cfg(feature = "tui")]
ActionOutput::TuiExited => String::new(),
}
}
}
fn main() -> ExitCode {
let mut cli = match Cli::try_parse() {
Ok(c) => c,
Err(e) => e.exit(),
};
apply_env_defaults(&mut cli);
init_tracing(cli.verbose);
let mode = RenderMode::resolve(cli.json, cli.compact);
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("failed to build async runtime");
let (profile, result) = runtime.block_on(dispatch(cli, mode));
match result {
Ok(out) => {
render_ok(&out, profile.as_deref(), mode);
if let ActionOutput::Lint(lint) = &out
&& let Some(code) = lint.strict_exit_code()
{
return ExitCode::from(code);
}
ExitCode::SUCCESS
}
Err(err) => {
render_error(&err, profile.as_deref(), mode);
ExitCode::from(err.exit_code())
}
}
}
async fn dispatch(cli: Cli, mode: RenderMode) -> (Option<String>, Result<ActionOutput, CoreError>) {
if let Commands::Completions { shell } = cli.command {
return (None, Ok(ActionOutput::Completions { shell }));
}
let path = config::config_path();
let mut config = match config::load(&path) {
Ok(config) => config,
Err(err) => return (None, Err(err)),
};
match cli.command {
Commands::Version => match resolve_profile_context(&mut config, cli.profile.as_deref()) {
Ok(None) => {
let result = actions::version::version(None, env!("CARGO_PKG_VERSION")).await;
(None, result.map(ActionOutput::Version))
}
Ok(Some((name, profile))) => {
let credential = match resolve_secret_opt(&name, &profile.auth) {
Ok(credential) => credential,
Err(err) => return (Some(name), Err(err)),
};
let result = match ReqwestGatewayApi::new(&profile, credential) {
Ok(api) => {
actions::version::version(Some(&api), env!("CARGO_PKG_VERSION")).await
}
Err(err) => Err(err),
};
(Some(name), result.map(ActionOutput::Version))
}
Err(err) => (None, Err(err)),
},
Commands::Status => {
run_inspection(&mut config, cli.profile.as_deref(), Inspection::Status).await
}
Commands::Modules { quarantined } => {
run_inspection(
&mut config,
cli.profile.as_deref(),
Inspection::Modules(quarantined),
)
.await
}
Commands::Metrics { history } => {
run_inspection(
&mut config,
cli.profile.as_deref(),
Inspection::Metrics(history),
)
.await
}
Commands::Sessions(SessionsArgs { r#type, command }) => match command {
None => {
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::sessions::sessions(&api, r#type.map(Into::into))
.await
.map(ActionOutput::Sessions),
Err(err) => Err(err),
};
(name, result)
}
Some(SessionsCmd::Terminate {
r#type,
id,
message,
}) => {
if let Err(err) = require_confirmation(cli.yes, "sessions terminate") {
return (None, Err(err));
}
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::sessions::terminate_session(
&api,
r#type.into(),
&id,
message.as_deref(),
)
.await
.map(ActionOutput::SessionsTerminate),
Err(err) => Err(err),
};
(name, result)
}
},
Commands::Connections { r#type } => {
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::connections::connections(&api, r#type.map(Into::into))
.await
.map(ActionOutput::Connections),
Err(err) => Err(err),
};
(name, result)
}
Commands::Logs(LogsArgs {
logger,
min_level,
since,
limit,
follow,
interval,
timeout,
command,
}) => match command {
None => {
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) if follow => {
let min_level = min_level.map(LogLevel::wire);
let header = name.clone();
let mut first = true;
let sink: &mut (dyn FnMut(&ignition_core::client::logs::LogEntry) + Send) =
&mut |entry| match mode {
RenderMode::Human => {
if first {
if let Some(name) = &header {
println!("[profile: {name}]");
}
first = false;
}
println!("{}", render_log_entry_line(entry));
}
RenderMode::PrettyJson | RenderMode::CompactJson => {
println!(
"{}",
serde_json::to_string(entry)
.expect("a log entry serializes")
);
}
};
actions::logs::tail(
&api,
logger.as_deref(),
min_level,
since,
std::time::Duration::from_secs(interval),
timeout.map(std::time::Duration::from_secs),
sink,
)
.await
.map(ActionOutput::LogsTail)
}
Ok(api) => {
let min_level = min_level.map(LogLevel::wire);
actions::logs::list_logs(&api, logger.as_deref(), min_level, since, limit)
.await
.map(ActionOutput::LogsList)
}
Err(err) => Err(err),
};
(name, result)
}
Some(LogsCmd::Download { output }) => {
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => {
let stem = name.as_deref().unwrap_or("gateway");
actions::logs::download(&api, output.as_deref(), stem)
.await
.map(ActionOutput::LogsDownload)
}
Err(err) => Err(err),
};
(name, result)
}
Some(LogsCmd::Loggers(loggers_args)) => match loggers_args.command {
None => {
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::logs::loggers(&api, loggers_args.search.as_deref())
.await
.map(ActionOutput::LoggersList),
Err(err) => Err(err),
};
(name, result)
}
Some(LoggersCmd::Set {
name: logger,
level,
}) => {
if let Err(err) = require_confirmation(cli.yes, "logs loggers set") {
return (None, Err(err));
}
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::logs::set_logger_level(&api, &logger, level.wire())
.await
.map(ActionOutput::LoggerSet),
Err(err) => Err(err),
};
(name, result)
}
Some(LoggersCmd::Reset) => {
if let Err(err) = require_confirmation(cli.yes, "logs loggers reset") {
return (None, Err(err));
}
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::logs::reset_logger_levels(&api)
.await
.map(ActionOutput::LoggerReset),
Err(err) => Err(err),
};
(name, result)
}
},
},
Commands::Restart {
wait,
timeout,
interval,
} => {
if let Err(err) = require_confirmation(cli.yes, "restart") {
return (None, Err(err));
}
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => {
let interval = interval.map_or(
actions::restart::DEFAULT_INTERVAL,
std::time::Duration::from_secs,
);
let timeout = timeout.map_or(
actions::restart::RESTART_TIMEOUT,
std::time::Duration::from_secs,
);
if wait {
actions::restart::restart_and_wait(
&api,
interval,
timeout,
actions::restart::RESTART_FLOOR,
)
.await
.map(ActionOutput::RestartWait)
} else {
actions::restart::restart(&api)
.await
.map(ActionOutput::Restart)
}
}
Err(err) => Err(err),
};
(name, result)
}
Commands::Wait(WaitArgs { command }) => match command {
WaitCmd::Gateway { interval, timeout } => {
let (name, api) = resolve_headerless_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::restart::wait_gateway(
&api,
std::time::Duration::from_secs(interval),
std::time::Duration::from_secs(timeout),
)
.await
.map(ActionOutput::Wait),
Err(err) => Err(err),
};
(name, result)
}
WaitCmd::Restart { interval, timeout } => {
let (name, api) = resolve_headerless_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::restart::wait_restart(
&api,
std::time::Duration::from_secs(interval),
std::time::Duration::from_secs(timeout),
actions::restart::RESTART_FLOOR,
)
.await
.map(ActionOutput::Wait),
Err(err) => Err(err),
};
(name, result)
}
WaitCmd::Module {
id,
interval,
timeout,
} => {
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::restart::wait_module(
&api,
&id,
std::time::Duration::from_secs(interval),
std::time::Duration::from_secs(timeout),
)
.await
.map(ActionOutput::Wait),
Err(err) => Err(err),
};
(name, result)
}
},
Commands::Doctor {
check_write,
webdev_route,
} => match resolve_profile_context(&mut config, cli.profile.as_deref()) {
Ok(None) => (None, Err(CoreError::NoActiveProfile)),
Ok(Some((name, profile))) => {
let credential = resolve_secret_opt(&name, &profile.auth);
match credential {
Ok(credential) => {
let credential_present = credential.is_some();
match ReqwestGatewayApi::new(&profile, credential) {
Ok(api) => {
let opts = actions::doctor::DoctorOptions {
check_write,
webdev_route,
};
let result = actions::doctor::doctor(
&api,
profile.url.as_str(),
credential_present,
&opts,
)
.await;
(Some(name), Ok(ActionOutput::Doctor(result)))
}
Err(err) => (Some(name), Err(err)),
}
}
Err(err) => (Some(name), Err(err)),
}
}
Err(err) => (None, Err(err)),
},
Commands::Project(ProjectArgs { command }) => match command {
ProjectCommand::List => {
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::projects::projects(&api)
.await
.map(ActionOutput::ProjectsList),
Err(err) => Err(err),
};
(profile, result)
}
ProjectCommand::New {
name: project_name,
title,
description,
parent,
inheritable,
disabled,
} => {
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => {
let opts = actions::projects::NewOptions {
enabled: !disabled,
title,
description,
parent,
inheritable: inheritable.then_some(true),
};
actions::projects::project_new(&api, &project_name, &opts)
.await
.map(ActionOutput::ProjectNew)
}
Err(err) => Err(err),
};
(profile, result)
}
ProjectCommand::Copy { src, dst } => {
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::projects::project_copy(&api, &src, &dst)
.await
.map(ActionOutput::ProjectCopy),
Err(err) => Err(err),
};
(profile, result)
}
ProjectCommand::Rename { old_name, new_name } => {
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::projects::project_rename(&api, &old_name, &new_name)
.await
.map(ActionOutput::ProjectRename),
Err(err) => Err(err),
};
(profile, result)
}
ProjectCommand::Set {
name: project_name,
title,
description,
parent,
set_enabled,
disabled,
inheritable,
} => {
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => {
let enabled = if set_enabled {
Some(true)
} else if disabled {
Some(false)
} else {
None
};
let opts = actions::projects::SetOptions {
title,
description,
parent,
enabled,
inheritable,
};
actions::projects::project_set(&api, &project_name, &opts)
.await
.map(ActionOutput::ProjectSet)
}
Err(err) => Err(err),
};
(profile, result)
}
ProjectCommand::Delete { name: project_name } => {
if let Err(err) = require_confirmation(cli.yes, "project delete") {
return (None, Err(err));
}
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::projects::project_delete(&api, &project_name)
.await
.map(ActionOutput::ProjectDelete),
Err(err) => Err(err),
};
(profile, result)
}
ProjectCommand::Export {
name: project_name,
output,
decode_scripts,
} => {
if mode == RenderMode::Human {
eprintln!("exporting {project_name} …");
}
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => {
if decode_scripts {
actions::projects::project_export_decoded(
&api,
&project_name,
output.as_deref(),
)
.await
.map(ActionOutput::ProjectExportDecoded)
} else {
actions::projects::project_export(
&api,
&project_name,
output.as_deref(),
)
.await
.map(ActionOutput::ProjectExport)
}
}
Err(err) => Err(err),
};
(profile, result)
}
ProjectCommand::Import {
name: project_name,
file,
collision_policy,
encode_scripts,
} => {
if matches!(collision_policy, cli::CollisionPolicy::Overwrite)
&& let Err(err) =
require_confirmation(cli.yes, "project import --collision-policy overwrite")
{
return (None, Err(err));
}
let zip = if encode_scripts {
if file == "-" {
return (
None,
Err(CoreError::InvalidInput {
reason: "--encode-scripts needs the decoded export \
DIRECTORY via --file (stdin cannot carry one)"
.to_string(),
}),
);
}
match ignition_core::client::scripts_codec::encode_export_tree(
std::path::Path::new(&file),
) {
Ok(zip) => zip,
Err(err) => return (None, Err(err)),
}
} else if file == "-" {
use tokio::io::AsyncReadExt;
let mut buffer = Vec::new();
match tokio::io::stdin().read_to_end(&mut buffer).await {
Ok(_) => buffer,
Err(err) => {
return (
None,
Err(CoreError::InvalidImportFile {
reason: format!("cannot read stdin: {err}"),
}),
);
}
}
} else {
match std::fs::read(&file) {
Ok(bytes) => bytes,
Err(err) => {
return (
None,
Err(CoreError::InvalidImportFile {
reason: format!("cannot read {file}: {err}"),
}),
);
}
}
};
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::projects::project_import(
&api,
&project_name,
zip,
collision_policy.into(),
)
.await
.map(ActionOutput::ProjectImport),
Err(err) => Err(err),
};
(profile, result)
}
ProjectCommand::Diff {
profile_a,
profile_b,
project: project_name,
} => {
let (active, sides) = resolve_two_clients(
&mut config,
cli.profile.as_deref(),
&profile_a,
&profile_b,
);
let result = match sides {
Ok((api_a, api_b)) => actions::projects::project_diff(
&api_a,
&api_b,
&project_name,
&profile_a,
&profile_b,
)
.await
.map(ActionOutput::ProjectDiff),
Err(err) => Err(err),
};
(active, result)
}
ProjectCommand::Sync {
profile_a,
profile_b,
project: project_name,
resource,
all_changed,
delete,
} => {
if resource.is_empty() && !all_changed {
return (
None,
Err(CoreError::InvalidInput {
reason: "sync needs a selection — pass --resource PATH \
(repeatable) and/or --all-changed"
.to_string(),
}),
);
}
if let Err(err) = require_confirmation(
cli.yes,
&format!(
"project sync (overwrite-import the whole project on {profile_b} — \
replaces concurrent Designer edits)"
),
) {
return (None, Err(err));
}
let (active, sides) = resolve_two_clients(
&mut config,
cli.profile.as_deref(),
&profile_a,
&profile_b,
);
let result = match sides {
Ok((api_a, api_b)) => {
let selection = actions::projects::SyncSelection {
resources: resource,
all_changed,
};
actions::projects::project_sync(
&api_a,
&api_b,
&project_name,
&selection,
delete,
&profile_a,
&profile_b,
)
.await
.map(ActionOutput::ProjectSync)
}
Err(err) => Err(err),
};
(active, result)
}
},
Commands::Resource(ResourceArgs { command }) => match command {
ResourceCommand::List { project, prefix } => {
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => {
actions::resources::resources_list(&api, &project, prefix.as_deref())
.await
.map(ActionOutput::ResourcesList)
}
Err(err) => Err(err),
};
(profile, result)
}
ResourceCommand::Get { project, path } => {
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::resources::resource_get(&api, &project, &path)
.await
.map(ActionOutput::ResourceGet),
Err(err) => Err(err),
};
(profile, result)
}
ResourceCommand::Put {
project,
path,
file,
} => {
let input = if file == "-" {
use tokio::io::AsyncReadExt;
let mut buffer = Vec::new();
match tokio::io::stdin().read_to_end(&mut buffer).await {
Ok(_) => buffer,
Err(err) => {
return (
None,
Err(CoreError::InvalidInput {
reason: format!("cannot read stdin: {err}"),
}),
);
}
}
} else {
match std::fs::read(&file) {
Ok(bytes) => bytes,
Err(err) => {
return (
None,
Err(CoreError::InvalidInput {
reason: format!("cannot read {file}: {err}"),
}),
);
}
}
};
if let Err(err) = require_confirmation(
cli.yes,
"resource put (re-imports the project; concurrent Designer edits are replaced)",
) {
return (None, Err(err));
}
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::resources::resource_put(&api, &project, &path, input)
.await
.map(ActionOutput::ResourcePut),
Err(err) => Err(err),
};
(profile, result)
}
ResourceCommand::Delete { project, path } => {
if let Err(err) = require_confirmation(
cli.yes,
"resource delete (re-imports the project; concurrent Designer edits are replaced)",
) {
return (None, Err(err));
}
let (profile, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::resources::resource_delete(&api, &project, &path)
.await
.map(ActionOutput::ResourceDelete),
Err(err) => Err(err),
};
(profile, result)
}
},
Commands::Webdev(WebdevArgs { command }) => {
match resolve_profile_context(&mut config, cli.profile.as_deref()) {
Ok(None) => (None, Err(CoreError::NoActiveProfile)),
Ok(Some((name, profile))) => {
let client = config::resolve_secret(&name, &profile.auth, &secret_chain())
.and_then(|credential| ReqwestGatewayApi::new(&profile, Some(credential)));
match command {
WebdevCommand::Deploy {
project,
with_script_exec,
rotate_secret,
} => {
let result = match client {
Ok(api) => {
if mode == RenderMode::Human {
eprintln!("deploying webdev routes to {project} …");
}
actions::webdev::webdev_deploy(
&api,
&project,
with_script_exec,
rotate_secret,
&path,
&name,
)
.await
.map(ActionOutput::WebdevDeploy)
}
Err(err) => Err(err),
};
(Some(name), result)
}
WebdevCommand::Status { project } => {
let result = match client {
Ok(api) => actions::webdev::webdev_status(
&api,
&project,
profile.webdev_secret.as_deref(),
)
.await
.map(ActionOutput::WebdevStatus),
Err(err) => Err(err),
};
(Some(name), result)
}
}
}
Err(err) => (None, Err(err)),
}
}
Commands::Tags(TagsArgs { command }) => {
if let TagsCommand::Browse {
from_export: Some(export_path),
filter,
include_properties,
..
} = &command
{
let result = actions::tags::browse_rows_from_export(
export_path,
*include_properties,
filter.as_deref(),
)
.map(ActionOutput::TagsBrowseFromExport);
return (None, result);
}
let guard_operation = match &command {
TagsCommand::Provider(TagsProviderCommand::Delete { .. }) => {
Some("tags provider delete")
}
TagsCommand::Config(TagsConfigCommand::Delete { .. }) => Some("tags config delete"),
TagsCommand::Import {
collision_policy: cli::CollisionPolicy::Overwrite,
..
} => Some("tags import --collision-policy overwrite"),
_ => None,
};
if let Some(operation) = guard_operation
&& let Err(err) = require_confirmation(cli.yes, operation)
{
return (None, Err(err));
}
let write_value = match &command {
TagsCommand::Write { value, .. } => match parse_write_scalar(value) {
Ok(parsed) => Some(parsed),
Err(err) => return (None, Err(err)),
},
_ => None,
};
let json_input = match &command {
TagsCommand::Config(TagsConfigCommand::Create { file, .. })
| TagsCommand::Config(TagsConfigCommand::Edit { file, .. })
| TagsCommand::Import { file, .. } => match read_json_input(file).await {
Ok(parsed) => Some(parsed),
Err(err) => return (None, Err(err)),
},
_ => None,
};
let export_out = match &command {
TagsCommand::Export { paths, output, .. } => Some(match output {
Some(path) if path == std::path::Path::new("-") => None,
Some(path) => Some(path.clone()),
None => Some(std::path::PathBuf::from(
actions::tags::default_export_file_name(paths),
)),
}),
_ => None,
};
let time_args = match &command {
TagsCommand::Alarms(TagsAlarmsCommand::History { start, end, .. })
| TagsCommand::History(TagsHistoryCommand::Query { start, end, .. }) => {
match (
actions::tags::parse_time_ms(start),
actions::tags::parse_time_ms(end),
) {
(Ok(start_ms), Ok(end_ms)) => Some((start_ms, end_ms)),
(Err(err), _) | (_, Err(err)) => return (None, Err(err)),
}
}
_ => None,
};
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match (&command, api) {
(TagsCommand::Provider(TagsProviderCommand::List), Ok(api)) => {
actions::tags::tag_provider_list(&api)
.await
.map(ActionOutput::TagProviders)
}
(
TagsCommand::Provider(TagsProviderCommand::Create { name: provider }),
Ok(api),
) => actions::tags::tag_provider_create(&api, provider)
.await
.map(ActionOutput::TagProviderCreate),
(
TagsCommand::Provider(TagsProviderCommand::Delete { name: provider }),
Ok(api),
) => actions::tags::tag_provider_delete(&api, provider)
.await
.map(ActionOutput::TagProviderDelete),
(
TagsCommand::Browse {
path,
filter,
include_properties,
project,
..
},
Ok(api),
) => actions::tags::tags_browse(
&api,
project,
path.as_deref().unwrap_or(""),
filter.as_deref(),
*include_properties,
)
.await
.map(ActionOutput::TagsBrowse),
(TagsCommand::Read { paths, project }, Ok(api)) => {
actions::tags::tags_read(&api, project, paths)
.await
.map(ActionOutput::TagsRead)
}
(TagsCommand::Write { path, project, .. }, Ok(api)) => {
actions::tags::tags_write(&api, project, path, write_value.expect("parsed"))
.await
.map(ActionOutput::TagsWrite)
}
(TagsCommand::Config(TagsConfigCommand::Get { path, project }), Ok(api)) => {
actions::tags::tags_config_get(&api, project, path)
.await
.map(ActionOutput::TagsConfigGet)
}
(TagsCommand::Config(TagsConfigCommand::Create { path, project, .. }), Ok(api)) => {
actions::tags::tags_config_create(
&api,
project,
path,
json_input.as_ref().expect("parsed pre-resolution"),
)
.await
.map(ActionOutput::TagsConfigCreate)
}
(TagsCommand::Config(TagsConfigCommand::Edit { path, project, .. }), Ok(api)) => {
actions::tags::tags_config_edit(
&api,
project,
path,
json_input.as_ref().expect("parsed pre-resolution"),
)
.await
.map(ActionOutput::TagsConfigEdit)
}
(TagsCommand::Config(TagsConfigCommand::Delete { paths, project }), Ok(api)) => {
actions::tags::tags_config_delete(&api, project, paths)
.await
.map(ActionOutput::TagsConfigDelete)
}
(TagsCommand::Udt(TagsUdtCommand::Types { provider, project }), Ok(api)) => {
actions::tags::tags_udt_types(&api, project, provider)
.await
.map(ActionOutput::TagsUdtTypes)
}
(
TagsCommand::Udt(TagsUdtCommand::Def {
name: udt_name,
provider,
project,
}),
Ok(api),
) => actions::tags::tags_udt_def(&api, project, provider, udt_name)
.await
.map(ActionOutput::TagsUdtDef),
(TagsCommand::Export { paths, project, .. }, Ok(api)) => {
actions::tags::tags_export(
&api,
project,
paths,
export_out
.as_ref()
.expect("resolved pre-resolution")
.as_deref(),
)
.await
.map(ActionOutput::TagsExport)
}
(
TagsCommand::Import {
provider,
project,
collision_policy,
..
},
Ok(api),
) => actions::tags::tags_import(
&api,
project,
provider,
json_input.expect("parsed pre-resolution"),
(*collision_policy).into(),
)
.await
.map(ActionOutput::TagsImport),
(
TagsCommand::Alarms(TagsAlarmsCommand::Active {
source,
priority,
state,
project,
}),
Ok(api),
) => actions::tags::tags_alarms_active(
&api,
project,
source.as_deref(),
priority.as_deref(),
state.as_deref(),
)
.await
.map(ActionOutput::TagsAlarmsActive),
(TagsCommand::Alarms(TagsAlarmsCommand::History { project, .. }), Ok(api)) => {
let (start_ms, end_ms) = time_args.expect("parsed pre-resolution");
actions::tags::tags_alarms_history(&api, project, start_ms, end_ms)
.await
.map(ActionOutput::TagsAlarmsHistory)
}
(
TagsCommand::Alarms(TagsAlarmsCommand::Ack {
ids,
note,
username,
project,
}),
Ok(api),
) => actions::tags::tags_alarms_ack(
&api,
project,
ids,
note.as_deref().unwrap_or(""),
username,
)
.await
.map(ActionOutput::TagsAlarmsAck),
(
TagsCommand::History(TagsHistoryCommand::Query {
paths,
return_size,
aggregation,
project,
..
}),
Ok(api),
) => {
let (start_ms, end_ms) = time_args.expect("parsed pre-resolution");
actions::tags::tags_history_query(
&api,
project,
paths,
start_ms,
end_ms,
*return_size,
aggregation.as_deref(),
)
.await
.map(ActionOutput::TagsHistoryQuery)
}
(_, Err(err)) => Err(err),
};
(name, result)
}
Commands::Rig(RigArgs { rig, command }) => {
let guarded_operation = match &command {
RigCommand::Reset { .. } => Some("rig reset"),
RigCommand::Restore { .. } => Some("rig restore"),
RigCommand::Trial(trial_args) => match trial_args.command {
cli::TrialCommand::Reset { .. } => Some("rig trial reset"),
cli::TrialCommand::Status => None,
},
_ => None,
};
if let Some(operation) = guarded_operation
&& let Err(err) = require_confirmation(cli.yes, operation)
{
return (None, Err(err));
}
let runner = ignition_core::rig::DockerCompose;
let selection = match rig {
Some(name) => ignition_core::rig::RigSelection::Named(name),
None => ignition_core::rig::RigSelection::Auto,
};
let gateway_verb_echo = config.active.clone();
let is_gateway_verb = matches!(
command,
RigCommand::Trial(_) | RigCommand::Snapshot { .. } | RigCommand::Restore { .. }
);
let result = match ignition_core::rig::resolve_plan(&runner, selection, &config).await {
Ok(plan) => match command {
RigCommand::Up { .. } | RigCommand::Reset { .. } => {
let probe = commissioned_probe(&plan);
let probe_dyn: Option<&dyn ignition_core::client::GatewayApi> = probe
.as_ref()
.map(|api| api as &dyn ignition_core::client::GatewayApi);
match command {
RigCommand::Up { timeout } => {
actions::rig::rig_up(&runner, &plan, timeout, probe_dyn)
.await
.map(ActionOutput::RigUp)
}
RigCommand::Reset { timeout } => {
actions::rig::rig_reset(&runner, &plan, timeout, probe_dyn)
.await
.map(ActionOutput::RigReset)
}
_ => unreachable!("guarded by the outer match arm"),
}
}
RigCommand::Down => actions::rig::rig_down(&runner, &plan)
.await
.map(ActionOutput::RigDown),
RigCommand::Status => actions::rig::rig_status(&runner, &plan)
.await
.map(ActionOutput::RigStatus),
RigCommand::Logs {
tail,
follow,
service,
} => {
let mut sink = |line: String| println!("{line}");
actions::rig::rig_logs(
&runner,
&plan,
tail,
follow,
service.as_deref(),
&mut sink,
)
.await
.map(ActionOutput::RigLogs)
}
RigCommand::Trial(ref trial_args) => match trial_args.command.clone() {
cli::TrialCommand::Status => match rig_gateway_client(&plan, None) {
Some(api) => actions::rig::trial_status(&api)
.await
.map(ActionOutput::RigTrialStatus),
None => Err(trial_no_gateway(&plan)),
},
cli::TrialCommand::Reset { user } => {
let token = std::env::var("IGNITION_TOKEN")
.ok()
.filter(|value| !value.is_empty());
let username = user.clone().or_else(|| env_non_empty("IGNITION_USER"));
let password = env_non_empty("IGNITION_PASSWORD");
let basic = username.zip(password).map(|(user, password)| {
(user, ignition_core::config::Secret::new(password))
});
if token.is_none() && basic.is_none() {
return (
gateway_verb_echo,
Err(CoreError::SecretUnavailable {
profile: plan.name.clone(),
}),
);
}
let credential =
token.map(|token| Credential::Token(config::Secret::new(token)));
let token_available = credential.is_some();
match rig_gateway_client(&plan, credential) {
Some(api) => {
let rig_url = actions::rig::gateway_url_from(&plan)
.expect("rig_gateway_client derived it or returned None");
let basic_ref = basic
.as_ref()
.map(|(user, password)| (user.as_str(), password));
actions::rig::trial_reset(
&api,
&rig_url,
token_available,
basic_ref,
)
.await
.map(ActionOutput::RigTrialReset)
}
None => Err(trial_no_gateway(&plan)),
}
}
},
RigCommand::Snapshot { output } => {
let Some(token) = env_non_empty("IGNITION_TOKEN") else {
return (
gateway_verb_echo,
Err(CoreError::SecretUnavailable {
profile: plan.name.clone(),
}),
);
};
let credential = Some(Credential::Token(config::Secret::new(token)));
match rig_gateway_client(&plan, credential) {
Some(api) => {
actions::rig::rig_snapshot(&api, &plan.name, output.as_deref())
.await
.map(ActionOutput::RigSnapshot)
}
None => Err(trial_no_gateway(&plan)),
}
}
RigCommand::Restore { file, timeout } => {
let Some(token) = env_non_empty("IGNITION_TOKEN") else {
return (
gateway_verb_echo,
Err(CoreError::SecretUnavailable {
profile: plan.name.clone(),
}),
);
};
let credential = Some(Credential::Token(config::Secret::new(token)));
match rig_gateway_client(&plan, credential) {
Some(api) => {
let rig_url = actions::rig::gateway_url_from(&plan)
.expect("rig_gateway_client derived it or returned None");
actions::rig::rig_restore(&api, &rig_url, &file, timeout)
.await
.map(ActionOutput::RigRestore)
}
None => Err(trial_no_gateway(&plan)),
}
}
},
Err(err) => Err(err),
};
let echo = if is_gateway_verb {
gateway_verb_echo
} else {
None
};
(echo, result)
}
Commands::Backup(BackupArgs { command }) => match command {
BackupCommand::Download { output, r#type } => {
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => {
let stem = name.as_deref().unwrap_or("gateway");
actions::backup::backup_download(
&api,
output.as_deref(),
stem,
r#type.into(),
)
.await
.map(ActionOutput::BackupDownload)
}
Err(err) => Err(err),
};
(name, result)
}
BackupCommand::Restore { file } => {
if let Err(err) = require_confirmation(
cli.yes,
"backup restore (overwrites this gateway's state from the gwbk — \
gateway restarts and blocks ~minutes)",
) {
return (None, Err(err));
}
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::backup::backup_restore(&api, &file)
.await
.map(ActionOutput::BackupRestore),
Err(err) => Err(err),
};
(name, result)
}
},
Commands::Eam(EamArgs { command }) => match command {
EamCommand::History { limit, search } => {
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::eam::eam_history(&api, limit, search.as_deref())
.await
.map(ActionOutput::EamHistory),
Err(err) => Err(err),
};
(name, result)
}
EamCommand::Tasks { name: task_name } => {
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => match task_name {
Some(task_name) => actions::eam::eam_task_detail(&api, &task_name)
.await
.map(ActionOutput::EamTaskDetail),
None => actions::eam::eam_tasks(&api)
.await
.map(ActionOutput::EamTasks),
},
Err(err) => Err(err),
};
(name, result)
}
EamCommand::Task(task_command) => match task_command {
EamTaskCommand::New {
name: task_name,
r#type,
target,
setting,
definition,
schedule_mode,
} => {
use actions::eam::TaskCreateVerdict;
match actions::eam::task_create_guard(&r#type, schedule_mode.wire()) {
TaskCreateVerdict::Refused => {
return (
None,
Err(CoreError::EamTaskTypeRefused {
task_type: r#type.clone(),
}),
);
}
TaskCreateVerdict::NeedsYes => {
let operation = if schedule_mode != ScheduleMode::OnDemand {
format!(
"eam task new (scheduleMode {} arms autonomous \
gateway actions)",
schedule_mode.wire()
)
} else {
format!(
"eam task new ({} mutates the agent \
targets it dispatches to)",
r#type
)
};
if let Err(err) = require_confirmation(cli.yes, &operation) {
return (None, Err(err));
}
}
TaskCreateVerdict::Unguarded => {}
}
let definition_value = match definition.as_deref() {
Some(path) => match read_json_input(path).await {
Ok(value) => Some(value),
Err(err) => return (None, Err(err)),
},
None => None,
};
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::eam::eam_task_create(
&api,
&task_name,
&r#type,
&target,
&setting,
definition_value.as_ref(),
schedule_mode.wire(),
)
.await
.map(ActionOutput::EamTaskCreate),
Err(err) => Err(err),
};
(name, result)
}
EamTaskCommand::Force { name: task_name } => {
if let Err(err) = require_confirmation(
cli.yes,
"eam task force (dispatches the task to agent targets NOW)",
) {
return (None, Err(err));
}
let (name, api) = resolve_gateway_api(&mut config, cli.profile.as_deref());
let result = match api {
Ok(api) => actions::eam::eam_task_force(&api, &task_name)
.await
.map(ActionOutput::EamTaskForce),
Err(err) => Err(err),
};
(name, result)
}
},
},
Commands::Script(ScriptArgs { command }) => match command {
ScriptCommand::Run {
code,
file,
project,
} => {
let script =
match actions::script::read_script_input(code.as_deref(), file.as_deref()) {
Ok(script) => script,
Err(err) => return (None, Err(err)),
};
match resolve_profile_context(&mut config, cli.profile.as_deref()) {
Ok(None) => (None, Err(CoreError::NoActiveProfile)),
Ok(Some((name, profile))) => {
let client = config::resolve_secret(&name, &profile.auth, &secret_chain())
.and_then(|credential| {
ReqwestGatewayApi::new(&profile, Some(credential))
});
let result = match client {
Ok(api) => {
actions::script::script_run(&api, &config, &name, &project, &script)
.await
.map(ActionOutput::ScriptRun)
}
Err(err) => Err(err),
};
(Some(name), result)
}
Err(err) => (None, Err(err)),
}
}
},
Commands::Lint(LintArgs {
paths,
strict,
passthrough,
}) => {
let result = actions::lint::lint_run(&paths, strict, &passthrough)
.await
.map(ActionOutput::Lint);
(None, result)
}
Commands::Profile(ProfileArgs { command }) => match command {
ProfileCmd::List => {
match resolve_profile_context(&mut config, cli.profile.as_deref()) {
Ok(selection) => (
selection.as_ref().map(|(name, _)| name.clone()),
Ok(ActionOutput::ProfileList(actions::profile::list(&config))),
),
Err(err) => (None, Err(err)),
}
}
ProfileCmd::Add {
name,
url,
label,
token_env,
keyring,
user_env,
password_env,
active,
} => {
let prior_active = config.active.clone();
let result = actions::profile::add(
&path,
&name,
&url,
label.as_deref(),
auth_ref_from_flags(token_env, keyring, user_env, password_env),
active,
);
match result {
Ok(result) => {
let envelope_profile = if result.active {
Some(result.name.clone())
} else {
prior_active
};
(envelope_profile, Ok(ActionOutput::ProfileAdd(result)))
}
Err(err) => (None, Err(err)),
}
}
ProfileCmd::Use { name } => match actions::profile::use_profile(&path, &name) {
Ok(result) => (
Some(result.active.clone()),
Ok(ActionOutput::ProfileUse(result)),
),
Err(err) => (None, Err(err)),
},
},
Commands::Completions { .. } => {
unreachable!("completions handled before config load")
}
#[cfg(feature = "tui")]
Commands::Tui => {
if !std::io::stdout().is_terminal() {
return (None, Err(CoreError::tui_tty_refusal()));
}
match ignition_tui::run(cli.profile.clone()).await {
Ok(()) => (None, Ok(ActionOutput::TuiExited)),
Err(err) => (None, Err(err)),
}
}
}
}
fn resolve_profile_context(
config: &mut Config,
flag: Option<&str>,
) -> Result<Option<(String, config::Profile)>, CoreError> {
let overlay_target = flag.map(str::to_string).or_else(|| config.active.clone());
config::apply_env_overlay(config, overlay_target.as_deref());
config::resolve_selection(config, flag)
}
enum Inspection {
Status,
Modules(bool),
Metrics(bool),
}
async fn run_inspection(
config: &mut Config,
flag: Option<&str>,
inspection: Inspection,
) -> (Option<String>, Result<ActionOutput, CoreError>) {
let (name, api) = resolve_gateway_api(config, flag);
let result = match api {
Ok(api) => match inspection {
Inspection::Status => actions::inspect::status(&api)
.await
.map(ActionOutput::Status),
Inspection::Modules(quarantined) => actions::inspect::modules(&api, quarantined)
.await
.map(ActionOutput::Modules),
Inspection::Metrics(history) => actions::inspect::metrics(&api, history)
.await
.map(ActionOutput::Metrics),
},
Err(err) => Err(err),
};
(name, result)
}
fn resolve_gateway_api(
config: &mut Config,
flag: Option<&str>,
) -> (Option<String>, Result<ReqwestGatewayApi, CoreError>) {
match resolve_profile_context(config, flag) {
Ok(None) => (None, Err(CoreError::NoActiveProfile)),
Ok(Some((name, profile))) => {
let credential = config::resolve_secret(&name, &profile.auth, &secret_chain());
let result = credential
.and_then(|credential| ReqwestGatewayApi::new(&profile, Some(credential)));
(Some(name), result)
}
Err(err) => (None, Err(err)),
}
}
fn named_profile_client(config: &mut Config, name: &str) -> Result<ReqwestGatewayApi, CoreError> {
let Some((_resolved, profile)) = config::resolve_selection(config, Some(name))? else {
return Err(CoreError::Internal(
"a named profile selection resolved to nothing".to_string(),
));
};
let credential = config::resolve_secret(name, &profile.auth, &secret_chain())?;
ReqwestGatewayApi::new(&profile, Some(credential))
}
fn resolve_two_clients(
config: &mut Config,
flag: Option<&str>,
name_a: &str,
name_b: &str,
) -> (
Option<String>,
Result<(ReqwestGatewayApi, ReqwestGatewayApi), CoreError>,
) {
match resolve_profile_context(config, flag) {
Ok(None) => (None, Err(CoreError::NoActiveProfile)),
Ok(Some((active, _))) => {
let sides = named_profile_client(config, name_a)
.and_then(|api_a| named_profile_client(config, name_b).map(|api_b| (api_a, api_b)));
(Some(active), sides)
}
Err(err) => (None, Err(err)),
}
}
fn resolve_headerless_api(
config: &mut Config,
flag: Option<&str>,
) -> (Option<String>, Result<ReqwestGatewayApi, CoreError>) {
match resolve_profile_context(config, flag) {
Ok(None) => (None, Err(CoreError::NoActiveProfile)),
Ok(Some((name, profile))) => {
let credential = resolve_secret_opt(&name, &profile.auth);
let result =
credential.and_then(|credential| ReqwestGatewayApi::new(&profile, credential));
(Some(name), result)
}
Err(err) => (None, Err(err)),
}
}
fn commissioned_probe(plan: &ignition_core::rig::RigPlan) -> Option<ReqwestGatewayApi> {
rig_gateway_client(plan, None)
}
fn rig_gateway_client(
plan: &ignition_core::rig::RigPlan,
credential: Option<Credential>,
) -> Option<ReqwestGatewayApi> {
actions::rig::gateway_url_from(plan).and_then(|url| {
let profile = config::Profile {
url: url.parse().ok()?,
label: None,
ssl_verify: false,
auth: AuthRef::default(),
webdev_secret: None,
};
ReqwestGatewayApi::new(&profile, credential).ok()
})
}
fn trial_no_gateway(plan: &ignition_core::rig::RigPlan) -> CoreError {
CoreError::Rig(format!(
"rig {} publishes no gateway port (target 8088/443) — trial \
commands address the rig's gateway",
plan.name
))
}
fn env_non_empty(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|value| !value.is_empty())
}
fn secret_chain() -> Vec<Box<dyn SecretStore>> {
vec![
Box::new(config::EnvStore),
Box::new(config::KeyringStore),
Box::new(config::BasicEnvStore),
]
}
fn resolve_secret_opt(profile: &str, auth: &AuthRef) -> Result<Option<Credential>, CoreError> {
config::resolve_secret(profile, auth, &secret_chain())
.map(Some)
.or_else(|err| match err {
CoreError::SecretUnavailable { .. } => Ok(None),
other => Err(other),
})
}
fn auth_ref_from_flags(
token_env: Option<String>,
keyring: Option<String>,
user_env: Option<String>,
password_env: Option<String>,
) -> config::AuthRef {
if let Some(token_env) = token_env {
config::AuthRef::TokenEnv { token_env }
} else if let Some(keyring) = keyring {
config::AuthRef::Keyring { keyring }
} else if let (Some(user_env), Some(password_env)) = (user_env, password_env) {
config::AuthRef::Basic {
user_env,
password_env,
}
} else {
config::AuthRef::default()
}
}
fn apply_env_defaults(cli: &mut Cli) {
if cli.profile.is_none()
&& let Ok(profile) = std::env::var("IGNITION_PROFILE")
&& !profile.is_empty()
{
cli.profile = Some(profile);
}
if !cli.json && std::env::var("IGNITION_JSON").is_ok_and(|v| v == "1") {
cli.json = true;
}
if !cli.yes && std::env::var("IGNITION_YES").is_ok_and(|v| v == "1") {
cli.yes = true;
}
if let Commands::Rig(rig_args) = &mut cli.command
&& rig_args.rig.is_none()
&& let Ok(rig) = std::env::var("IGNITION_RIG")
&& !rig.is_empty()
{
rig_args.rig = Some(rig);
}
}
fn require_confirmation(yes: bool, operation: &str) -> Result<(), CoreError> {
if yes {
Ok(())
} else {
Err(CoreError::ConfirmationRequired {
operation: operation.to_string(),
})
}
}
fn parse_write_scalar(raw: &str) -> Result<serde_json::Value, CoreError> {
match serde_json::from_str::<serde_json::Value>(raw) {
Ok(value) if !value.is_array() && !value.is_object() => Ok(value),
Ok(_) => Err(CoreError::InvalidInput {
reason: format!(
"--value must be a JSON scalar (number, bool, null, or string) — \
arrays/objects cannot ride the tag write slot: {raw:?}"
),
}),
Err(_) => Ok(serde_json::Value::String(raw.to_string())),
}
}
async fn read_json_input(file: &std::path::Path) -> Result<serde_json::Value, CoreError> {
let label = if file == std::path::Path::new("-") {
"stdin".to_string()
} else {
file.display().to_string()
};
let bytes = if file == std::path::Path::new("-") {
use tokio::io::AsyncReadExt;
let mut buffer = Vec::new();
tokio::io::stdin()
.read_to_end(&mut buffer)
.await
.map_err(|err| CoreError::InvalidInput {
reason: format!("cannot read stdin: {err}"),
})?;
buffer
} else {
std::fs::read(file).map_err(|err| CoreError::InvalidInput {
reason: format!("cannot read {label}: {err}"),
})?
};
serde_json::from_slice(&bytes).map_err(|err| CoreError::InvalidInput {
reason: format!("{label} is not valid JSON: {err}"),
})
}
fn init_tracing(verbosity: u8) {
use tracing_subscriber::EnvFilter;
let filter = match std::env::var("IGNITION_LOG") {
Ok(spec) if !spec.is_empty() => EnvFilter::new(spec),
_ => EnvFilter::new(match verbosity {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
}),
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
}
#[cfg(test)]
mod tests {
use super::require_confirmation;
#[test]
fn confirmation_guard_refuses_without_yes() {
let err = require_confirmation(false, "project delete").expect_err("refuses without --yes");
assert_eq!(err.exit_code(), 2, "usage class — it names a missing flag");
assert_eq!(err.code(), "confirmation_required");
let hint = err.hint().expect("hint required");
assert!(
hint.contains("--yes") && hint.contains("IGNITION_YES"),
"hint names the flag and the env escape hatch: {hint}"
);
assert!(
err.to_string().contains("project delete"),
"message names the operation: {err}"
);
require_confirmation(true, "project delete").expect("--yes confirms");
}
}