diff --git a/src/cli.rs b/src/cli.rs index 4b08e27..4ca3808 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -29,6 +29,7 @@ pub async fn execute_cmd(opts: MainOpts) -> Result<(), CmdError> { SubCommand::Dataset(input) => input.exec(ctx).await?, SubCommand::Job(input) => input.exec(ctx).await?, + SubCommand::Launcher(input) => input.exec(ctx).await?, SubCommand::Logout(input) => input.exec(&ctx).await?, }; Ok(()) diff --git a/src/cli/cmd.rs b/src/cli/cmd.rs index 8a4468e..c4ecf09 100644 --- a/src/cli/cmd.rs +++ b/src/cli/cmd.rs @@ -1,5 +1,6 @@ pub mod dataset; pub mod job; +pub mod launcher; pub mod login; pub mod logout; pub mod project; @@ -85,6 +86,8 @@ pub enum CmdError { #[snafu(display("Job - {}", source))] Job { source: job::Error }, + #[snafu(display("Launcher - {}", source))] + Launcher { source: launcher::Error }, #[snafu(display("Logout - {}", source))] Logout { source: logout::Error }, @@ -95,6 +98,11 @@ impl From for CmdError { CmdError::Job { source } } } +impl From for CmdError { + fn from(source: launcher::Error) -> Self { + CmdError::Launcher { source } + } +} impl From for CmdError { fn from(source: version::Error) -> Self { diff --git a/src/cli/cmd/job/stop.rs b/src/cli/cmd/job/stop.rs index 89a5214..8402297 100644 --- a/src/cli/cmd/job/stop.rs +++ b/src/cli/cmd/job/stop.rs @@ -13,7 +13,7 @@ use snafu::{ResultExt, Snafu}; /// Stop a running non-interactive session. #[derive(Parser, Debug)] pub struct Input { - /// The launcher to use for launching the job. + /// The id of the job to stop #[arg(value_hint=ValueHint::Other, add = ArgValueCompleter::new(complete_job_name))] pub job_id: String, } diff --git a/src/cli/cmd/launcher.rs b/src/cli/cmd/launcher.rs new file mode 100644 index 0000000..41e9572 --- /dev/null +++ b/src/cli/cmd/launcher.rs @@ -0,0 +1,32 @@ +pub mod list; + +use super::Context; +use clap::Parser; +use snafu::{ResultExt, Snafu}; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("Error listing launchers: {}", source))] + List { source: list::Error }, +} + +/// Sub command for managing launchers +#[derive(Parser, Debug)] +pub struct Input { + #[command(subcommand)] + pub subcmd: LauncherCommand, +} + +impl Input { + pub async fn exec(&self, ctx: Context) -> Result<(), Error> { + match &self.subcmd { + LauncherCommand::List(input) => input.exec(ctx).await.context(ListSnafu), + } + } +} + +#[derive(Parser, Debug)] +pub enum LauncherCommand { + #[command()] + List(list::Input), +} diff --git a/src/cli/cmd/launcher/list.rs b/src/cli/cmd/launcher/list.rs new file mode 100644 index 0000000..1f108f2 --- /dev/null +++ b/src/cli/cmd/launcher/list.rs @@ -0,0 +1,36 @@ +use super::Context; +use crate::{ + cli::sink::Error as SinkError, + httpclient::{self, data::SessionMode}, +}; + +use clap::Parser; + +use snafu::{ResultExt, Snafu}; + +/// Listing launchers. +/// +/// List currently running launchers. +#[derive(Parser, Debug)] +pub struct Input {} + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("Error writing data: {}", source))] + WriteResult { source: SinkError }, + + #[snafu(display("Http error: {}", source))] + HttpClient { source: httpclient::Error }, +} + +impl Input { + pub async fn exec(&self, ctx: Context) -> Result<(), Error> { + let mut result = ctx.client.list_launchers().await.context(HttpClientSnafu)?; + + result.retain(|e| e.launcher_type == SessionMode::NonInteractive); + if let Ok(Some(project)) = ctx.resolve_project_context().await { + result.retain(|v| v.project_id == project.id); + } + ctx.write_result(&result).await.context(WriteResultSnafu) + } +} diff --git a/src/cli/cmd/project.rs b/src/cli/cmd/project.rs index 74a82e9..5980361 100644 --- a/src/cli/cmd/project.rs +++ b/src/cli/cmd/project.rs @@ -1,4 +1,5 @@ pub mod clone; +pub mod list; use super::Context; use clap::Parser; @@ -8,6 +9,8 @@ use snafu::{ResultExt, Snafu}; pub enum Error { #[snafu(display("Error cloning project: {}", source))] Clone { source: clone::Error }, + #[snafu(display("Error listing projects: {}", source))] + List { source: list::Error }, } /// Sub command for managing projects @@ -21,6 +24,7 @@ impl Input { pub async fn exec(&self, ctx: Context) -> Result<(), Error> { match &self.subcmd { ProjectCommand::Clone(input) => input.exec(ctx).await.context(CloneSnafu), + ProjectCommand::List(input) => input.exec(ctx).await.context(ListSnafu), } } } @@ -29,4 +33,6 @@ impl Input { pub enum ProjectCommand { #[command()] Clone(clone::Input), + #[command()] + List(list::Input), } diff --git a/src/cli/cmd/project/list.rs b/src/cli/cmd/project/list.rs new file mode 100644 index 0000000..f52d183 --- /dev/null +++ b/src/cli/cmd/project/list.rs @@ -0,0 +1,40 @@ +use super::Context; +use crate::{ + cli::sink::Error as SinkError, + httpclient::{self}, +}; + +use clap::Parser; + +use snafu::{ResultExt, Snafu}; + +/// Listing projects. +/// +/// List all projects owned by a user +#[derive(Parser, Debug)] +pub struct Input { + /// Get all projects, not just ones you're a member of + #[arg(long, short, default_value_t = false)] + pub all: bool, +} + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("Error writing data: {}", source))] + WriteResult { source: SinkError }, + + #[snafu(display("Http error: {}", source))] + HttpClient { source: httpclient::Error }, +} + +impl Input { + pub async fn exec(&self, ctx: Context) -> Result<(), Error> { + let result = ctx + .client + .list_projects(!self.all) + .await + .context(HttpClientSnafu)?; + + ctx.write_result(&result).await.context(WriteResultSnafu) + } +} diff --git a/src/cli/complete.rs b/src/cli/complete.rs index 08e32a6..b659c2a 100644 --- a/src/cli/complete.rs +++ b/src/cli/complete.rs @@ -141,6 +141,7 @@ pub fn complete_job_launcher_id(current: &ffi::OsStr) -> Vec None, }; for launcher in launchers + .0 .iter() .filter(|e| e.launcher_type == SessionMode::NonInteractive) .filter(|e| match &project_id { diff --git a/src/cli/opts.rs b/src/cli/opts.rs index 106e1bd..86b7eee 100644 --- a/src/cli/opts.rs +++ b/src/cli/opts.rs @@ -164,6 +164,8 @@ pub enum SubCommand { #[command()] Job(job::Input), + #[command()] + Launcher(launcher::Input), #[command()] Logout(logout::Input), diff --git a/src/cli/sink.rs b/src/cli/sink.rs index fcb94e1..3966f7a 100644 --- a/src/cli/sink.rs +++ b/src/cli/sink.rs @@ -56,6 +56,7 @@ impl From for Error { } impl Sink for ProjectDetails {} +impl Sink for ProjectList {} impl Sink for SimpleMessage {} impl Sink for BuildInfo {} impl Sink for PathEntry {} @@ -63,5 +64,6 @@ impl Sink for UserCode {} impl Sink for Response {} impl Sink for SessionStartResponse {} impl Sink for SessionList {} +impl Sink for LauncherList {} impl Sink for SessionLogs {} impl Sink for VersionInfo {} diff --git a/src/httpclient.rs b/src/httpclient.rs index 7529c9d..5941686 100644 --- a/src/httpclient.rs +++ b/src/httpclient.rs @@ -362,6 +362,16 @@ impl Client { }) } } + pub async fn list_projects(&self, direct_member: bool) -> Result { + let mut url = self.make_url("/api/data/projects")?; + url.query_pairs_mut() + .append_pair("direct_member", &direct_member.to_string()); + let req = self.set_bearer_token(self.client.get(url.clone())).await?; + + self.run_request::>(req, url) + .await + .map(ProjectList) + } pub async fn get_namespace( &self, @@ -455,10 +465,10 @@ impl Client { Ok(r) } - pub async fn list_launchers(&self) -> Result, Error> { + pub async fn list_launchers(&self) -> Result { let path = "/api/data/session_launchers"; let result = self.json_get::>(path).await?; - Ok(result) + Ok(LauncherList(result)) } pub async fn get_launcher(&self, id: &str) -> Result, Error> { diff --git a/src/httpclient/data.rs b/src/httpclient/data.rs index 4ead0df..d689b31 100644 --- a/src/httpclient/data.rs +++ b/src/httpclient/data.rs @@ -81,6 +81,52 @@ impl fmt::Display for SessionStartRequest { ) } } +#[derive(Debug, Serialize, Deserialize)] +pub struct LauncherList(pub Vec); +impl LauncherList { + pub fn filter(self, f: F) -> LauncherList + where + F: Fn(&SessionLauncher) -> bool, + { + let l: Vec = self.0.into_iter().filter(|v| f(v)).collect(); + LauncherList(l) + } + + pub fn retain(&mut self, f: F) + where + F: Fn(&SessionLauncher) -> bool, + { + self.0.retain(|v| f(v)); + } +} +fn create_launcher_table<'a, I>(data: I) -> Table +where + I: IntoIterator, +{ + let mut builder = Builder::default(); + for r in data { + let data = vec![&r.name, r.id.as_str(), &r.project_id]; + builder.push_record(data); + } + builder.insert_record(0, vec!["Launcher", "Id", "Project Id"]); + + let mut table = builder.build(); + let settings = Settings::default().with(Style::sharp()); + + table.with(settings); + table +} + +impl fmt::Display for LauncherList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.is_empty() { + write!(f, "No launchers found.") + } else { + let table = create_launcher_table(&self.0); + write!(f, "{}", table) + } + } +} #[derive(Debug, Serialize, Deserialize)] pub struct SessionList(pub Vec); @@ -290,6 +336,54 @@ impl fmt::Display for ProjectDetails { } } +#[derive(Debug, Serialize, Deserialize)] +pub struct ProjectList(pub Vec); +impl ProjectList { + pub fn filter(self, f: F) -> ProjectList + where + F: Fn(&ProjectDetails) -> bool, + { + let l: Vec = self.0.into_iter().filter(|v| f(v)).collect(); + ProjectList(l) + } + + pub fn retain(&mut self, f: F) + where + F: Fn(&ProjectDetails) -> bool, + { + self.0.retain(|v| f(v)); + } +} + +fn create_project_table<'a, I>(data: I) -> Table +where + I: IntoIterator, +{ + let mut builder = Builder::default(); + for r in data { + let data = vec![&r.name, r.id.as_str(), &r.namespace, &r.slug]; + builder.push_record(data); + } + builder.insert_record(0, vec!["Project", "Id", "Namespace", "Slug"]); + + let mut table = builder.build(); + let settings = Settings::default().with(Style::sharp()); + + table.with(settings); + table +} + +impl fmt::Display for ProjectList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.is_empty() { + write!(f, "No projects found.") + } else { + let table = create_project_table(&self.0); + write!(f, "{}", table) + } + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct RenkuError { pub code: i32,