Files
komodo/bin/cli/src/command/list.rs
Maxwell Becker 2fa9d9ecce 1.19.0 (#722)
* start 1.18.5

* prevent empty additional permission check (ie for new resources)

* dev-2

* bump rust to 1.88

* tweaks

* repo based stack commit happens from core repo cache rather than on server to simplify

* clippy auto fix

* clippy lints periphery

* clippy fix komodo_client

* dev-3

* emphasize ferret version pinning

* bump svi with PR fix

* dev-4

* webhook disabled early return

* Fix missing alert types for whitelist

* add "ScheduleRun"

* fix status cache not cleaning on resource delete

* dev-5

* forgot to pipe through poll in previous refactor

* refetch given in ms

* fix configure build extra args

* reorder resource sync config

* Implement ability to run actions at startup (#664)

* Implement ability to run actions at startup

* run post-startup actions after server is listening

* startup use action query

* fmt

* Fix Google Login enabled message (#668)

- it was showing "Github Login" instead of "Google Login"

* Allow CIDR ranges in Allowed IPs (#666)

* Allow CIDR ranges in Allowed IPs

* Catch mixed IPv4/IPv6 mappings that are probably intended to match

* forgiving vec

* dev-6

* forgiving vec log. allowed ips docs

* server stats UI: move current disk breakdown above charts

* searchable container stats, toggle collaple container / disk sections

* Add Clear repo cache method

* fix execute usage docs

* Komodo managed env-file should take precedence in all cases (ie come last in env file list)

* tag include unused flag for future use

* combine users page search

* util backup / restore

* refactor backup/restore duplication

* cleanup restore

* core image include util binary

* dev-7

* back to LinesCodec

* dev-8

* clean up

* clean up logs

* rename to komodo-util

* dev-9

* enable_fance_toml

* dev-10 enable fancy toml

* add user agent to oidc requests (#701)

Co-authored-by: eleith <online-github@eleith.com>

* fmt

* use database library

* clippy lint

* consolidate and standardize cli

* dev-11

* dev-12 implement backup using cli

* dev-13 logs

* command variant fields need to be #[arg]

* tweak cli

* gen client

* fix terminal reconnect issue

* rename cli to `km`

* tweaks for the cli logs

* wait for enter on --yes empty println

* fix --yes

* dev-15

* bump deps

* update croner to latest, use static parser

* dev-16

* cli execute polls updates until complete before logging

* remove repo cache mount

* cli nice

* /backup -> /backups

* dev-17 config loading preserves CONFIG_PATHS precedence

* update dockerfile default docker cli config keywords

* dev-18

* support .kmignore

* add ignores log

* Implement automatic backup pruning, default 14 backups before prune

* db copy / restore uses idempotent upsert

* cli update variable - "km set var VAR value"

* improve cli initial logs

* time the executions

* implement update for most resources

* dev 20

* add update page

* dev 21 support cli update link

* dev-22 test the deploy

* dev-23 use indexmap

* install-cli.py

* Frontend mobile fixes (#714)

* Allow ResourcePageHeader items to wrap

* Allow CardHeader items to wrap

* Increase z-index of sticky TableHeader, fixes #690

* Remove fixed widths from ActionButton, let them flex more to fit more layouts

* Make Section scroll overflow

* Remove grid class from Tabs, seems to prevent them from overflowing at small sizes

* deploy 1.18.5-dev-24

* auto version increment and deploy

* cli: profiles support aliases and merge on top of Default (root) config

* fix page set titles

* rust 1.89 and improve config logs

* skip serializing for proper merge

* fix clippy lints re 1.89

* remove layouts overflow-x-scroll

* deploy 1.18.5-dev-25

* 1.89 docker images not ready yet

* km cfg -a (print all profiles)

* include commit variables

* skip serializing profiles when empty

* skip serialize default db / log configs

* km cfg --debug print mode

* correct defaults for CLI and only can pass restore folder from cli arg

* some more skip serialization

* db restore / copy index optional

* add runfile command aliases

* remove second schedule updating loop, can causes some schedules to be missed

* deploy 1.18.5-dev-26

* add log when target db indexing disabled

* cli: user password reset, update user super admin

* Add manual network interface configuration for multi-NIC Docker environments (#719)

* Add iproute2 to debian-debs

* feat: Add manual network interface configuration for multi-NIC support

Complete implementation of manual interface configuration:
- Add internet_interface config option
- Implement manual gateway routing
- Add NET_ADMIN capability requirement
- Clean up codebase changes

* fix: Update internet interface handling for multi-NIC support

* refactor: Enhance error messages and logging in networking module

* refactor: Simplify interface argument handling and improve logging in network configuration and cleanup

* refactor(network): simplify startup integration and improve error handling

- Move config access and error handling into network::configure_internet_gateway()
- Simplify startup.rs to single function call without parameters
- Remove redundant check_network_privileges() function
- Improve error handling by checking actual command output instead of pre-validation
- Better separation of concerns between startup and network modules

Addresses feedback from PR discussion:
https://github.com/moghtech/komodo/pull/719#discussion_r2261542921

* fix(config): update default internet interface setting
Addresses feedback from PR discussion:
https://github.com/moghtech/komodo/pull/719#discussion_r2261552279

* fix(config): remove custom default for internet interface in CoreConfig

* move mod.rs -> network.rs
Addresses feedback from PR discussion:
https://github.com/moghtech/komodo/pull/719#discussion_r2261558332

* add internet interface example

* docs(build-images): document multi-platform builds with Docker Buildx (#721)

* docs(build-images): add multi-platform buildx guide to builders.md

* docs(build-images): add multi-platform buildx guide and clarify platform selection in Komodo UI Extra Args field

* move to 1.19.0

* core support reading from multiple config files

* config support yaml

* deploy 1.19.0-dev-1

* deploy 1.19.0-dev-2

* add default komodo cli config

* better config merge with base

* no need to panic if empty config paths

* improve km --help

* prog on cli docs

* tweak cli docs

* tweak doc

* split the runfile commands

* update docsite deps

* km ps initial

* km ls

* list resource apis

* km con inspect

* deploy 1.19.0-dev-3

* fix: need serde default

* dev-4 fix container parsing issue

* tweak

* use include-based file finding for much faster discovery

* just move to standard config dir .config/komodo/komodo.cli.*

* update fe w/ new contianer info minimal serialization

* add links to table names

* deploy 1.19.0-dev-5

* links in tables

* backend for Action arguments

* deploy 1.19.0-dev-6

* deploy 1.19.0-dev-7

* deploy 1.19.0-dev-8

* no space at front of KeyValue default args

* webhook branch / body optional

* The incoming arguments

* deploy 1.19.0-dev-9

* con -> cn

* add config -> cf alias

* .kmignore

* .peripheryinclude

* outdated

* optional links, configurable table format

* table_format -> table_borders

* get types

* include docsite in yarn install

* update runnables command in docs

* tweak

* improve km ls only show important stuff

* Add BackupCoreDatabase

* deploy 1.19.0-dev-10

* backup command needs "--yes"

* deploy 1.19.0-dev-11

* update rustc 1.89.0

* cli tweak

* try chef

* Fix chef (after dependencies)

* try other compile command

* fix

* fix comment

* cleanup stats page

* ensure database backup procedure

* UI allow configure Backup Core Database in Procedures

* procedure description

* deploy 1.19.0-dev-12

* deploy 1.19.0-dev-13

* GlobalAutoUpdate

* deploy 1.19.0-dev-14

* default tags and global auto update procedure

* deploy 1.19.0-dev-15

* trim the default procedure descriptions

* deploy 1.19.0-dev-16

* in "system" theme, also poll for updates to the theme based on time.

* Add next run to Action / Procedure column

* km ls support filter by templates

* fix procedure toml serialization when params = {}

* deploy 1.19.0-dev-17

* KOMODO_INIT_ADMIN_USERNAME

* KOMODO_FIRST_SERVER_NAME

* add server.config.external_address for use with links

* deploy 1.19.0-dev-18

* improve auto prune

* fix system theme auto update

* deploy 1.19.0-dev-19

* rename auth/CreateLocalUser -> SignUpLocalUser. Add write/CreateLocalUser for in-ui initialization.

* deploy 1.19.0-dev-20

* UI can handle multiple active logins

* deploy 1.19.0-dev-21

* fix

* add logout function

* fix oauth redirect

* fix multi user exchange token function

* default external address

* just Add

* style account switcher

* backup and restore docs

* rework docsite file / sidebar structure, start auto update docs

* auto update docs

* tweak

* fix doc links

* only pull / update running stacks / deployments images

* deploy 1.19.0-dev-22

* deploy 1.19.0-dev-23

* fix #737

* community docs

* add BackupCoreDatabase link to docs

* update ferret v2 update guide using komodo-cli

* fix data table headers overlapping topbar

* don't alert when deploying

* CommitSync returns Update

* deploy 1.19.0-dev-24

* trim the decoded branch

* action uses file contents deserializer

* deploy 1.19.0-dev-25

* remove Toml from action args format

* clarify External Address purpose

* Fix podman compatibility in `get_container_stats` (#739)

* Add podman compability for querying stats

Podman and docker stats differ in results in significant ways but this filter change they will output the same stats

* syntax fix

* feat(dashboard): display CPU, memory, and disk usage on server cards (#729)

* feat: mini-stats-card: Expose Server CPU , Memory, Disk Usage to Dashboard View

* comment: resolved

* Feat: fix overflow card , DRY stats-mini, add unreachable mini stats

* lint: fix

* deploy 1.19.0-dev-26

* 1.19.0

* linux, macos container install

* cli main config

---------

Co-authored-by: Brian Bradley <brian.bradley.p@gmail.com>
Co-authored-by: Daniel <daniel.barabasa@gmail.com>
Co-authored-by: eleith <eleith@users.noreply.github.com>
Co-authored-by: eleith <online-github@eleith.com>
Co-authored-by: Sam Edwards <sam@samedwards.ca>
Co-authored-by: Marcel Pfennig <82059270+MP-Tool@users.noreply.github.com>
Co-authored-by: itsmesid <693151+arevindh@users.noreply.github.com>
Co-authored-by: mbecker20 <max@mogh.tech>
Co-authored-by: Rhyn <Rhyn@users.noreply.github.com>
Co-authored-by: Anh Nguyen <tuananh131001@gmail.com>
2025-08-17 17:25:45 -07:00

1172 lines
32 KiB
Rust

use std::{cmp::Ordering, collections::HashMap};
use comfy_table::{Attribute, Cell, Color};
use futures_util::{FutureExt, try_join};
use komodo_client::{
KomodoClient,
api::read::{
ListActions, ListAlerters, ListBuilders, ListBuilds,
ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,
ListSchedules, ListServers, ListStacks, ListTags,
},
entities::{
ResourceTargetVariant,
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},
resource_link,
schedule::Schedule,
server::{ServerListItem, ServerListItemInfo, ServerState},
stack::{StackListItem, StackListItemInfo, StackState},
sync::{
ResourceSyncListItem, ResourceSyncListItemInfo,
ResourceSyncState,
},
},
};
use serde::Serialize;
use crate::{
command::{
PrintTable, format_timetamp, matches_wildcards, parse_wildcards,
print_items,
},
config::cli_config,
};
pub async fn handle(list: &args::list::List) -> anyhow::Result<()> {
match &list.command {
None => list_all(list).await,
Some(ListCommand::Servers(filters)) => {
list_resources::<ServerListItem>(filters, false).await
}
Some(ListCommand::Stacks(filters)) => {
list_resources::<StackListItem>(filters, false).await
}
Some(ListCommand::Deployments(filters)) => {
list_resources::<DeploymentListItem>(filters, false).await
}
Some(ListCommand::Builds(filters)) => {
list_resources::<BuildListItem>(filters, false).await
}
Some(ListCommand::Repos(filters)) => {
list_resources::<RepoListItem>(filters, false).await
}
Some(ListCommand::Procedures(filters)) => {
list_resources::<ProcedureListItem>(filters, false).await
}
Some(ListCommand::Actions(filters)) => {
list_resources::<ActionListItem>(filters, false).await
}
Some(ListCommand::Syncs(filters)) => {
list_resources::<ResourceSyncListItem>(filters, false).await
}
Some(ListCommand::Builders(filters)) => {
list_resources::<BuilderListItem>(filters, false).await
}
Some(ListCommand::Alerters(filters)) => {
list_resources::<AlerterListItem>(filters, false).await
}
Some(ListCommand::Schedules(filters)) => {
list_schedules(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,
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, true),
StackListItem::list(client, &filters, true),
DeploymentListItem::list(client, &filters, true),
BuildListItem::list(client, &filters, true),
RepoListItem::list(client, &filters, true),
ProcedureListItem::list(client, &filters, true),
ActionListItem::list(client, &filters, true),
ResourceSyncListItem::list(client, &filters, true),
)?;
if !servers.is_empty() {
fix_tags(&mut servers, &tags);
print_items(servers, filters.format, list.links)?;
println!();
}
if !stacks.is_empty() {
fix_tags(&mut stacks, &tags);
print_items(stacks, filters.format, list.links)?;
println!();
}
if !deployments.is_empty() {
fix_tags(&mut deployments, &tags);
print_items(deployments, filters.format, list.links)?;
println!();
}
if !builds.is_empty() {
fix_tags(&mut builds, &tags);
print_items(builds, filters.format, list.links)?;
println!();
}
if !repos.is_empty() {
fix_tags(&mut repos, &tags);
print_items(repos, filters.format, list.links)?;
println!();
}
if !procedures.is_empty() {
fix_tags(&mut procedures, &tags);
print_items(procedures, filters.format, list.links)?;
println!();
}
if !actions.is_empty() {
fix_tags(&mut actions, &tags);
print_items(actions, filters.format, list.links)?;
println!();
}
if !syncs.is_empty() {
fix_tags(&mut syncs, &tags);
print_items(syncs, filters.format, list.links)?;
println!();
}
Ok(())
}
async fn list_resources<T>(
filters: &ResourceFilters,
minimal: bool,
) -> anyhow::Result<()>
where
T: ListResources,
ResourceListItem<T::Info>: PrintTable + Serialize,
{
let client = crate::command::komodo_client().await?;
let (mut resources, tags) = tokio::try_join!(
T::list(client, filters, minimal),
client.read(ListTags::default()).map(|res| res.map(|res| res
.into_iter()
.map(|t| (t.id, t.name))
.collect::<HashMap<_, _>>()))
)?;
fix_tags(&mut resources, &tags);
if !resources.is_empty() {
print_items(resources, filters.format, filters.links)?;
}
Ok(())
}
async fn list_schedules(
filters: &ResourceFilters,
) -> anyhow::Result<()> {
let client = crate::command::komodo_client().await?;
let (mut schedules, tags) = tokio::try_join!(
client
.read(ListSchedules {
tags: filters.tags.clone(),
tag_behavior: Default::default(),
})
.map(|res| res.map(|res| res
.into_iter()
.filter(|s| s.next_scheduled_run.is_some())
.collect::<Vec<_>>())),
client.read(ListTags::default()).map(|res| res.map(|res| res
.into_iter()
.map(|t| (t.id, t.name))
.collect::<HashMap<_, _>>()))
)?;
schedules.iter_mut().for_each(|resource| {
resource.tags.iter_mut().for_each(|id| {
let Some(name) = tags.get(id) else {
*id = String::new();
return;
};
id.clone_from(name);
});
});
schedules.sort_by(|a, b| {
match (a.next_scheduled_run, b.next_scheduled_run) {
(Some(_), None) => return Ordering::Less,
(None, Some(_)) => return Ordering::Greater,
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => {}
}
a.name.cmp(&b.name).then(a.enabled.cmp(&b.enabled))
});
if !schedules.is_empty() {
print_items(schedules, filters.format, filters.links)?;
}
Ok(())
}
fn fix_tags<T>(
resources: &mut Vec<ResourceListItem<T>>,
tags: &HashMap<String, String>,
) {
resources.iter_mut().for_each(|resource| {
resource.tags.iter_mut().for_each(|id| {
let Some(name) = tags.get(id) else {
*id = String::new();
return;
};
id.clone_from(name);
});
});
}
trait ListResources: Sized
where
ResourceListItem<Self::Info>: PrintTable,
{
type Info;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
// For use with root `km ls`
minimal: bool,
) -> anyhow::Result<Vec<ResourceListItem<Self::Info>>>;
}
// LIST
impl ListResources for ServerListItem {
type Info = ServerListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let servers = client
.read(ListServers {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
})
.await?;
let names = parse_wildcards(&filters.names);
let server_wildcards = parse_wildcards(&filters.servers);
let mut servers = servers
.into_iter()
.filter(|server| {
let state_check = if filters.all {
true
} else if filters.down {
!matches!(server.info.state, ServerState::Ok)
} else if filters.in_progress {
false
} else {
matches!(server.info.state, ServerState::Ok)
};
let name_items = &[server.name.as_str()];
state_check
&& matches_wildcards(&names, name_items)
&& matches_wildcards(&server_wildcards, name_items)
})
.collect::<Vec<_>>();
servers.sort_by(|a, b| {
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
});
Ok(servers)
}
}
impl ListResources for StackListItem {
type Info = StackListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let (servers, mut stacks) = 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(ListStacks {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
})
)?;
stacks.iter_mut().for_each(|stack| {
if stack.info.server_id.is_empty() {
return;
}
let Some(server) = servers.get(&stack.info.server_id) else {
return;
};
stack.info.server_id.clone_from(&server.name);
});
let names = parse_wildcards(&filters.names);
let servers = parse_wildcards(&filters.servers);
let mut stacks = stacks
.into_iter()
.filter(|stack| {
let state_check = if filters.all {
true
} else if filters.down {
!matches!(
stack.info.state,
StackState::Running | StackState::Deploying
)
} else if filters.in_progress {
matches!(stack.info.state, StackState::Deploying)
} else {
matches!(
stack.info.state,
StackState::Running | StackState::Deploying
)
};
state_check
&& matches_wildcards(&names, &[stack.name.as_str()])
&& matches_wildcards(
&servers,
&[stack.info.server_id.as_str()],
)
})
.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))
});
Ok(stacks)
}
}
impl ListResources for DeploymentListItem {
type Info = DeploymentListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
_minimal: bool,
) -> 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)
.templates(filters.templates)
.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 | DeploymentState::Deploying
)
} else if filters.in_progress {
matches!(deployment.info.state, DeploymentState::Deploying)
} else {
matches!(
deployment.info.state,
DeploymentState::Running | DeploymentState::Deploying
)
};
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,
minimal: bool,
) -> 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)
.templates(filters.templates)
.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| {
let state_check = if filters.all {
true
} else if filters.down {
matches!(
build.info.state,
BuildState::Failed | BuildState::Unknown
)
} else if minimal || filters.in_progress {
matches!(build.info.state, BuildState::Building)
} else {
true
};
state_check
&& 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,
minimal: bool,
) -> 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)
.templates(filters.templates)
.build(),
})
.await?
.into_iter()
.filter(|repo| {
let state_check = if filters.all {
true
} else if filters.down {
matches!(
repo.info.state,
RepoState::Failed | RepoState::Unknown
)
} else if minimal || filters.in_progress {
matches!(
repo.info.state,
RepoState::Building | RepoState::Cloning
)
} else {
true
};
state_check
&& 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,
minimal: bool,
) -> 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)
.templates(filters.templates)
.build(),
})
.await?
.into_iter()
.filter(|procedure| {
let state_check = if filters.all {
true
} else if filters.down {
matches!(
procedure.info.state,
ProcedureState::Failed | ProcedureState::Unknown
)
} else if minimal || filters.in_progress {
matches!(procedure.info.state, ProcedureState::Running)
} else {
true
};
state_check
&& matches_wildcards(&names, &[procedure.name.as_str()])
})
.collect::<Vec<_>>();
procedures.sort_by(|a, b| {
match (a.info.next_scheduled_run, b.info.next_scheduled_run) {
(Some(_), None) => return Ordering::Less,
(None, Some(_)) => return Ordering::Greater,
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => {}
}
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,
minimal: bool,
) -> 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)
.templates(filters.templates)
.build(),
})
.await?
.into_iter()
.filter(|action| {
let state_check = if filters.all {
true
} else if filters.down {
matches!(
action.info.state,
ActionState::Failed | ActionState::Unknown
)
} else if minimal || filters.in_progress {
matches!(action.info.state, ActionState::Running)
} else {
true
};
state_check
&& matches_wildcards(&names, &[action.name.as_str()])
})
.collect::<Vec<_>>();
actions.sort_by(|a, b| {
match (a.info.next_scheduled_run, b.info.next_scheduled_run) {
(Some(_), None) => return Ordering::Less,
(None, Some(_)) => return Ordering::Greater,
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => {}
}
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,
minimal: bool,
) -> 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)
.templates(filters.templates)
.build(),
})
.await?
.into_iter()
.filter(|sync| {
let state_check = if filters.all {
true
} else if filters.down {
matches!(
sync.info.state,
ResourceSyncState::Failed | ResourceSyncState::Unknown
)
} else if minimal || filters.in_progress {
matches!(
sync.info.state,
ResourceSyncState::Syncing | ResourceSyncState::Pending
)
} else {
true
};
state_check
&& 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,
minimal: bool,
) -> 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)
.templates(filters.templates)
.build(),
})
.await?
.into_iter()
.filter(|builder| {
(!minimal || filters.all)
&& 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,
minimal: bool,
) -> 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)
.templates(filters.templates)
.build(),
})
.await?
.into_iter()
.filter(|sync| {
(!minimal || filters.all)
&& 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> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Server", "State", "Address", "Tags", "Link"]
} else {
&["Server", "State", "Address", "Tags"]
}
}
fn row(self, links: bool) -> Vec<Cell> {
let color = match self.info.state {
ServerState::Ok => Color::Green,
ServerState::NotOk => Color::Red,
ServerState::Disabled => Color::Blue,
};
let mut res = 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.address),
Cell::new(self.tags.join(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Server,
&self.id,
)))
}
res
}
}
impl PrintTable for ResourceListItem<StackListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Stack", "State", "Server", "Tags", "Link"]
} else {
&["Stack", "State", "Server", "Tags"]
}
}
fn row(self, links: bool) -> Vec<comfy_table::Cell> {
let color = match self.info.state {
StackState::Down => Color::Blue,
StackState::Running => Color::Green,
StackState::Deploying => Color::DarkYellow,
StackState::Paused => Color::DarkYellow,
StackState::Unknown => Color::Magenta,
_ => Color::Red,
};
// let source = if self.info.files_on_host {
// "On Host"
// } else if !self.info.repo.is_empty() {
// self.info.repo_link.as_str()
// } else {
// "UI Defined"
// };
let mut res = 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(source),
Cell::new(self.tags.join(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Stack,
&self.id,
)))
}
res
}
}
impl PrintTable for ResourceListItem<DeploymentListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Deployment", "State", "Server", "Tags", "Link"]
} else {
&["Deployment", "State", "Server", "Tags"]
}
}
fn row(self, links: bool) -> Vec<comfy_table::Cell> {
let color = match self.info.state {
DeploymentState::NotDeployed => Color::Blue,
DeploymentState::Running => Color::Green,
DeploymentState::Deploying => Color::DarkYellow,
DeploymentState::Paused => Color::DarkYellow,
DeploymentState::Unknown => Color::Magenta,
_ => Color::Red,
};
let mut res = 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(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Deployment,
&self.id,
)))
}
res
}
}
impl PrintTable for ResourceListItem<BuildListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Build", "State", "Builder", "Tags", "Link"]
} else {
&["Build", "State", "Builder", "Tags"]
}
}
fn row(self, links: bool) -> 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,
};
let mut res = 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(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Build,
&self.id,
)));
}
res
}
}
impl PrintTable for ResourceListItem<RepoListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Repo", "State", "Link", "Tags", "Link"]
} else {
&["Repo", "State", "Link", "Tags"]
}
}
fn row(self, links: bool) -> 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,
};
let mut res = 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(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Repo,
&self.id,
)))
}
res
}
}
impl PrintTable for ResourceListItem<ProcedureListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Procedure", "State", "Next Run", "Tags", "Link"]
} else {
&["Procedure", "State", "Next Run", "Tags"]
}
}
fn row(self, links: bool) -> 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"))
};
let mut res = 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(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Procedure,
&self.id,
)))
}
res
}
}
impl PrintTable for ResourceListItem<ActionListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Action", "State", "Next Run", "Tags", "Link"]
} else {
&["Action", "State", "Next Run", "Tags"]
}
}
fn row(self, links: bool) -> 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"))
};
let mut res = 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(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Action,
&self.id,
)));
}
res
}
}
impl PrintTable for ResourceListItem<ResourceSyncListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Sync", "State", "Tags", "Link"]
} else {
&["Sync", "State", "Tags"]
}
}
fn row(self, links: bool) -> 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,
};
let mut res = 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(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::ResourceSync,
&self.id,
)))
}
res
}
}
impl PrintTable for ResourceListItem<BuilderListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Builder", "Type", "Tags", "Link"]
} else {
&["Builder", "Type", "Tags"]
}
}
fn row(self, links: bool) -> Vec<comfy_table::Cell> {
let mut res = vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.builder_type),
Cell::new(self.tags.join(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Builder,
&self.id,
)));
}
res
}
}
impl PrintTable for ResourceListItem<AlerterListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Alerter", "Type", "Enabled", "Tags", "Link"]
} else {
&["Alerter", "Type", "Enabled", "Tags"]
}
}
fn row(self, links: bool) -> Vec<comfy_table::Cell> {
let mut row = 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(", ")),
];
if links {
row.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Alerter,
&self.id,
)));
}
row
}
}
impl PrintTable for Schedule {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Name", "Type", "Next Run", "Tags", "Link"]
} else {
&["Name", "Type", "Next Run", "Tags"]
}
}
fn row(self, links: bool) -> Vec<comfy_table::Cell> {
let next_run = if let Some(ts) = self.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"))
};
let (resource_type, id) = self.target.extract_variant_id();
let mut res = vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.target.extract_variant_id().0),
next_run,
Cell::new(self.tags.join(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
resource_type,
id,
)));
}
res
}
}