list resource apis

This commit is contained in:
mbecker20
2025-08-10 00:13:53 -07:00
parent 1fd3836a4a
commit 108c9215d1
12 changed files with 677 additions and 32 deletions

1
Cargo.lock generated
View File

@@ -2621,6 +2621,7 @@ name = "komodo_cli"
version = "1.19.0-dev-2"
dependencies = [
"anyhow",
"chrono",
"clap",
"colored",
"comfy-table",

View File

@@ -29,6 +29,7 @@ tracing.workspace = true
colored.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
chrono.workspace = true
tokio.workspace = true
serde.workspace = true
clap.workspace = true

View File

@@ -4,21 +4,41 @@ use comfy_table::{Attribute, Cell, Color};
use futures_util::{FutureExt, try_join};
use komodo_client::{
KomodoClient,
api::read::{ListServers, ListStacks, ListTags},
api::read::{
ListActions, ListAlerters, ListBuilders, ListBuilds,
ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,
ListServers, ListStacks, ListTags,
},
entities::{
action::{ActionListItem, ActionListItemInfo, ActionState},
alerter::{AlerterListItem, AlerterListItemInfo},
build::{BuildListItem, BuildListItemInfo, BuildState},
builder::{BuilderListItem, BuilderListItemInfo},
config::cli::args::{
self,
list::{ListCommand, ResourceFilters},
},
deployment::{
DeploymentListItem, DeploymentListItemInfo, DeploymentState,
},
procedure::{
ProcedureListItem, ProcedureListItemInfo, ProcedureState,
},
repo::{RepoListItem, RepoListItemInfo, RepoState},
resource::{ResourceListItem, ResourceQuery},
server::{ServerListItem, ServerListItemInfo, ServerState},
stack::{StackListItem, StackListItemInfo, StackState},
sync::{
ResourceSyncListItem, ResourceSyncListItemInfo,
ResourceSyncState,
},
},
};
use serde::Serialize;
use crate::command::{
PrintTable, matches_wildcards, parse_wildcards, print_items,
PrintTable, format_timetamp, matches_wildcards, parse_wildcards,
print_items,
};
pub async fn handle(list: &args::list::List) -> anyhow::Result<()> {
@@ -30,20 +50,60 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> {
Some(ListCommand::Stacks(filters)) => {
list_resources::<StackListItem>(filters).await
}
Some(ListCommand::Deployments(filters)) => {
list_resources::<DeploymentListItem>(filters).await
}
Some(ListCommand::Builds(filters)) => {
list_resources::<BuildListItem>(filters).await
}
Some(ListCommand::Repos(filters)) => {
list_resources::<RepoListItem>(filters).await
}
Some(ListCommand::Procedures(filters)) => {
list_resources::<ProcedureListItem>(filters).await
}
Some(ListCommand::Actions(filters)) => {
list_resources::<ActionListItem>(filters).await
}
Some(ListCommand::Syncs(filters)) => {
list_resources::<ResourceSyncListItem>(filters).await
}
Some(ListCommand::Builders(filters)) => {
list_resources::<BuilderListItem>(filters).await
}
Some(ListCommand::Alerters(filters)) => {
list_resources::<AlerterListItem>(filters).await
}
}
}
/// Includes all resources besides builds and alerters.
async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
let filters: ResourceFilters = list.clone().into();
let client = super::komodo_client().await?;
let (tags, mut servers, mut stacks) = try_join!(
let (
tags,
mut servers,
mut stacks,
mut deployments,
mut builds,
mut repos,
mut procedures,
mut actions,
mut syncs,
) = try_join!(
client.read(ListTags::default()).map(|res| res.map(|res| res
.into_iter()
.map(|t| (t.id, t.name))
.collect::<HashMap<_, _>>())),
ServerListItem::list(client, &filters),
StackListItem::list(client, &filters)
StackListItem::list(client, &filters),
DeploymentListItem::list(client, &filters),
BuildListItem::list(client, &filters),
RepoListItem::list(client, &filters),
ProcedureListItem::list(client, &filters),
ActionListItem::list(client, &filters),
ResourceSyncListItem::list(client, &filters),
)?;
if !servers.is_empty() {
@@ -58,6 +118,42 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
println!();
}
if !deployments.is_empty() {
fix_tags(&mut deployments, &tags);
print_items(deployments, filters.format)?;
println!();
}
if !builds.is_empty() {
fix_tags(&mut builds, &tags);
print_items(builds, filters.format)?;
println!();
}
if !repos.is_empty() {
fix_tags(&mut repos, &tags);
print_items(repos, filters.format)?;
println!();
}
if !procedures.is_empty() {
fix_tags(&mut procedures, &tags);
print_items(procedures, filters.format)?;
println!();
}
if !actions.is_empty() {
fix_tags(&mut actions, &tags);
print_items(actions, filters.format)?;
println!();
}
if !syncs.is_empty() {
fix_tags(&mut syncs, &tags);
print_items(syncs, filters.format)?;
println!();
}
Ok(())
}
@@ -202,16 +298,297 @@ impl ListResources for StackListItem {
})
.collect::<Vec<_>>();
stacks.sort_by(|a, b| {
a.info.state.cmp(&b.info.state).then(
a.name
.cmp(&b.name)
.then(a.info.server_id.cmp(&b.info.server_id)),
)
a.info
.state
.cmp(&b.info.state)
.then(a.name.cmp(&b.name))
.then(a.info.server_id.cmp(&b.info.server_id))
});
Ok(stacks)
}
}
impl ListResources for DeploymentListItem {
type Info = DeploymentListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
) -> anyhow::Result<Vec<Self>> {
let (servers, mut deployments) = tokio::try_join!(
client
.read(ListServers {
query: ResourceQuery::builder().build(),
})
.map(|res| res.map(|res| res
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListDeployments {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.build(),
})
)?;
deployments.iter_mut().for_each(|deployment| {
if deployment.info.server_id.is_empty() {
return;
}
let Some(server) = servers.get(&deployment.info.server_id)
else {
return;
};
deployment.info.server_id.clone_from(&server.name);
});
let names = parse_wildcards(&filters.names);
let servers = parse_wildcards(&filters.servers);
let mut deployments = deployments
.into_iter()
.filter(|deployment| {
let state_check = if filters.all {
true
} else if filters.down {
!matches!(deployment.info.state, DeploymentState::Running)
} else {
matches!(deployment.info.state, DeploymentState::Running)
};
state_check
&& matches_wildcards(&names, &[deployment.name.as_str()])
&& matches_wildcards(
&servers,
&[deployment.info.server_id.as_str()],
)
})
.collect::<Vec<_>>();
deployments.sort_by(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then(a.name.cmp(&b.name))
.then(a.info.server_id.cmp(&b.info.server_id))
});
Ok(deployments)
}
}
impl ListResources for BuildListItem {
type Info = BuildListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
) -> anyhow::Result<Vec<Self>> {
let (builders, mut builds) = tokio::try_join!(
client
.read(ListBuilders {
query: ResourceQuery::builder().build(),
})
.map(|res| res.map(|res| res
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListBuilds {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.build(),
})
)?;
builds.iter_mut().for_each(|build| {
if build.info.builder_id.is_empty() {
return;
}
let Some(builder) = builders.get(&build.info.builder_id) else {
return;
};
build.info.builder_id.clone_from(&builder.name);
});
let names = parse_wildcards(&filters.names);
let builders = parse_wildcards(&filters.builders);
let mut builds = builds
.into_iter()
.filter(|build| {
matches_wildcards(&names, &[build.name.as_str()])
&& matches_wildcards(
&builders,
&[build.info.builder_id.as_str()],
)
})
.collect::<Vec<_>>();
builds.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then(a.info.builder_id.cmp(&b.info.builder_id))
.then(a.info.state.cmp(&b.info.state))
});
Ok(builds)
}
}
impl ListResources for RepoListItem {
type Info = RepoListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
let mut repos = client
.read(ListRepos {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.build(),
})
.await?
.into_iter()
.filter(|repo| matches_wildcards(&names, &[repo.name.as_str()]))
.collect::<Vec<_>>();
repos.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then(a.info.server_id.cmp(&b.info.server_id))
.then(a.info.builder_id.cmp(&b.info.builder_id))
});
Ok(repos)
}
}
impl ListResources for ProcedureListItem {
type Info = ProcedureListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
let mut procedures = client
.read(ListProcedures {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.build(),
})
.await?
.into_iter()
.filter(|procedure| {
matches_wildcards(&names, &[procedure.name.as_str()])
})
.collect::<Vec<_>>();
procedures.sort_by(|a, b| {
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
});
Ok(procedures)
}
}
impl ListResources for ActionListItem {
type Info = ActionListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
let mut actions = client
.read(ListActions {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.build(),
})
.await?
.into_iter()
.filter(|action| {
matches_wildcards(&names, &[action.name.as_str()])
})
.collect::<Vec<_>>();
actions.sort_by(|a, b| {
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
});
Ok(actions)
}
}
impl ListResources for ResourceSyncListItem {
type Info = ResourceSyncListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
let mut syncs = client
.read(ListResourceSyncs {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.build(),
})
.await?
.into_iter()
.filter(|sync| matches_wildcards(&names, &[sync.name.as_str()]))
.collect::<Vec<_>>();
syncs.sort_by(|a, b| {
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
});
Ok(syncs)
}
}
impl ListResources for BuilderListItem {
type Info = BuilderListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
let mut builders = client
.read(ListBuilders {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.build(),
})
.await?
.into_iter()
.filter(|builder| {
matches_wildcards(&names, &[builder.name.as_str()])
})
.collect::<Vec<_>>();
builders.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then(a.info.builder_type.cmp(&b.info.builder_type))
});
Ok(builders)
}
}
impl ListResources for AlerterListItem {
type Info = AlerterListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
let mut syncs = client
.read(ListAlerters {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.build(),
})
.await?
.into_iter()
.filter(|sync| matches_wildcards(&names, &[sync.name.as_str()]))
.collect::<Vec<_>>();
syncs.sort_by(|a, b| {
a.info
.enabled
.cmp(&b.info.enabled)
.then(a.name.cmp(&b.name))
.then(a.info.endpoint_type.cmp(&b.info.endpoint_type))
});
Ok(syncs)
}
}
// TABLE
impl PrintTable for ResourceListItem<ServerListItemInfo> {
@@ -257,3 +634,188 @@ impl PrintTable for ResourceListItem<StackListItemInfo> {
]
}
}
impl PrintTable for ResourceListItem<DeploymentListItemInfo> {
fn header() -> &'static [&'static str] {
&["Deployment", "State", "Server", "Tags"]
}
fn row(self) -> Vec<comfy_table::Cell> {
let color = match self.info.state {
DeploymentState::NotDeployed => Color::Blue,
DeploymentState::Running => Color::Green,
DeploymentState::Paused => Color::DarkYellow,
DeploymentState::Unknown => Color::Magenta,
_ => Color::Red,
};
vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.state.to_string())
.fg(color)
.add_attribute(Attribute::Bold),
Cell::new(self.info.server_id),
Cell::new(self.tags.join(", ")),
]
}
}
impl PrintTable for ResourceListItem<BuildListItemInfo> {
fn header() -> &'static [&'static str] {
&["Build", "State", "Builder", "Tags"]
}
fn row(self) -> Vec<comfy_table::Cell> {
let color = match self.info.state {
BuildState::Ok => Color::Green,
BuildState::Building => Color::DarkYellow,
BuildState::Unknown => Color::Magenta,
BuildState::Failed => Color::Red,
};
vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.state.to_string())
.fg(color)
.add_attribute(Attribute::Bold),
Cell::new(self.info.builder_id),
Cell::new(self.tags.join(", ")),
]
}
}
impl PrintTable for ResourceListItem<RepoListItemInfo> {
fn header() -> &'static [&'static str] {
&["Repo", "State", "Link", "Tags"]
}
fn row(self) -> Vec<comfy_table::Cell> {
let color = match self.info.state {
RepoState::Ok => Color::Green,
RepoState::Building
| RepoState::Cloning
| RepoState::Pulling => Color::DarkYellow,
RepoState::Unknown => Color::Magenta,
RepoState::Failed => Color::Red,
};
vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.state.to_string())
.fg(color)
.add_attribute(Attribute::Bold),
Cell::new(self.info.repo_link),
Cell::new(self.tags.join(", ")),
]
}
}
impl PrintTable for ResourceListItem<ProcedureListItemInfo> {
fn header() -> &'static [&'static str] {
&["Procedure", "State", "Schedule", "Tags"]
}
fn row(self) -> Vec<comfy_table::Cell> {
let color = match self.info.state {
ProcedureState::Ok => Color::Green,
ProcedureState::Running => Color::DarkYellow,
ProcedureState::Unknown => Color::Magenta,
ProcedureState::Failed => Color::Red,
};
let next_run = if let Some(ts) = self.info.next_scheduled_run {
Cell::new(
format_timetamp(ts)
.unwrap_or(String::from("Invalid next ts")),
)
.add_attribute(Attribute::Bold)
} else {
Cell::new(String::from("None"))
};
vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.state.to_string())
.fg(color)
.add_attribute(Attribute::Bold),
next_run,
Cell::new(self.tags.join(", ")),
]
}
}
impl PrintTable for ResourceListItem<ActionListItemInfo> {
fn header() -> &'static [&'static str] {
&["Procedure", "State", "Schedule", "Tags"]
}
fn row(self) -> Vec<comfy_table::Cell> {
let color = match self.info.state {
ActionState::Ok => Color::Green,
ActionState::Running => Color::DarkYellow,
ActionState::Unknown => Color::Magenta,
ActionState::Failed => Color::Red,
};
let next_run = if let Some(ts) = self.info.next_scheduled_run {
Cell::new(
format_timetamp(ts)
.unwrap_or(String::from("Invalid next ts")),
)
.add_attribute(Attribute::Bold)
} else {
Cell::new(String::from("None"))
};
vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.state.to_string())
.fg(color)
.add_attribute(Attribute::Bold),
next_run,
Cell::new(self.tags.join(", ")),
]
}
}
impl PrintTable for ResourceListItem<ResourceSyncListItemInfo> {
fn header() -> &'static [&'static str] {
&["Sync", "State", "Tags"]
}
fn row(self) -> Vec<comfy_table::Cell> {
let color = match self.info.state {
ResourceSyncState::Ok => Color::Green,
ResourceSyncState::Pending | ResourceSyncState::Syncing => {
Color::DarkYellow
}
ResourceSyncState::Unknown => Color::Magenta,
ResourceSyncState::Failed => Color::Red,
};
vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.state.to_string())
.fg(color)
.add_attribute(Attribute::Bold),
Cell::new(self.tags.join(", ")),
]
}
}
impl PrintTable for ResourceListItem<BuilderListItemInfo> {
fn header() -> &'static [&'static str] {
&["Builder", "Type", "Tags"]
}
fn row(self) -> Vec<comfy_table::Cell> {
vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.builder_type),
Cell::new(self.tags.join(", ")),
]
}
}
impl PrintTable for ResourceListItem<AlerterListItemInfo> {
fn header() -> &'static [&'static str] {
&["Alerter", "Type", "Enabled", "Tags"]
}
fn row(self) -> Vec<comfy_table::Cell> {
vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.endpoint_type),
if self.info.enabled {
Cell::new(self.info.enabled.to_string()).fg(Color::Green)
} else {
Cell::new(self.info.enabled.to_string()).fg(Color::Red)
},
Cell::new(self.tags.join(", ")),
]
}
}

View File

@@ -1,6 +1,7 @@
use std::io::Read;
use anyhow::{Context, anyhow};
use chrono::TimeZone;
use colored::Colorize;
use comfy_table::{Cell, Table};
use komodo_client::{
@@ -138,3 +139,13 @@ fn matches_wildcards(
wildcards.iter().any(|wc| wc.is_match(item.as_bytes()))
})
}
fn format_timetamp(ts: i64) -> anyhow::Result<String> {
let ts = chrono::Local
.timestamp_millis_opt(ts)
.single()
.context("Invalid ts")?
.format("%Y%m%d-%H%M%S")
.to_string();
Ok(ts)
}

View File

@@ -38,7 +38,17 @@ pub struct ActionListItemInfo {
#[typeshare]
#[derive(
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Display,
Serialize,
Deserialize,
)]
pub enum ActionState {
/// Unknown case

View File

@@ -100,14 +100,18 @@ impl Default for AlerterConfig {
Debug, Clone, PartialEq, Serialize, Deserialize, EnumVariants,
)]
#[variant_derive(
Serialize,
Deserialize,
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Display,
EnumString,
AsRefStr
AsRefStr,
Serialize,
Deserialize
)]
#[serde(tag = "type", content = "params")]
pub enum AlerterEndpoint {

View File

@@ -68,15 +68,25 @@ pub struct BuildListItemInfo {
#[typeshare]
#[derive(
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
)]
pub enum BuildState {
/// Currently building
Building,
/// Last build successful (or never built)
Ok,
/// Last build failed
Failed,
/// Currently building
Building,
/// Other case
#[default]
Unknown,

View File

@@ -57,6 +57,30 @@ pub enum ListCommand {
/// List Stacks (aliases: `stack`, `stk`)
#[clap(alias = "stack", alias = "stk")]
Stacks(ResourceFilters),
/// List Deployments (aliases: `deployment`, `dep`)
#[clap(alias = "deployment", alias = "dep")]
Deployments(ResourceFilters),
/// List Builds (aliases: `build`, `bld`)
#[clap(alias = "build", alias = "bld")]
Builds(ResourceFilters),
/// List Repos (alias: `repo`)
#[clap(alias = "repo")]
Repos(ResourceFilters),
/// List Procedures (aliases: `procedure`, `proc`)
#[clap(alias = "procedure", alias = "proc")]
Procedures(ResourceFilters),
/// List Actions (aliases: `action`, `act`)
#[clap(alias = "action", alias = "act")]
Actions(ResourceFilters),
/// List Syncs (alias: `sync`)
#[clap(alias = "sync")]
Syncs(ResourceFilters),
/// List Builders (aliases: `builder`, `bldr`)
#[clap(alias = "builder", alias = "bldr")]
Builders(ResourceFilters),
/// List Alerters (aliases: `alerter`, `alrt`)
#[clap(alias = "alerter", alias = "alrt")]
Alerters(ResourceFilters),
}
#[derive(Debug, Clone, clap::Parser)]

View File

@@ -329,17 +329,19 @@ pub fn conversions_from_str(
/// - Running -> running.
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
PartialEq,
Hash,
Eq,
Clone,
Copy,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Default,
Display,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]

View File

@@ -35,15 +35,25 @@ pub struct ProcedureListItemInfo {
#[typeshare]
#[derive(
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
)]
pub enum ProcedureState {
/// Currently running
Running,
/// Last run successful
Ok,
/// Last run failed
Failed,
/// Currently running
Running,
/// Other case (never run)
#[default]
Unknown,

View File

@@ -131,10 +131,10 @@ pub struct StackServiceWithUpdate {
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum StackState {
/// All containers are running.
Running,
/// The stack is currently re/deploying
Deploying,
/// All containers are running.
Running,
/// All containers are paused
Paused,
/// All contianers are stopped

View File

@@ -53,17 +53,27 @@ pub struct ResourceSyncListItemInfo {
#[typeshare]
#[derive(
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
)]
pub enum ResourceSyncState {
/// Last sync successful (or never synced). No Changes pending
Ok,
/// Last sync failed
Failed,
/// Currently syncing
Syncing,
/// Updates pending
Pending,
/// Last sync successful (or never synced). No Changes pending
Ok,
/// Last sync failed
Failed,
/// Other case
#[default]
Unknown,