Compare commits

..
1 Commits
Author SHA1 Message Date
mbecker20 a246eaf35a fix docker build requires xtask folder copy 2026-05-07 11:32:36 -07:00
353 changed files with 6284 additions and 16609 deletions
Generated
+271 -360
View File
File diff suppressed because it is too large Load Diff
+34 -36
View File
@@ -9,8 +9,9 @@ members = [
]
[workspace.package]
version = "2.3.0"
version = "2.2.0"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
repository = "https://github.com/moghtech/komodo"
homepage = "https://komo.do"
@@ -33,34 +34,34 @@ slack = { version = "2.0.0", package = "slack_client_rs", default-features = fal
mogh_error = { version = "1.0.4", default-features = false }
derive_default_builder = "0.1.8"
async_timing_util = "1.1.0"
mogh_auth_client = "1.7.0"
mogh_auth_server = "1.7.0"
mogh_auth_client = "1.5.0"
mogh_auth_server = "1.5.0"
mogh_secret_file = "1.0.1"
mogh_validations = "1.0.1"
mogh_rate_limit = "1.0.1"
partial_derive2 = "0.5.0"
mongo_indexed = "2.1.0"
mongo_indexed = "2.0.2"
mogh_resolver = "1.0.0"
mogh_config = "1.1.0"
mogh_logger = "1.3.4"
mogh_logger = "1.3.3"
mogh_server = "1.5.0"
toml_pretty = "2.0.0"
mogh_cache = "1.1.2"
mogh_pki = "1.1.3"
mungos = "3.7.0"
mungos = "3.2.2"
svi = "1.2.0"
# ASYNC
reqwest = { version = "0.13.4", default-features = false, features = ["json", "stream", "form", "query", "rustls"] }
tokio = { version = "1.53.1", features = ["full"] }
tokio-util = { version = "0.7.19", features = ["io", "codec"] }
tokio-stream = { version = "0.1.19", features = ["sync"] }
reqwest = { version = "0.13.3", default-features = false, features = ["json", "stream", "form", "query", "rustls"] }
tokio = { version = "1.52.2", features = ["full"] }
tokio-util = { version = "0.7.18", features = ["io", "codec"] }
tokio-stream = { version = "0.1.18", features = ["sync"] }
pin-project-lite = "0.2.17"
futures-util = "0.3.33"
arc-swap = "1.9.2"
futures-util = "0.3.32"
arc-swap = "1.9.0"
# SERVER
tokio-tungstenite = { version = "0.30.0", features = ["rustls-tls-native-roots"] }
tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-native-roots"] }
axum = { version = "0.8.9", features = ["ws", "json", "macros"] }
axum-extra = { version = "0.12.6", features = ["typed-header"] }
@@ -71,40 +72,37 @@ utoipa = "5.5.0"
# SER/DE
ipnetwork = { version = "0.21.1", features = ["serde"] }
indexmap = { version = "2.14.0", features = ["serde"] }
serde = { version = "1.0.229", features = ["derive"] }
serde = { version = "1.0.227", features = ["derive"] }
strum = { version = "0.28.0", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
toml = "1.1.3"
toml = "1.1.2"
serde_yaml_ng = "0.10.0"
serde_json = "1.0.151"
serde_qs = "1.1.2"
serde_json = "1.0.149"
serde_qs = "1.1.0"
url = "2.5.8"
# ERROR
anyhow = "1.0.104"
thiserror = "2.0.19"
# SYSTEM
libc = "0.2.189"
anyhow = "1.0.102"
thiserror = "2.0.18"
# LOGGING
tracing = "0.1.44"
# CONFIG
clap = { version = "4.6.4", features = ["derive"] }
clap = { version = "4.6.1", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.24.0", features = ["v4", "fast-rng", "serde"] }
rustls = { version = "0.23.42", features = ["aws-lc-rs"] }
data-encoding = "2.11.0"
uuid = { version = "1.23.1", features = ["v4", "fast-rng", "serde"] }
rustls = { version = "0.23.40", features = ["aws-lc-rs"] }
data-encoding = "2.10.0"
urlencoding = "2.1.3"
bcrypt = "0.19.2"
bcrypt = "0.19.0"
hmac = "0.13.0"
sha1 = "0.11.0"
sha2 = "0.11.0"
rand = "0.10.2"
rand = "0.10.1"
hex = "0.4.3"
# SYSTEM
@@ -113,18 +111,18 @@ portable-pty = "0.9.0"
shell-escape = "0.1.5"
crossterm = "0.29.0"
bollard = "0.21.0"
sysinfo = "0.39.6"
shlex = "2.0.1"
sysinfo = "0.38.4"
shlex = "1.3.0"
# CLOUD
aws-config = "1.10.0"
aws-sdk-ec2 = "1.241.0"
aws-credential-types = "1.3.0"
aws-config = "1.8.16"
aws-sdk-ec2 = "1.224.0"
aws-credential-types = "1.2.14"
## CRON
english-to-cron = "0.1.7"
chrono-tz = "0.10.4"
chrono = "0.4.45"
chrono = "0.4.44"
croner = "3.0.1"
# MISC
@@ -135,8 +133,8 @@ comfy-table = "7.2.2"
typeshare = "1.0.5"
wildcard = "0.3.0"
colored = "3.1.1"
bytes = "1.12.1"
regex = "1.13.1"
bytes = "1.11.1"
regex = "1.12.3"
[profile.release]
strip = "debuginfo"
+1 -1
View File
@@ -1,4 +1,4 @@
const cmd = "km run -y action deploy-komodo-ui-change";
const cmd = "km run -y action deploy-komodo-fe-change";
new Deno.Command("bash", {
args: ["-c", cmd],
}).spawn();
+1 -1
View File
@@ -1,7 +1,7 @@
## Builds the Komodo Core, Periphery, and Util binaries
## for a specific architecture. Requires OpenSSL 3 or later.
FROM rust:1.97.1-bookworm AS builder
FROM rust:1.95.0-bookworm AS builder
RUN cargo install cargo-strip
WORKDIR /builder
+1 -1
View File
@@ -3,7 +3,7 @@
## Uses chef for dependency caching to help speed up back-to-back builds.
FROM lukemathwalker/cargo-chef:latest-rust-1.97.1-bookworm AS chef
FROM lukemathwalker/cargo-chef:latest-rust-1.95.0-bookworm AS chef
WORKDIR /builder
# Plan just the RECIPE to see if things have changed
+1
View File
@@ -3,6 +3,7 @@ name = "komodo_cli"
description = "Command line tool for Komodo"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
+1 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.97.1-trixie AS builder
FROM rust:1.95.0-trixie AS builder
RUN cargo install cargo-strip
WORKDIR /builder
+27 -25
View File
@@ -7,16 +7,16 @@ use futures_util::{
FutureExt, TryStreamExt, stream::FuturesUnordered,
};
use komodo_client::{
api::read::{self, ListAllContainers, ListServers},
api::read::{
InspectDockerContainer, ListAllDockerContainers, ListServers,
},
entities::{
config::cli::args::container::{
Container, ContainerCommand, InspectContainer,
},
docker::{
self,
container::{
ContainerListItem, ContainerSortBy, ContainerStateStatusEnum,
},
container::{ContainerListItem, ContainerStateStatusEnum},
},
},
};
@@ -44,12 +44,11 @@ async fn list_containers(
down,
links,
reverse,
containers: terms,
containers: names,
images,
networks,
servers,
format,
page,
command: _,
}: &Container,
) -> anyhow::Result<()> {
@@ -61,16 +60,9 @@ async fn list_containers(
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllContainers {
client.read(ListAllDockerContainers {
servers: Default::default(),
tags: Default::default(),
terms: terms.clone(),
state: Default::default(),
limit: Some(100),
// Page is more naturally given starting as 1, 2, 3.
page: if *page == 0 { 0 } else { *page - 1 },
sort_by: ContainerSortBy::Server,
sort_desc: false,
containers: Default::default(),
}),
)?;
@@ -86,6 +78,7 @@ async fn list_containers(
(Some(server.name.as_str()), c)
});
let names = parse_wildcards(names);
let servers = parse_wildcards(servers);
let images = parse_wildcards(images);
let networks = parse_wildcards(networks);
@@ -112,6 +105,7 @@ async fn list_containers(
);
state_check
&& network_check
&& matches_wildcards(&names, &[c.name.as_str()])
&& matches_wildcards(
&servers,
&server_name
@@ -151,15 +145,9 @@ pub async fn inspect_container(
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllContainers {
servers: inspect.servers.clone(),
tags: Default::default(),
terms: vec![inspect.container.clone()],
state: Default::default(),
limit: Some(0),
page: 0,
sort_by: ContainerSortBy::Server,
sort_desc: false,
client.read(ListAllDockerContainers {
servers: Default::default(),
containers: Default::default()
}),
)?;
@@ -174,11 +162,25 @@ pub async fn inspect_container(
c.server_id = Some(server.name.clone());
});
let names = [inspect.container.to_string()];
let names = parse_wildcards(&names);
let servers = parse_wildcards(&inspect.servers);
let mut containers = containers
.into_iter()
.filter(|c| {
matches_wildcards(&names, &[c.name.as_str()])
&& matches_wildcards(
&servers,
&c.server_id
.as_deref()
.map(|i| vec![i])
.unwrap_or_default(),
)
})
.map(|c| async move {
client
.read(read::InspectContainer {
.read(InspectDockerContainer {
container: c.name,
server: c.server_id.context("No server...")?,
})
-2
View File
@@ -81,9 +81,7 @@ pub async fn handle(
let res = handle_execution!(
execute: [
RunAction,
CancelAction,
RunProcedure,
CancelProcedure,
RunBuild,
CancelBuild,
Deploy,
+31 -193
View File
@@ -7,8 +7,7 @@ use komodo_client::{
api::read::{
ListActions, ListAlerters, ListBuilders, ListBuilds,
ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,
ListSchedules, ListServers, ListStacks, ListSwarms, ListTags,
ListTerminals,
ListSchedules, ListServers, ListStacks, ListTags, ListTerminals,
},
entities::{
ResourceTargetVariant,
@@ -32,7 +31,6 @@ use komodo_client::{
schedule::Schedule,
server::{ServerListItem, ServerListItemInfo, ServerState},
stack::{StackListItem, StackListItemInfo, StackState},
swarm::{SwarmListItem, SwarmListItemInfo, SwarmState},
sync::{
ResourceSyncListItem, ResourceSyncListItemInfo,
ResourceSyncState,
@@ -54,46 +52,28 @@ 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, list.page, false)
.await
}
Some(ListCommand::Swarms(filters)) => {
list_resources::<SwarmListItem>(filters, list.page, false).await
list_resources::<ServerListItem>(filters, false).await
}
Some(ListCommand::Stacks(filters)) => {
list_resources::<StackListItem>(filters, list.page, false).await
list_resources::<StackListItem>(filters, false).await
}
Some(ListCommand::Deployments(filters)) => {
list_resources::<DeploymentListItem>(filters, list.page, false)
.await
list_resources::<DeploymentListItem>(filters, false).await
}
Some(ListCommand::Builds(filters)) => {
list_resources::<BuildListItem>(filters, list.page, false).await
list_resources::<BuildListItem>(filters, false).await
}
Some(ListCommand::Repos(filters)) => {
list_resources::<RepoListItem>(filters, list.page, false).await
list_resources::<RepoListItem>(filters, false).await
}
Some(ListCommand::Procedures(filters)) => {
list_resources::<ProcedureListItem>(filters, list.page, false)
.await
list_resources::<ProcedureListItem>(filters, false).await
}
Some(ListCommand::Actions(filters)) => {
list_resources::<ActionListItem>(filters, list.page, false)
.await
list_resources::<ActionListItem>(filters, false).await
}
Some(ListCommand::Syncs(filters)) => {
list_resources::<ResourceSyncListItem>(
filters, list.page, false,
)
.await
}
Some(ListCommand::Builders(filters)) => {
list_resources::<BuilderListItem>(filters, list.page, false)
.await
}
Some(ListCommand::Alerters(filters)) => {
list_resources::<AlerterListItem>(filters, list.page, false)
.await
list_resources::<ResourceSyncListItem>(filters, false).await
}
Some(ListCommand::Terminals(filters)) => {
list_terminals(filters).await
@@ -101,6 +81,12 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> {
Some(ListCommand::Schedules(filters)) => {
list_schedules(filters).await
}
Some(ListCommand::Builders(filters)) => {
list_resources::<BuilderListItem>(filters, false).await
}
Some(ListCommand::Alerters(filters)) => {
list_resources::<AlerterListItem>(filters, false).await
}
}
}
@@ -111,7 +97,6 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
let (
tags,
mut servers,
mut swarms,
mut stacks,
mut deployments,
mut builds,
@@ -124,15 +109,14 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
.into_iter()
.map(|t| (t.id, t.name))
.collect::<HashMap<_, _>>())),
ServerListItem::list(client, &filters, list.page, true),
SwarmListItem::list(client, &filters, list.page, true),
StackListItem::list(client, &filters, list.page, true),
DeploymentListItem::list(client, &filters, list.page, true),
BuildListItem::list(client, &filters, list.page, true),
RepoListItem::list(client, &filters, list.page, true),
ProcedureListItem::list(client, &filters, list.page, true),
ActionListItem::list(client, &filters, list.page, true),
ResourceSyncListItem::list(client, &filters, list.page, true),
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() {
@@ -141,12 +125,6 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
println!();
}
if !swarms.is_empty() {
fix_tags(&mut swarms, &tags);
print_items(swarms, filters.format, list.links)?;
println!();
}
if !stacks.is_empty() {
fix_tags(&mut stacks, &tags);
print_items(stacks, filters.format, list.links)?;
@@ -194,7 +172,6 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
async fn list_resources<T>(
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<()>
where
@@ -203,7 +180,7 @@ where
{
let client = crate::command::komodo_client().await?;
let (mut resources, tags) = tokio::try_join!(
T::list(client, filters, page, minimal),
T::list(client, filters, minimal),
client.read(ListTags::default()).map(|res| res.map(|res| res
.into_iter()
.map(|t| (t.id, t.name))
@@ -228,8 +205,6 @@ async fn list_terminals(
.read(ListTerminals {
target: None,
use_names: true,
limit: Some(0),
..Default::default()
})
.await?;
if !terminals.is_empty() {
@@ -247,8 +222,6 @@ async fn list_schedules(
.read(ListSchedules {
tags: filters.tags.clone(),
tag_behavior: Default::default(),
limit: Some(0),
..Default::default()
})
.map(|res| res.map(|res| res
.into_iter()
@@ -306,7 +279,6 @@ where
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
// For use with root `km ls`
minimal: bool,
) -> anyhow::Result<Vec<ResourceListItem<Self::Info>>>;
@@ -319,7 +291,6 @@ impl ListResources for ServerListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let servers = client
@@ -329,10 +300,6 @@ impl ListResources for ServerListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?;
let names = parse_wildcards(&filters.names);
@@ -362,68 +329,17 @@ impl ListResources for ServerListItem {
}
}
impl ListResources for SwarmListItem {
type Info = SwarmListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let servers = client
.read(ListSwarms {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.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, SwarmState::Healthy)
} else if filters.in_progress {
false
} else {
matches!(server.info.state, SwarmState::Healthy)
};
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,
page: u64,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let (servers, mut stacks) = tokio::try_join!(
client
.read(ListServers {
query: ResourceQuery::builder().build(),
limit: Some(0),
..Default::default()
})
.map(|res| res.map(|res| res
.into_iter()
@@ -435,10 +351,6 @@ impl ListResources for StackListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false
})
)?;
stacks.iter_mut().for_each(|stack| {
@@ -494,15 +406,12 @@ impl ListResources for DeploymentListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let (servers, mut deployments) = tokio::try_join!(
client
.read(ListServers {
query: ResourceQuery::builder().build(),
limit: Some(0),
..Default::default()
})
.map(|res| res.map(|res| res
.into_iter()
@@ -514,10 +423,6 @@ impl ListResources for DeploymentListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false
})
)?;
deployments.iter_mut().for_each(|deployment| {
@@ -574,15 +479,12 @@ impl ListResources for BuildListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let (builders, mut builds) = tokio::try_join!(
client
.read(ListBuilders {
query: ResourceQuery::builder().build(),
limit: Some(0),
..Default::default()
})
.map(|res| res.map(|res| res
.into_iter()
@@ -594,10 +496,6 @@ impl ListResources for BuildListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false
})
)?;
builds.iter_mut().for_each(|build| {
@@ -635,7 +533,10 @@ impl ListResources for BuildListItem {
})
.collect::<Vec<_>>();
builds.sort_by(|a, b| {
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
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)
}
@@ -646,7 +547,6 @@ impl ListResources for RepoListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -657,10 +557,6 @@ impl ListResources for RepoListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -699,7 +595,6 @@ impl ListResources for ProcedureListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -710,10 +605,6 @@ impl ListResources for ProcedureListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -741,7 +632,7 @@ impl ListResources for ProcedureListItem {
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => {}
}
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
});
Ok(procedures)
}
@@ -752,7 +643,6 @@ impl ListResources for ActionListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -763,10 +653,6 @@ impl ListResources for ActionListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -794,7 +680,7 @@ impl ListResources for ActionListItem {
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => {}
}
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
});
Ok(actions)
}
@@ -805,7 +691,6 @@ impl ListResources for ResourceSyncListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -816,10 +701,6 @@ impl ListResources for ResourceSyncListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -844,7 +725,7 @@ impl ListResources for ResourceSyncListItem {
})
.collect::<Vec<_>>();
syncs.sort_by(|a, b| {
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
});
Ok(syncs)
}
@@ -855,7 +736,6 @@ impl ListResources for BuilderListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -866,10 +746,6 @@ impl ListResources for BuilderListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -892,7 +768,6 @@ impl ListResources for AlerterListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -903,10 +778,6 @@ impl ListResources for AlerterListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -961,39 +832,6 @@ impl PrintTable for ResourceListItem<ServerListItemInfo> {
}
}
impl PrintTable for ResourceListItem<SwarmListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Swarm", "State", "Tags", "Link"]
} else {
&["Swarm", "State", "Tags"]
}
}
fn row(self, links: bool) -> Vec<Cell> {
let color = match self.info.state {
SwarmState::Healthy => Color::Green,
SwarmState::Unhealthy => Color::Red,
SwarmState::Down => Color::Blue,
SwarmState::Unknown => Color::Magenta,
};
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::Server,
&self.id,
)))
}
res
}
}
impl PrintTable for ResourceListItem<StackListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
+3 -13
View File
@@ -2,7 +2,7 @@ use anyhow::{Context, anyhow};
use colored::Colorize;
use komodo_client::{
api::{
read::{GetServer, ListAllContainers, ListServers},
read::{GetServer, ListAllDockerContainers, ListServers},
terminal::InitTerminal,
},
entities::{
@@ -118,8 +118,6 @@ async fn get_server(
server: Option<String>,
container: &str,
) -> anyhow::Result<String> {
// Don't need to find server for terminal,
// user provided it, early return.
if let Some(server) = server {
return Ok(server);
}
@@ -127,15 +125,9 @@ async fn get_server(
let client = super::komodo_client().await?;
let mut containers = client
.read(ListAllContainers {
.read(ListAllDockerContainers {
servers: Default::default(),
tags: Default::default(),
terms: vec![container.to_string()],
state: Default::default(),
limit: Some(0),
page: 0,
sort_by: Default::default(),
sort_desc: false,
containers: vec![container.to_string()],
})
.await?;
@@ -164,8 +156,6 @@ async fn get_server(
let servers = client
.read(ListServers {
query: ServerQuery::builder().names(servers).build(),
limit: Some(0),
..Default::default()
})
.await?
.into_iter()
+1
View File
@@ -2,6 +2,7 @@
name = "komodo_core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
+1 -1
View File
@@ -1,7 +1,7 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.97.1-trixie AS core-builder
FROM rust:1.95.0-trixie AS core-builder
RUN cargo install cargo-strip
WORKDIR /builder
+14 -97
View File
@@ -4,26 +4,23 @@ use std::{
sync::OnceLock,
};
use anyhow::Context as _;
use command::{CommandOptions, run_komodo_standard_command};
use database::{
bson::doc,
mungos::{by_id::update_one_by_id, mongodb::bson::to_document},
use anyhow::Context;
use command::run_komodo_standard_command;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
};
use interpolate::Interpolator;
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunAction, CancelAction, RunAction,
},
api::execute::{BatchExecutionResponse, BatchRunAction, RunAction},
entities::{
FileFormat, JsonObject, Operation,
FileFormat, JsonObject,
action::Action,
alert::{Alert, AlertData, SeverityLevel},
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
random_string,
update::{Update, UpdateStatus},
update::Update,
user::action_user,
},
parsers::parse_key_value_list,
@@ -37,7 +34,6 @@ use mogh_auth_server::api::manage::api_key::{
use mogh_config::merge_objects;
use mogh_resolver::Resolve;
use tokio::fs;
use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
@@ -50,7 +46,7 @@ use crate::{
},
permission::get_check_permissions,
resource::refresh_action_state_cache,
state::{action_cancel_cache, action_states, db_client},
state::{action_states, db_client},
};
use super::ExecuteArgs;
@@ -73,7 +69,6 @@ impl Resolve<ExecuteArgs> for BatchRunAction {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
@@ -81,12 +76,8 @@ impl Resolve<ExecuteArgs> for BatchRunAction {
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunAction>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchRunAction>(&self.pattern, user)
.await?,
)
}
}
@@ -125,7 +116,7 @@ impl Resolve<ExecuteArgs> for RunAction {
// This will set action state back to default when dropped.
// Will also check to ensure action not already busy before updating.
let action_guard = action_state.update_custom(
let _action_guard = action_state.update_custom(
|state| state.running += 1,
|state| state.running -= 1,
false,
@@ -206,20 +197,14 @@ impl Resolve<ExecuteArgs> for RunAction {
""
};
let cancel = CancellationToken::new();
action_cancel_cache()
.insert(update.id.clone(), cancel.clone())
.await;
let mut res = run_komodo_standard_command(
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action",
None,
format!(
"deno run --allow-all{https_cert_flag}{reload} {}",
path.display()
),
CommandOptions::default().cancel(cancel),
)
.await;
@@ -233,7 +218,7 @@ impl Resolve<ExecuteArgs> for RunAction {
update.logs.push(res);
update.finalize();
mogh_error::Ok(())
mogh_error::Ok(update)
}
.await;
@@ -246,13 +231,7 @@ impl Resolve<ExecuteArgs> for RunAction {
);
};
action_cancel_cache().remove(&update.id).await;
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
res?;
let update = res?;
// Need to manually update the update before cache refresh,
// and before broadcast with update_update.
@@ -480,65 +459,3 @@ fn parse_action_arguments(
.context("Failed to parse Json to action args"),
}
}
impl Resolve<ExecuteArgs> for CancelAction {
#[instrument(
"CancelAction",
skip_all,
fields(
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
action = self.action,
)
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let action = get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Execute.into(),
)
.await?;
let update_id = if let Some(update_id) = self.update_id
&& !update_id.is_empty()
{
update_id
} else {
db_client()
.updates
.find_one(doc! {
"target.type": "Action",
"target.id": &action.id,
"operation": Operation::RunAction.as_ref(),
"status": UpdateStatus::InProgress.as_ref(),
})
.await?
.context("No active run to cancel found")?
.id
};
action_cancel_cache()
.get(&update_id)
.await
.context("Action run cancel token not found")?
.cancel();
let mut update = update.clone();
update.push_simple_log(
"Cancel Triggered",
"The action cancel has been triggered.",
);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
-2
View File
@@ -114,8 +114,6 @@ impl Resolve<ExecuteArgs> for SendAlert {
) -> Result<Self::Response, Self::Error> {
let alerters = list_full_for_user::<Alerter>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+23 -36
View File
@@ -28,7 +28,7 @@ use komodo_client::{
alert::{Alert, AlertData, SeverityLevel},
all_logs_success,
build::{Build, BuildConfig},
builder::Builder,
builder::{Builder, BuilderConfig},
deployment::DeploymentState,
komodo_timestamp, optional_string,
permission::PermissionLevel,
@@ -78,7 +78,6 @@ impl Resolve<ExecuteArgs> for BatchRunBuild {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
@@ -86,12 +85,8 @@ impl Resolve<ExecuteArgs> for BatchRunBuild {
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunBuild>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchRunBuild>(&self.pattern, user)
.await?,
)
}
}
@@ -153,7 +148,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
// This will set action state back to default when dropped.
// Will also check to ensure build not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.building = true)?;
if build.config.auto_increment_version {
@@ -180,6 +175,9 @@ impl Resolve<ExecuteArgs> for RunBuild {
let builder =
resource::get::<Builder>(&build.config.builder_id).await?;
let is_server_builder =
matches!(&builder.config, BuilderConfig::Server(_));
tokio::spawn(async move {
let poll = async {
loop {
@@ -188,13 +186,19 @@ impl Resolve<ExecuteArgs> for RunBuild {
id = cancel_recv.recv() => id?
};
if incoming_build_id == build_id {
update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time.");
if is_server_builder {
update.push_error_log("Cancel acknowledged", "Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature.");
} else {
update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time.");
}
update.finalize();
let id = update.id.clone();
if let Err(e) = update_update(update).await {
warn!("Failed to modify Update {id} on db | {e:#}");
}
cancel_clone.cancel();
if !is_server_builder {
cancel_clone.cancel();
}
return Ok(());
}
}
@@ -212,7 +216,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
build.name.clone(),
Some(build.config.version),
builder,
Some(&mut update),
&mut update,
)
.await
{
@@ -269,11 +273,11 @@ impl Resolve<ExecuteArgs> for RunBuild {
replacers: Default::default(),
}) => res,
_ = cancel.cancelled() => {
debug!("Build cancelled during repo clone, cleaning up builder");
update.push_error_log("Build cancelled", String::from("Build cancelled during repo clone"));
debug!("Build cancelled during clone, cleaning up builder");
update.push_error_log("Build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
debug!("Builder cleaned up");
info!("Builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
@@ -319,14 +323,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
}) => res.context("Failed at call to Periphery to build"),
_ = cancel.cancelled() => {
info!("Build cancelled during build, cleaning up builder");
if let Err(e) = periphery.request(api::build::CancelBuild {
id: build.id.clone()
})
.await
.context("Failed to cancel build execution on Server") {
update.push_error_log("Cancel Build", format_serror(&e.into()));
}
update.push_error_log("Build Cancelled", String::from("User cancelled build during image build step"));
update.push_error_log("Build cancelled", String::from("User cancelled build during docker build"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
return handle_early_return(update, build.id, build.name, true).await
@@ -376,10 +373,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
@@ -568,8 +561,8 @@ impl Resolve<ExecuteArgs> for CancelBuild {
let mut update = update.clone();
update.push_simple_log(
"Cancel Triggered",
"The build cancel has been triggered",
"cancel triggered",
"the build cancel has been triggered",
);
update_update(update.clone()).await?;
@@ -624,13 +617,7 @@ async fn handle_post_build_redeploy(build_id: &str) {
let state = get_deployment_state(&deployment.id)
.await
.unwrap_or_default();
if ![
DeploymentState::NotDeployed,
DeploymentState::Exited,
DeploymentState::Unknown,
]
.contains(&state)
{
if state == DeploymentState::Running {
let req = super::ExecuteRequest::Deploy(Deploy {
deployment: deployment.id.clone(),
stop_signal: None,
+30 -93
View File
@@ -58,7 +58,6 @@ impl Resolve<ExecuteArgs> for BatchDeploy {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
@@ -66,12 +65,8 @@ impl Resolve<ExecuteArgs> for BatchDeploy {
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeploy>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchDeploy>(&self.pattern, user)
.await?,
)
}
}
@@ -115,7 +110,7 @@ impl Resolve<ExecuteArgs> for Deploy {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
@@ -127,16 +122,26 @@ impl Resolve<ExecuteArgs> for Deploy {
let (version, registry_token) = match &deployment.config.image {
DeploymentImage::Build { build_id, version } => {
let build = resource::get::<Build>(build_id).await?;
let image_names = build.get_image_names();
let image_name = image_names
.first()
.context("No image name could be created")
.context("Failed to create image name")?;
let version = if version.is_none() {
build.config.version
} else {
*version
};
let image_name = build.get_deployment_image_name();
let image_tag = build.get_deployment_image_tag(version);
let version_str = version.to_string();
// Potentially add the build image_tag postfix
let version_str = if build.config.image_tag.is_empty() {
version_str
} else {
format!("{version_str}-{}", build.config.image_tag)
};
// replace image with corresponding build image.
deployment.config.image = DeploymentImage::Image {
image: format!("{image_name}:{image_tag}"),
image: format!("{image_name}:{version_str}"),
};
let first_registry = build
.config
@@ -206,12 +211,6 @@ impl Resolve<ExecuteArgs> for Deploy {
update_update(update.clone()).await?;
let deployment_id = deployment.id.clone();
// Track the name the container / service is deployed under,
// so the Deployment stays matched to it even if the
// name configuration changes before the next deploy.
let fresh_name = deployment.custom_name().to_string();
let prev_deployed_name = deployment.info.deployed_name.clone();
let mut deployed = false;
match swarm_or_server {
SwarmOrServer::None => unreachable!(),
@@ -228,7 +227,6 @@ impl Resolve<ExecuteArgs> for Deploy {
{
Ok(logs) => {
refresh_swarm_cache(&swarm, true).await;
deployed = logs.iter().all(|log| log.success);
update.logs.extend(logs)
}
Err(e) => {
@@ -253,7 +251,6 @@ impl Resolve<ExecuteArgs> for Deploy {
{
Ok(log) => {
refresh_server_cache(&server, true).await;
deployed = log.success;
update.logs.push(log)
}
Err(e) => {
@@ -270,11 +267,6 @@ impl Resolve<ExecuteArgs> for Deploy {
&deployment_id,
&DeploymentInfo {
latest_image_digest: Default::default(),
deployed_name: if deployed {
fresh_name
} else {
prev_deployed_name
},
},
)
.await
@@ -286,11 +278,6 @@ impl Resolve<ExecuteArgs> for Deploy {
}
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -471,7 +458,7 @@ impl Resolve<ExecuteArgs> for PullDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
@@ -482,10 +469,6 @@ impl Resolve<ExecuteArgs> for PullDeployment {
update.logs.push(log);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -533,7 +516,7 @@ impl Resolve<ExecuteArgs> for StartDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.starting = true)?;
let mut update = update.clone();
@@ -544,7 +527,7 @@ impl Resolve<ExecuteArgs> for StartDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::StartContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
{
@@ -558,10 +541,6 @@ impl Resolve<ExecuteArgs> for StartDeployment {
update.logs.push(log);
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -609,7 +588,7 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.restarting = true)?;
let mut update = update.clone();
@@ -620,7 +599,7 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::RestartContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
{
@@ -636,10 +615,6 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
update.logs.push(log);
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -687,7 +662,7 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pausing = true)?;
let mut update = update.clone();
@@ -698,7 +673,7 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::PauseContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
{
@@ -712,10 +687,6 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
update.logs.push(log);
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -763,7 +734,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.unpausing = true)?;
let mut update = update.clone();
@@ -774,7 +745,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::UnpauseContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
{
@@ -790,10 +761,6 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
update.logs.push(log);
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -843,7 +810,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.stopping = true)?;
let mut update = update.clone();
@@ -854,7 +821,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::StopContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
@@ -876,10 +843,6 @@ impl Resolve<ExecuteArgs> for StopDeployment {
update.logs.push(log);
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -905,7 +868,6 @@ impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
@@ -915,7 +877,6 @@ impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
Ok(
super::batch_execute::<BatchDestroyDeployment>(
&self.pattern,
self.tags,
user,
)
.await?,
@@ -961,7 +922,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.destroying = true)?;
let mut update = update.clone();
@@ -975,7 +936,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
match swarm_request(
&swarm.config.server_ids,
api::swarm::RemoveSwarmServices {
services: vec![deployment.deployed_name().to_string()],
services: vec![deployment.name],
},
)
.await
@@ -996,7 +957,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
match periphery_client(&server)
.await?
.request(api::container::RemoveContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
@@ -1022,33 +983,9 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
}
};
// Clear the tracked deployed name once the container / service
// is confirmed removed, so the next deploy uses the fresh name.
if log.success
&& let Err(e) = resource::update_info::<Deployment>(
&deployment.id,
&DeploymentInfo {
latest_image_digest: deployment
.info
.latest_image_digest
.clone(),
deployed_name: Default::default(),
},
)
.await
{
warn!(
"Failed to clear deployment {} ({}) deployed name after destroy | {e:#}",
deployment.name, deployment.id,
);
}
update.logs.push(log);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
+2 -2
View File
@@ -1,7 +1,7 @@
use std::{fmt::Write as _, sync::OnceLock};
use anyhow::{Context, anyhow};
use command::{CommandOptions, run_komodo_standard_command};
use command::run_komodo_standard_command;
use database::{
bson::{Document, doc},
mungos::find::find_collect,
@@ -167,8 +167,8 @@ impl Resolve<ExecuteArgs> for BackupCoreDatabase {
let res = run_komodo_standard_command(
"Backup Core Database",
None,
"km database backup --yes",
CommandOptions::default(),
)
.await;
+3 -21
View File
@@ -13,7 +13,6 @@ use komodo_client::{
entities::{
Operation,
permission::PermissionLevel,
resource::ResourceQuery,
update::{Log, Update},
user::User,
},
@@ -30,10 +29,7 @@ use uuid::Uuid;
use crate::{
auth::KomodoAuthImpl,
helpers::{
query::get_all_tags,
update::{init_execution_update, update_update},
},
helpers::update::{init_execution_update, update_update},
resource::{KomodoResource, list_full_for_user_using_pattern},
state::db_client,
};
@@ -115,12 +111,10 @@ pub enum ExecuteRequest {
// ==== PROCEDURE ====
RunProcedure(RunProcedure),
BatchRunProcedure(BatchRunProcedure),
CancelProcedure(CancelProcedure),
// ==== ACTION ====
RunAction(RunAction),
BatchRunAction(BatchRunAction),
CancelAction(CancelAction),
// ==== SYNC ====
RunSync(RunSync),
@@ -362,26 +356,14 @@ trait BatchExecute {
#[instrument("BatchExecute", skip(user))]
async fn batch_execute<E: BatchExecute>(
pattern: &str,
tags: Vec<String>,
user: &User,
) -> anyhow::Result<BatchExecutionResponse> {
let all_tags = if tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let resources = list_full_for_user_using_pattern::<E::Resource>(
pattern,
ResourceQuery {
tags,
..Default::default()
},
None,
None,
Default::default(),
user,
PermissionLevel::Execute.into(),
&all_tags,
&[],
)
.await?;
+7 -70
View File
@@ -1,14 +1,12 @@
use std::pin::Pin;
use anyhow::Context as _;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
};
use formatting::{Color, bold, colored, format_serror, muted};
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunProcedure, CancelProcedure,
RunProcedure,
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
@@ -21,14 +19,13 @@ use komodo_client::{
};
use mogh_resolver::Resolve;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
helpers::{procedure::execute_procedure, update::update_update},
permission::get_check_permissions,
resource::refresh_procedure_state_cache,
state::{action_states, db_client, procedure_cancel_cache},
state::{action_states, db_client},
};
use super::{ExecuteArgs, ExecuteRequest};
@@ -51,12 +48,8 @@ impl Resolve<ExecuteArgs> for BatchRunProcedure {
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunProcedure>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchRunProcedure>(&self.pattern, user)
.await?,
)
}
}
@@ -124,22 +117,14 @@ fn resolve_inner(
// This will set action state back to default when dropped.
// Will also check to ensure procedure not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.running = true)?;
update_update(update.clone()).await?;
let cancel = CancellationToken::new();
procedure_cancel_cache()
.insert(procedure.id.clone(), cancel.clone())
.await;
let update = Mutex::new(update);
let res = execute_procedure(&procedure, &update, cancel).await;
procedure_cancel_cache().remove(&procedure.id).await;
let res = execute_procedure(&procedure, &update).await;
let mut update = update.into_inner();
@@ -155,15 +140,11 @@ fn resolve_inner(
);
}
Err(e) => update
.push_error_log("Execution error", format_serror(&e.into())),
.push_error_log("execution error", format_serror(&e.into())),
}
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
@@ -203,47 +184,3 @@ fn resolve_inner(
Ok(update)
})
}
impl Resolve<ExecuteArgs> for CancelProcedure {
#[instrument(
"CancelProcedure",
skip_all,
fields(
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
procedure = self.procedure,
)
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let procedure = get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Execute.into(),
)
.await?;
procedure_cancel_cache()
.get(&procedure.id)
.await
.context("Procedure run cancel token not found")?
.cancel();
let mut update = update.clone();
update.push_simple_log(
"Cancel Triggered",
"Procedure cancel has been triggered. The procedure will exit after the currently running stage is complete.",
);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
+11 -44
View File
@@ -58,7 +58,6 @@ impl Resolve<ExecuteArgs> for BatchCloneRepo {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
@@ -66,12 +65,8 @@ impl Resolve<ExecuteArgs> for BatchCloneRepo {
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchCloneRepo>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
.await?,
)
}
}
@@ -108,7 +103,7 @@ impl Resolve<ExecuteArgs> for CloneRepo {
// This will set action state back to default when dropped.
// Will also check to ensure repo not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.cloning = true)?;
let mut update = update.clone();
@@ -179,10 +174,6 @@ impl Resolve<ExecuteArgs> for CloneRepo {
);
};
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
handle_repo_update_return(update).await
}
}
@@ -201,8 +192,7 @@ impl Resolve<ExecuteArgs> for BatchPullRepo {
fields(
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
pattern = self.pattern
)
)]
async fn resolve(
@@ -210,12 +200,8 @@ impl Resolve<ExecuteArgs> for BatchPullRepo {
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullRepo>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
.await?,
)
}
}
@@ -252,7 +238,7 @@ impl Resolve<ExecuteArgs> for PullRepo {
// This will set action state back to default when dropped.
// Will also check to ensure repo not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
@@ -327,10 +313,6 @@ impl Resolve<ExecuteArgs> for PullRepo {
);
};
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
handle_repo_update_return(update).await
}
}
@@ -392,7 +374,6 @@ impl Resolve<ExecuteArgs> for BatchBuildRepo {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
@@ -400,12 +381,8 @@ impl Resolve<ExecuteArgs> for BatchBuildRepo {
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchBuildRepo>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
.await?,
)
}
}
@@ -446,7 +423,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
// This will set action state back to default when dropped.
// Will also check to ensure repo not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.building = true)?;
let mut update = update.clone();
@@ -513,7 +490,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
repo.name.clone(),
None,
builder,
Some(&mut update),
&mut update,
)
.await
{
@@ -524,9 +501,6 @@ impl Resolve<ExecuteArgs> for BuildRepo {
"get builder",
format_serror(&e.context("failed to get builder").into()),
));
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
return handle_builder_early_return(
update, repo.id, repo.name, false,
)
@@ -559,9 +533,6 @@ impl Resolve<ExecuteArgs> for BuildRepo {
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
return handle_builder_early_return(update, repo.id, repo.name, true).await
},
};
@@ -609,10 +580,6 @@ impl Resolve<ExecuteArgs> for BuildRepo {
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
+29 -134
View File
@@ -56,11 +56,8 @@ impl Resolve<ExecuteArgs> for StartContainer {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.starting_containers += 1,
|state| state.starting_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.starting_containers = true)?;
let mut update = update.clone();
@@ -86,10 +83,6 @@ impl Resolve<ExecuteArgs> for StartContainer {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -131,11 +124,8 @@ impl Resolve<ExecuteArgs> for RestartContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.restarting_containers += 1,
|state| state.restarting_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.restarting_containers = true)?;
let mut update = update.clone();
@@ -163,10 +153,6 @@ impl Resolve<ExecuteArgs> for RestartContainer {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -208,11 +194,8 @@ impl Resolve<ExecuteArgs> for PauseContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.pausing_containers += 1,
|state| state.pausing_containers -= 1,
false,
)?;
let _action_guard =
action_state.update(|state| state.pausing_containers = true)?;
let mut update = update.clone();
@@ -238,10 +221,6 @@ impl Resolve<ExecuteArgs> for PauseContainer {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -283,11 +262,8 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.unpausing_containers += 1,
|state| state.unpausing_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.unpausing_containers = true)?;
let mut update = update.clone();
@@ -315,10 +291,6 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -362,11 +334,8 @@ impl Resolve<ExecuteArgs> for StopContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.stopping_containers += 1,
|state| state.stopping_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.stopping_containers = true)?;
let mut update = update.clone();
@@ -394,10 +363,6 @@ impl Resolve<ExecuteArgs> for StopContainer {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -447,11 +412,8 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.destroying_containers += 1,
|state| state.destroying_containers -= 1,
false,
)?;
let _action_guard =
action_state.update(|state| state.pruning_containers = true)?;
let mut update = update.clone();
@@ -481,10 +443,6 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -525,11 +483,8 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.starting_containers += 1,
|state| state.starting_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.starting_containers = true)?;
let mut update = update.clone();
@@ -552,10 +507,6 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -596,11 +547,8 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.restarting_containers += 1,
|state| state.restarting_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.restarting_containers = true)?;
let mut update = update.clone();
@@ -625,10 +573,6 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -669,11 +613,8 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.pausing_containers += 1,
|state| state.pausing_containers -= 1,
false,
)?;
let _action_guard =
action_state.update(|state| state.pausing_containers = true)?;
let mut update = update.clone();
@@ -696,10 +637,6 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -740,11 +677,8 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.unpausing_containers += 1,
|state| state.unpausing_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.unpausing_containers = true)?;
let mut update = update.clone();
@@ -769,10 +703,6 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -813,11 +743,8 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.stopping_containers += 1,
|state| state.stopping_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.stopping_containers = true)?;
let mut update = update.clone();
@@ -840,10 +767,6 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -884,7 +807,7 @@ impl Resolve<ExecuteArgs> for PruneContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_containers = true)?;
let mut update = update.clone();
@@ -913,10 +836,6 @@ impl Resolve<ExecuteArgs> for PruneContainers {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1022,7 +941,7 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_networks = true)?;
let mut update = update.clone();
@@ -1049,10 +968,6 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1155,7 +1070,7 @@ impl Resolve<ExecuteArgs> for PruneImages {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_images = true)?;
let mut update = update.clone();
@@ -1180,10 +1095,6 @@ impl Resolve<ExecuteArgs> for PruneImages {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1289,7 +1200,7 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_volumes = true)?;
let mut update = update.clone();
@@ -1314,10 +1225,6 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1358,7 +1265,7 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_builders = true)?;
let mut update = update.clone();
@@ -1383,10 +1290,6 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1427,7 +1330,7 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_buildx = true)?;
let mut update = update.clone();
@@ -1452,10 +1355,6 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1496,7 +1395,7 @@ impl Resolve<ExecuteArgs> for PruneSystem {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_system = true)?;
let mut update = update.clone();
@@ -1520,10 +1419,6 @@ impl Resolve<ExecuteArgs> for PruneSystem {
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
+11 -44
View File
@@ -72,7 +72,6 @@ impl Resolve<ExecuteArgs> for BatchDeployStack {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
@@ -80,12 +79,8 @@ impl Resolve<ExecuteArgs> for BatchDeployStack {
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStack>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchDeployStack>(&self.pattern, user)
.await?,
)
}
}
@@ -136,7 +131,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
// Will check to ensure stack not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
@@ -330,10 +325,6 @@ impl Resolve<ExecuteArgs> for DeployStack {
}
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -358,7 +349,6 @@ impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
@@ -368,7 +358,6 @@ impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
Ok(
super::batch_execute::<BatchDeployStackIfChanged>(
&self.pattern,
self.tags,
user,
)
.await?,
@@ -788,7 +777,6 @@ impl Resolve<ExecuteArgs> for BatchPullStack {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
@@ -796,12 +784,8 @@ impl Resolve<ExecuteArgs> for BatchPullStack {
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullStack>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchPullStack>(&self.pattern, user)
.await?,
)
}
}
@@ -959,7 +943,7 @@ impl Resolve<ExecuteArgs> for PullStack {
// Will check to ensure stack not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
@@ -976,10 +960,6 @@ impl Resolve<ExecuteArgs> for PullStack {
update.logs.extend(res.logs);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1173,20 +1153,15 @@ impl Resolve<ExecuteArgs> for BatchDestroyStack {
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(
&self.pattern,
self.tags,
user,
)
.await
.map_err(Into::into)
super::batch_execute::<BatchDestroyStack>(&self.pattern, user)
.await
.map_err(Into::into)
}
}
@@ -1239,7 +1214,7 @@ impl Resolve<ExecuteArgs> for DestroyStack {
// Will check to ensure stack not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.destroying = true)?;
let mut update = update.clone();
@@ -1269,10 +1244,6 @@ impl Resolve<ExecuteArgs> for DestroyStack {
refresh_swarm_cache(&swarm, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1343,7 +1314,7 @@ impl Resolve<ExecuteArgs> for RunStackService {
let action_state =
action_states().stack.get_or_insert_default(&stack.id).await;
let action_guard =
let _action_guard =
action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
@@ -1403,10 +1374,6 @@ impl Resolve<ExecuteArgs> for RunStackService {
update.logs.push(log);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
+1 -9
View File
@@ -98,7 +98,7 @@ impl Resolve<ExecuteArgs> for RunSync {
// This will set action state back to default when dropped.
// Will also check to ensure sync not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.syncing = true)?;
let mut update = update.clone();
@@ -311,10 +311,6 @@ impl Resolve<ExecuteArgs> for RunSync {
),
);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
return Ok(update);
}
@@ -446,10 +442,6 @@ impl Resolve<ExecuteArgs> for RunSync {
}
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
+4 -12
View File
@@ -1,5 +1,6 @@
use std::sync::Arc;
use anyhow::anyhow;
use axum::{Router, http::HeaderMap};
use komodo_client::entities::resource::Resource;
use mogh_cache::CloneCache;
@@ -40,21 +41,12 @@ trait VerifySecret {
/// Implemented on the integration struct, eg [integrations::github::Github]
trait ExtractBranch {
fn extract_branch(body: &str) -> anyhow::Result<String>;
/// Whether the webhook body's branch matches `expected`.
/// A mismatch is routine and only logged at debug; errors
/// only when the branch cannot be extracted from the body.
fn branch_matches(
body: &str,
expected: &str,
) -> anyhow::Result<bool> {
fn verify_branch(body: &str, expected: &str) -> anyhow::Result<()> {
let branch = Self::extract_branch(body)?;
if branch == expected {
Ok(true)
Ok(())
} else {
debug!(
"Ignoring webhook | push to branch '{branch}' does not match expected branch '{expected}'"
);
Ok(false)
Err(anyhow!("request branch does not match expected"))
}
}
}
+23 -81
View File
@@ -1,4 +1,4 @@
use std::{str::FromStr, sync::OnceLock, time::Duration};
use std::{str::FromStr, sync::OnceLock};
use anyhow::{Context, anyhow};
use komodo_client::{
@@ -23,7 +23,6 @@ use crate::{
},
helpers::update::init_execution_update,
resource,
state::action_states,
};
use super::{ANY_BRANCH, ListenerLockCache};
@@ -38,6 +37,11 @@ impl super::CustomSecret for Build {
}
}
fn build_locks() -> &'static ListenerLockCache {
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
BUILD_LOCKS.get_or_init(Default::default)
}
pub async fn handle_build_webhook<B: super::ExtractBranch>(
build: Build,
body: String,
@@ -46,6 +50,12 @@ pub async fn handle_build_webhook<B: super::ExtractBranch>(
return Ok(());
}
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = build_locks().get_or_insert_default(&build.id).await;
let _lock = lock.lock().await;
// Use the correct target branch when using linked repo.
let branch = if build.config.linked_repo.is_empty() {
build.config.branch
@@ -57,49 +67,15 @@ pub async fn handle_build_webhook<B: super::ExtractBranch>(
.branch
};
if !B::branch_matches(&body, &branch)? {
return Ok(());
}
// Cancel if currently building
if action_states()
.build
.get(&build.id)
.await
.and_then(|states| {
states.get().ok().map(|states| states.building)
})
.unwrap_or_default()
{
let user = git_webhook_user().to_owned();
let cancel = ExecuteRequest::CancelBuild(CancelBuild {
build: build.id.clone(),
});
let update = init_execution_update(&cancel, &user).await?;
let ExecuteRequest::CancelBuild(cancel) = cancel else {
unreachable!()
};
cancel
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
})
.await
.ok();
poll_build_until_cancelled(&build.id).await?;
}
B::verify_branch(&body, &branch)?;
let user = git_webhook_user().to_owned();
let run = ExecuteRequest::RunBuild(RunBuild { build: build.id });
let update = init_execution_update(&run, &user).await?;
let ExecuteRequest::RunBuild(run) = run else {
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunBuild(req) = req else {
unreachable!()
};
run
req
.resolve(&ExecuteArgs {
user,
update,
@@ -107,32 +83,9 @@ pub async fn handle_build_webhook<B: super::ExtractBranch>(
})
.await
.map_err(|e| e.error)?;
Ok(())
}
async fn poll_build_until_cancelled(
build_id: &String,
) -> anyhow::Result<()> {
let action_states = action_states();
// Poll to ensure cancelled
for _ in 0..10 {
if !action_states
.build
.get(build_id)
.await
.and_then(|states| {
states.get().ok().map(|states| states.building)
})
.unwrap_or_default()
{
return Ok(());
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
Err(anyhow!("Build still running after cancel"))
}
// ======
// REPO
// ======
@@ -267,9 +220,7 @@ async fn handle_repo_webhook_inner<
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
if !B::branch_matches(&body, &repo.config.branch)? {
return Ok(());
}
B::verify_branch(&body, &repo.config.branch)?;
E::resolve(repo).await
}
@@ -400,9 +351,7 @@ pub async fn handle_stack_webhook_inner<
.branch
};
if !B::branch_matches(&body, &branch)? {
return Ok(());
}
B::verify_branch(&body, &branch)?;
E::resolve(stack).await.map_err(|e| e.error)
}
@@ -515,9 +464,7 @@ async fn handle_sync_webhook_inner<
.branch
};
if !B::branch_matches(&body, &branch)? {
return Ok(());
}
B::verify_branch(&body, &branch)?;
E::resolve(sync).await
}
@@ -554,10 +501,8 @@ pub async fn handle_procedure_webhook<B: super::ExtractBranch>(
procedure_locks().get_or_insert_default(&procedure.id).await;
let _lock = lock.lock().await;
if target_branch != ANY_BRANCH
&& !B::branch_matches(&body, target_branch)?
{
return Ok(());
if target_branch != ANY_BRANCH {
B::verify_branch(&body, target_branch)?;
}
let user = git_webhook_user().to_owned();
@@ -612,10 +557,7 @@ pub async fn handle_action_webhook<B: super::ExtractBranch>(
let branch = B::extract_branch(&body)?;
if target_branch != ANY_BRANCH && branch != target_branch {
debug!(
"Ignoring webhook | push to branch '{branch}' does not match expected branch '{target_branch}'"
);
return Ok(());
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
+12 -61
View File
@@ -3,8 +3,7 @@ use komodo_client::{
api::read::*,
entities::{
action::{
Action, ActionActionState, ActionListItem, ActionSortBy,
ActionState,
Action, ActionActionState, ActionListItem, ActionState,
},
permission::PermissionLevel,
},
@@ -12,13 +11,13 @@ use komodo_client::{
use mogh_resolver::Resolve;
use crate::{
helpers::query::{get_action_state, get_all_tags},
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_state_cache, action_states},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAction {
async fn resolve(
@@ -46,45 +45,15 @@ impl Resolve<ReadArgs> for ListActions {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<ActionListItem> =
match self.sort_by {
ActionSortBy::Name => resource::ListItemSort::Name,
ActionSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
ActionSortBy::NextRun => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.next_scheduled_run
.cmp(&b.info.next_scheduled_run)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let actions = resource::list_items_for_user::<Action>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|action| {
states.is_empty() || states.contains(&action.info.state)
},
Ok(
resource::list_for_user::<Action>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(actions)
}
}
@@ -98,28 +67,12 @@ impl Resolve<ReadArgs> for ListFullActions {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Action, _>(
resource::list_full_for_user::<Action>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|action| {
let states = states.clone();
async move {
if states.is_empty()
|| states.contains(&get_action_state(&action.id).await)
{
Some(action)
} else {
None
}
}
},
)
.await?,
)
@@ -154,8 +107,6 @@ impl Resolve<ReadArgs> for GetActionsSummary {
) -> mogh_error::Result<GetActionsSummaryResponse> {
let actions = resource::list_full_for_user::<Action>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+1 -1
View File
@@ -40,7 +40,7 @@ impl Resolve<ReadArgs> for ListAlerts {
FindOptions::builder()
.sort(doc! { "ts": -1 })
.limit(NUM_ALERTS_PER_PAGE as i64)
.skip(self.page.saturating_mul(NUM_ALERTS_PER_PAGE))
.skip(self.page * NUM_ALERTS_PER_PAGE)
.build(),
)
.await
+3 -26
View File
@@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc;
use komodo_client::{
api::read::*,
entities::{
alerter::{Alerter, AlerterListItem, AlerterSortBy},
alerter::{Alerter, AlerterListItem},
permission::PermissionLevel,
},
};
@@ -17,7 +17,7 @@ use crate::{
state::db_client,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAlerter {
async fn resolve(
@@ -45,30 +45,12 @@ impl Resolve<ReadArgs> for ListAlerters {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<AlerterListItem> =
match self.sort_by {
AlerterSortBy::Name => resource::ListItemSort::Name,
AlerterSortBy::Type => {
resource::ListItemSort::DbField("config.endpoint.type")
}
AlerterSortBy::Enabled => {
resource::ListItemSort::DbField("config.enabled")
}
};
Ok(
resource::list_items_for_user::<Alerter>(
resource::list_for_user::<Alerter>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|_| true,
)
.await?,
)
@@ -85,12 +67,9 @@ impl Resolve<ReadArgs> for ListFullAlerters {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user::<Alerter>(
self.query,
limit as i64,
self.page.saturating_mul(limit),
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -106,8 +85,6 @@ impl Resolve<ReadArgs> for GetAlertersSummary {
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetAlertersSummaryResponse> {
let query = match list_resource_ids_for_user::<Alerter>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
+12 -68
View File
@@ -11,9 +11,7 @@ use komodo_client::{
api::read::*,
entities::{
Operation,
build::{
Build, BuildActionState, BuildListItem, BuildSortBy, BuildState,
},
build::{Build, BuildActionState, BuildListItem, BuildState},
permission::PermissionLevel,
update::UpdateStatus,
},
@@ -27,7 +25,7 @@ use crate::{
state::{action_states, build_state_cache, db_client},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuild {
async fn resolve(
@@ -55,47 +53,15 @@ impl Resolve<ReadArgs> for ListBuilds {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<BuildListItem> =
match self.sort_by {
BuildSortBy::Name => resource::ListItemSort::Name,
BuildSortBy::Source => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.files_on_host
.cmp(&b.info.files_on_host)
.then_with(|| {
a.info.linked_repo_name.cmp(&b.info.linked_repo_name)
})
.then_with(|| a.info.repo.cmp(&b.info.repo))
.then_with(|| a.name.cmp(&b.name))
}))
}
BuildSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let builds = resource::list_items_for_user::<Build>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|build| states.is_empty() || states.contains(&build.info.state),
Ok(
resource::list_for_user::<Build>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(builds)
}
}
@@ -109,29 +75,12 @@ impl Resolve<ReadArgs> for ListFullBuilds {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Build, _>(
resource::list_full_for_user::<Build>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|build| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&resource::get_build_state(&build.id).await)
{
Some(build)
} else {
None
}
}
},
)
.await?,
)
@@ -166,8 +115,6 @@ impl Resolve<ReadArgs> for GetBuildsSummary {
) -> mogh_error::Result<GetBuildsSummaryResponse> {
let builds = resource::list_full_for_user::<Build>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
@@ -217,8 +164,7 @@ impl Resolve<ReadArgs> for GetBuildMonthlyStats {
let curr_ts = unix_timestamp_ms() as i64;
let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;
let close_ts =
next_day - (self.page as i64).saturating_mul(30 * ONE_DAY_MS);
let close_ts = next_day - self.page as i64 * 30 * ONE_DAY_MS;
let open_ts = close_ts - 30 * ONE_DAY_MS;
let mut build_updates = db_client()
@@ -335,8 +281,6 @@ impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
};
let builds = resource::list_full_for_user::<Build>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
+3 -31
View File
@@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc;
use komodo_client::{
api::read::*,
entities::{
builder::{Builder, BuilderListItem, BuilderSortBy},
builder::{Builder, BuilderListItem},
permission::PermissionLevel,
},
};
@@ -17,7 +17,7 @@ use crate::{
state::db_client,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuilder {
async fn resolve(
@@ -45,35 +45,12 @@ impl Resolve<ReadArgs> for ListBuilders {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<BuilderListItem> =
match self.sort_by {
BuilderSortBy::Name => resource::ListItemSort::Name,
BuilderSortBy::Provider => {
resource::ListItemSort::DbField("config.type")
}
BuilderSortBy::InstanceType => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.instance_type
.cmp(&b.info.instance_type)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
Ok(
resource::list_items_for_user::<Builder>(
resource::list_for_user::<Builder>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|_| true,
)
.await?,
)
@@ -90,12 +67,9 @@ impl Resolve<ReadArgs> for ListFullBuilders {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user::<Builder>(
self.query,
limit as i64,
self.page.saturating_mul(limit),
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -111,8 +85,6 @@ impl Resolve<ReadArgs> for GetBuildersSummary {
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetBuildersSummaryResponse> {
let query = match list_resource_ids_for_user::<Builder>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
+29 -99
View File
@@ -6,8 +6,8 @@ use komodo_client::{
entities::{
SwarmOrServer,
deployment::{
Deployment, DeploymentActionState, DeploymentListItem,
DeploymentSortBy, DeploymentState,
Deployment, DeploymentActionState, DeploymentConfig,
DeploymentListItem, DeploymentState,
},
docker::{
container::{Container, ContainerStats},
@@ -25,9 +25,7 @@ use reqwest::StatusCode;
use crate::{
helpers::{
periphery_client,
query::{get_all_tags, get_deployment_state},
swarm::swarm_request,
periphery_client, query::get_all_tags, swarm::swarm_request,
},
permission::get_check_permissions,
resource::{self, setup_deployment_execution},
@@ -36,7 +34,7 @@ use crate::{
},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetDeployment {
async fn resolve(
@@ -65,66 +63,21 @@ impl Resolve<ReadArgs> for ListDeployments {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<DeploymentListItem> =
match self.sort_by {
DeploymentSortBy::Name => resource::ListItemSort::Name,
DeploymentSortBy::Image => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.image
.cmp(&b.info.image)
.then_with(|| a.name.cmp(&b.name))
}))
}
DeploymentSortBy::Host => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
let host_a = if a.info.swarm_id.is_empty() {
&a.info.server_name
} else {
&a.info.swarm_name
};
let host_b = if b.info.swarm_id.is_empty() {
&b.info.server_name
} else {
&b.info.swarm_name
};
host_a.cmp(host_b).then_with(|| a.name.cmp(&b.name))
}))
}
DeploymentSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| {
// Use ! with update available to order 'true' first
(!a.info.update_available)
.cmp(&!b.info.update_available)
})
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let deployments = resource::list_items_for_user::<Deployment>(
let deployments = resource::list_for_user::<Deployment>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|deployment| {
(!only_update_available || deployment.info.update_available)
&& (states.is_empty()
|| states.contains(&deployment.info.state))
},
)
.await?;
let deployments = if only_update_available {
deployments
.into_iter()
.filter(|deployment| deployment.info.update_available)
.collect()
} else {
deployments
};
Ok(deployments)
}
}
@@ -139,32 +92,12 @@ impl Resolve<ReadArgs> for ListFullDeployments {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Deployment, _>(
resource::list_full_for_user::<Deployment>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|deployment| {
let states = states.clone();
async move {
if states.is_empty()
|| states.contains(
&get_deployment_state(&deployment.id)
.await
.unwrap_or_default(),
)
{
Some(deployment)
} else {
None
}
}
},
)
.await?,
)
@@ -221,7 +154,7 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
SwarmOrServer::Swarm(swarm) => swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLog {
service: deployment.deployed_name().to_string(),
service: deployment.name,
tail,
timestamps,
no_task_ids: false,
@@ -234,7 +167,7 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(api::container::GetContainerLog {
name: deployment.deployed_name().to_string(),
name: deployment.name,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
@@ -273,7 +206,7 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
SwarmOrServer::Swarm(swarm) => swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLogSearch {
service: deployment.deployed_name().to_string(),
service: deployment.name,
terms,
combinator,
invert,
@@ -288,7 +221,7 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(api::container::GetContainerLogSearch {
name: deployment.deployed_name().to_string(),
name: deployment.name,
terms,
combinator,
invert,
@@ -341,7 +274,7 @@ impl Resolve<ReadArgs> for InspectDeploymentContainer {
periphery_client(&server)
.await?
.request(InspectContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
.context("Failed to inspect container on server")
@@ -374,7 +307,7 @@ impl Resolve<ReadArgs> for InspectDeploymentSwarmService {
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmService {
service: deployment.deployed_name().to_string(),
service: deployment.name,
},
)
.await
@@ -388,24 +321,25 @@ impl Resolve<ReadArgs> for GetDeploymentStats {
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ContainerStats> {
let deployment = get_check_permissions::<Deployment>(
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?;
if deployment.config.server_id.is_empty() {
if server_id.is_empty() {
return Err(
anyhow!("Deployment has no Server attached").into(),
anyhow!("deployment has no server attached").into(),
);
}
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)
.await?
.request(api::container::GetContainerStats {
name: deployment.deployed_name().to_string(),
})
.request(api::container::GetContainerStats { name })
.await
.context("failed to get stats from periphery")?;
Ok(res)
@@ -440,8 +374,6 @@ impl Resolve<ReadArgs> for GetDeploymentsSummary {
) -> mogh_error::Result<GetDeploymentsSummaryResponse> {
let deployments = resource::list_full_for_user::<Deployment>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
@@ -490,8 +422,6 @@ impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
};
let deployments = resource::list_full_for_user::<Deployment>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
-590
View File
@@ -1,590 +0,0 @@
use std::cmp;
use anyhow::{Context as _, anyhow};
use database::bson::doc;
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{
Container, ContainerListItem, ContainerSortBy,
ContainerStateStatusEnum,
},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
},
permission::PermissionLevel,
server::{Server, ServerQuery, ServerState},
stack::{Stack, StackServiceNames},
update::Log,
},
};
use mogh_resolver::Resolve;
use periphery_client::api as periphery;
use crate::{
api::read::{ReadArgs, list_limit},
helpers::{periphery_client, query::get_all_tags},
permission::{get_check_permissions, list_resources_for_user},
resource,
stack::compose_container_match_regex,
state::{db_client, server_status_cache},
};
impl Resolve<ReadArgs> for GetContainersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetContainersSummaryResponse> {
let servers = resource::list_full_for_user::<Server>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
)
.await
.context("failed to get servers from db")?;
let mut res = GetContainersSummaryResponse::default();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
for container in &docker.containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
| ContainerStateStatusEnum::Paused
| ContainerStateStatusEnum::Exited => res.stopped += 1,
ContainerStateStatusEnum::Running => res.running += 1,
ContainerStateStatusEnum::Empty => res.unknown += 1,
_ => res.unhealthy += 1,
}
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for ListAllContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListAllContainersResponse> {
let all_tags = if self.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let servers = resource::list_for_user::<Server>(
ServerQuery::builder()
.names(self.servers.clone())
.tags(self.tags)
.build(),
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?;
let mut containers = Vec::<ContainerListItem>::new();
let limit = list_limit(self.limit);
// Match terms case insensitively.
let terms = self
.terms
.iter()
.map(|term| term.to_lowercase())
.collect::<Vec<_>>();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
let Some(docker) = &cache.docker else {
continue;
};
containers.extend(
docker
.containers
.iter()
.filter(|container| {
// Apply state filter if defined.
(self.state.is_empty() || self.state.contains(&container.state)) &&
// Apply terms filter if defined
(terms.is_empty()
// Match when all terms contained within a name.
|| {
let name = container.name.to_lowercase();
terms.iter().all(|term| name.contains(term))
})
})
.cloned(),
);
}
// The containers all come from the in memory status cache,
// so all matching containers are collected and sorted
// before applying pagination.
let compare = |a: &ContainerListItem, b: &ContainerListItem| {
match self.sort_by {
ContainerSortBy::Name => a.name.cmp(&b.name),
ContainerSortBy::Server => a.server_name.cmp(&b.server_name),
ContainerSortBy::State => a.state.cmp(&b.state),
ContainerSortBy::Image => a.image.cmp(&b.image),
ContainerSortBy::Networks => {
a.networks.first().cmp(&b.networks.first())
}
ContainerSortBy::Ports => a
.ports
.first()
.map(|port| port.private_port)
.cmp(&b.ports.first().map(|port| port.private_port)),
ContainerSortBy::Volumes => {
a.volumes.first().cmp(&b.volumes.first())
}
}
// Fall back to name based sorting for equal sort keys.
// Inside `compare`, so descending sorts are fully descending,
// matching the List<Resource> apis.
.then_with(|| a.name.cmp(&b.name))
};
if self.sort_desc {
containers.sort_by(|a, b| compare(b, a));
} else {
containers.sort_by(|a, b| compare(a, b));
}
let skip = limit.saturating_mul(self.page) as usize;
let take = if limit == 0 {
usize::MAX
} else {
limit as usize
};
Ok(containers.into_iter().skip(skip).take(take).collect())
}
}
impl Resolve<ReadArgs> for ListContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListContainersResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.containers.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Container> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.inspect(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::container::InspectContainer {
name: self.container,
})
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetResourceMatchingContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetResourceMatchingContainerResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
// First check deployments with a matching deployed / custom container name.
// The empty check is required to avoid matching
// deployments with no custom name configured.
if !self.container.is_empty()
&& let Ok(Some(deployment)) = db_client()
.deployments
.find_one(doc! {
"$or": [
{ "info.deployed_name": &self.container },
{ "config.custom_name": &self.container },
]
})
.await
{
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(),
});
}
// Then check deployments matching by name
if let Ok(deployment) =
resource::get::<Deployment>(&self.container).await
&& deployment.custom_name() == self.container
{
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(),
});
}
// then check stacks
let stacks = list_resources_for_user::<Stack>(
doc! { "config.server_id": &server.id },
None,
None,
user,
PermissionLevel::Read.into(),
)
.await?;
// check matching stack
for stack in stacks {
for StackServiceNames {
service_name,
container_name,
..
} in stack
.info
.deployed_services
.unwrap_or(stack.info.latest_services)
{
let is_match = match compose_container_match_regex(&container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Err(e) => {
warn!("{e:#}");
continue;
}
}.is_match(&self.container);
if is_match {
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Stack(stack.id).into(),
});
}
}
}
Ok(GetResourceMatchingContainerResponse { resource: None })
}
}
const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<ReadArgs> for GetContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Log> {
let GetContainerLog {
server,
container,
tail,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLog {
name: container,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for SearchContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Log> {
let SearchContainerLog {
server,
container,
terms,
combinator,
invert,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLogSearch {
name: container,
terms,
combinator,
invert,
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListComposeProjects {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListComposeProjectsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.projects.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for ListNetworks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListNetworksResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.networks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectNetwork {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Network> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect network: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::docker::InspectNetwork {
name: self.network,
})
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListImages {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListImagesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.images.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectImage {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Image> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect image: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::docker::InspectImage { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListImageHistory {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Vec<ImageHistoryResponseItem>> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot get image history: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::docker::ImageHistory { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListVolumes {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListVolumesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.volumes.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectVolume {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Volume> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect volume: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::docker::InspectVolume { name: self.volume })
.await?;
Ok(res)
}
}
+40 -75
View File
@@ -10,7 +10,7 @@ use komodo_client::{
ResourceTarget,
build::Build,
builder::{Builder, BuilderConfig},
config::{GitProvider, ImageRegistry},
config::{DockerRegistry, GitProvider},
permission::PermissionLevel,
repo::Repo,
server::Server,
@@ -44,7 +44,6 @@ mod alerter;
mod build;
mod builder;
mod deployment;
mod docker;
mod onboarding_key;
mod permission;
mod procedure;
@@ -67,12 +66,6 @@ pub struct ReadArgs {
pub user: User,
}
/// Resolve the page limit for List apis, falling back to the
/// configured `default_pagination_limit` when not provided.
fn list_limit(limit: Option<u64>) -> u64 {
limit.unwrap_or(core_config().default_pagination_limit)
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
@@ -87,8 +80,7 @@ enum ReadRequest {
GetCoreInfo(GetCoreInfo),
ListSecrets(ListSecrets),
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
#[serde(alias = "ListDockerRegistriesFromConfig")]
ListImageRegistriesFromConfig(ListImageRegistriesFromConfig),
ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),
// ==== SWARM ====
GetSwarmsSummary(GetSwarmsSummary),
@@ -125,33 +117,22 @@ enum ReadRequest {
// ==== TERMINAL ====
ListTerminals(ListTerminals),
// ==== CONTAINER ====
#[serde(alias = "GetDockerContainersSummary")]
GetContainersSummary(GetContainersSummary),
#[serde(alias = "ListAllDockerContainers")]
ListAllContainers(ListAllContainers),
#[serde(alias = "ListDockerContainers")]
ListContainers(ListContainers),
#[serde(alias = "InspectDockerContainer")]
InspectContainer(InspectContainer),
// ==== DOCKER ====
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
InspectDockerContainer(InspectDockerContainer),
GetResourceMatchingContainer(GetResourceMatchingContainer),
GetContainerLog(GetContainerLog),
SearchContainerLog(SearchContainerLog),
ListComposeProjects(ListComposeProjects),
#[serde(alias = "ListDockerNetworks")]
ListNetworks(ListNetworks),
#[serde(alias = "InspectDockerNetwork")]
InspectNetwork(InspectNetwork),
#[serde(alias = "ListDockerImages")]
ListImages(ListImages),
#[serde(alias = "InspectDockerImage")]
InspectImage(InspectImage),
#[serde(alias = "ListDockerImageHistory")]
ListImageHistory(ListImageHistory),
#[serde(alias = "ListDockerVolumes")]
ListVolumes(ListVolumes),
#[serde(alias = "InspectDockerVolume")]
InspectVolume(InspectVolume),
ListDockerNetworks(ListDockerNetworks),
InspectDockerNetwork(InspectDockerNetwork),
ListDockerImages(ListDockerImages),
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
ListDockerVolumes(ListDockerVolumes),
InspectDockerVolume(InspectDockerVolume),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
@@ -170,7 +151,6 @@ enum ReadRequest {
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListAllStackServices(ListAllStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
@@ -278,10 +258,8 @@ enum ReadRequest {
// ==== PROVIDER ====
GetGitProviderAccount(GetGitProviderAccount),
ListGitProviderAccounts(ListGitProviderAccounts),
#[serde(alias = "GetDockerRegistryAccount")]
GetImageRegistryAccount(GetImageRegistryAccount),
#[serde(alias = "ListDockerRegistryAccounts")]
ListImageRegistryAccounts(ListImageRegistryAccounts),
GetDockerRegistryAccount(GetDockerRegistryAccount),
ListDockerRegistryAccounts(ListDockerRegistryAccounts),
// ==== ONBOARDING KEY ====
ListOnboardingKeys(ListOnboardingKeys),
@@ -376,7 +354,6 @@ impl Resolve<ReadArgs> for GetCoreInfo {
disable_websocket_reconnect: config.disable_websocket_reconnect,
enable_fancy_toml: config.enable_fancy_toml,
timezone: config.timezone.clone(),
default_pagination_limit: config.default_pagination_limit,
public_key: core_keys().load().public.to_string(),
};
Ok(info)
@@ -402,9 +379,7 @@ impl Resolve<ReadArgs> for ListSecrets {
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => None,
BuilderConfig::Server(config) => {
config.server_ids.first().cloned()
}
BuilderConfig::Server(config) => Some(config.server_id),
BuilderConfig::Aws(config) => {
secrets.extend(config.secrets);
None
@@ -459,13 +434,11 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
if let Some(server_id) = config.server_ids.first() {
merge_git_providers_for_server(
&mut providers,
server_id,
)
.await?;
}
merge_git_providers_for_server(
&mut providers,
&config.server_id,
)
.await?;
}
BuilderConfig::Aws(config) => {
merge_git_providers(
@@ -487,24 +460,18 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
let (builds, repos, syncs) = tokio::try_join!(
resource::list_full_for_user::<Build>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[]
),
resource::list_full_for_user::<Repo>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[]
),
resource::list_full_for_user::<ResourceSync>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[]
@@ -556,35 +523,33 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
//
impl Resolve<ReadArgs> for ListImageRegistriesFromConfig {
impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
async fn resolve(
self,
_: &ReadArgs,
) -> mogh_error::Result<ListImageRegistriesFromConfigResponse> {
let mut registries = core_config().image_registries.clone();
) -> mogh_error::Result<ListDockerRegistriesFromConfigResponse> {
let mut registries = core_config().docker_registries.clone();
if let Some(target) = self.target {
match target {
ResourceTarget::Server(id) => {
merge_image_registries_for_server(&mut registries, &id)
merge_docker_registries_for_server(&mut registries, &id)
.await?;
}
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
if let Some(server_id) = config.server_ids.first() {
merge_image_registries_for_server(
&mut registries,
server_id,
)
.await?;
}
merge_docker_registries_for_server(
&mut registries,
&config.server_id,
)
.await?;
}
BuilderConfig::Aws(config) => {
merge_image_registries(
merge_docker_registries(
&mut registries,
config.image_registries,
config.docker_registries,
);
}
}
@@ -642,14 +607,14 @@ fn merge_git_providers(
}
}
async fn merge_image_registries_for_server(
registries: &mut Vec<ImageRegistry>,
async fn merge_docker_registries_for_server(
registries: &mut Vec<DockerRegistry>,
server_id: &str,
) -> mogh_error::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)
.await?
.request(periphery_client::api::ListImageRegistries {})
.request(periphery_client::api::ListDockerRegistries {})
.await
.with_context(|| {
format!(
@@ -657,13 +622,13 @@ async fn merge_image_registries_for_server(
server.name
)
})?;
merge_image_registries(registries, more);
merge_docker_registries(registries, more);
Ok(())
}
fn merge_image_registries(
registries: &mut Vec<ImageRegistry>,
more: Vec<ImageRegistry>,
fn merge_docker_registries(
registries: &mut Vec<DockerRegistry>,
more: Vec<DockerRegistry>,
) {
for incoming_registry in more {
if let Some(registry) = registries
+12 -63
View File
@@ -3,21 +3,19 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
procedure::{
Procedure, ProcedureListItem, ProcedureSortBy, ProcedureState,
},
procedure::{Procedure, ProcedureState},
},
};
use mogh_resolver::Resolve;
use crate::{
helpers::query::{get_all_tags, get_procedure_state},
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_states, procedure_state_cache},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetProcedure {
async fn resolve(
@@ -45,45 +43,15 @@ impl Resolve<ReadArgs> for ListProcedures {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<ProcedureListItem> =
match self.sort_by {
ProcedureSortBy::Name => resource::ListItemSort::Name,
ProcedureSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
ProcedureSortBy::NextRun => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.next_scheduled_run
.cmp(&b.info.next_scheduled_run)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let procedures = resource::list_items_for_user::<Procedure>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|procedure| {
states.is_empty() || states.contains(&procedure.info.state)
},
Ok(
resource::list_for_user::<Procedure>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(procedures)
}
}
@@ -97,29 +65,12 @@ impl Resolve<ReadArgs> for ListFullProcedures {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Procedure, _>(
resource::list_full_for_user::<Procedure>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|procedure| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&get_procedure_state(&procedure.id).await)
{
Some(procedure)
} else {
None
}
}
},
)
.await?,
)
@@ -133,8 +84,6 @@ impl Resolve<ReadArgs> for GetProceduresSummary {
) -> mogh_error::Result<GetProceduresSummaryResponse> {
let procedures = resource::list_full_for_user::<Procedure>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+4 -4
View File
@@ -61,11 +61,11 @@ impl Resolve<ReadArgs> for ListGitProviderAccounts {
}
}
impl Resolve<ReadArgs> for GetImageRegistryAccount {
impl Resolve<ReadArgs> for GetDockerRegistryAccount {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetImageRegistryAccountResponse> {
) -> mogh_error::Result<GetDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")
@@ -83,11 +83,11 @@ impl Resolve<ReadArgs> for GetImageRegistryAccount {
}
}
impl Resolve<ReadArgs> for ListImageRegistryAccounts {
impl Resolve<ReadArgs> for ListDockerRegistryAccounts {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListImageRegistryAccountsResponse> {
) -> mogh_error::Result<ListDockerRegistryAccountsResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")
+11 -58
View File
@@ -3,9 +3,7 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
repo::{
Repo, RepoActionState, RepoListItem, RepoSortBy, RepoState,
},
repo::{Repo, RepoActionState, RepoListItem, RepoState},
},
};
use mogh_resolver::Resolve;
@@ -17,7 +15,7 @@ use crate::{
state::{action_states, repo_state_cache},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetRepo {
async fn resolve(
@@ -45,41 +43,15 @@ impl Resolve<ReadArgs> for ListRepos {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<RepoListItem> =
match self.sort_by {
RepoSortBy::Name => resource::ListItemSort::Name,
RepoSortBy::Repo => {
resource::ListItemSort::DbField("config.repo")
}
RepoSortBy::Branch => {
resource::ListItemSort::DbField("config.branch")
}
RepoSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let repos = resource::list_items_for_user::<Repo>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|repo| states.is_empty() || states.contains(&repo.info.state),
Ok(
resource::list_for_user::<Repo>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(repos)
}
}
@@ -93,29 +65,12 @@ impl Resolve<ReadArgs> for ListFullRepos {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Repo, _>(
resource::list_full_for_user::<Repo>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|repo| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&resource::get_repo_state(&repo.id).await)
{
Some(repo)
} else {
None
}
}
},
)
.await?,
)
@@ -150,8 +105,6 @@ impl Resolve<ReadArgs> for GetReposSummary {
) -> mogh_error::Result<GetReposSummaryResponse> {
let repos = resource::list_full_for_user::<Repo>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+15 -68
View File
@@ -1,15 +1,13 @@
use std::cmp::Ordering;
use futures_util::future::join_all;
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
action::{Action, ActionQuerySpecifics},
action::Action,
permission::PermissionLevel,
procedure::{Procedure, ProcedureQuerySpecifics},
procedure::Procedure,
resource::{ResourceQuery, TemplatesQueryBehavior},
schedule::{Schedule, ScheduleSortBy},
schedule::Schedule,
},
};
use mogh_resolver::Resolve;
@@ -20,7 +18,7 @@ use crate::{
schedule::get_schedule_item_info,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for ListSchedules {
async fn resolve(
@@ -31,36 +29,24 @@ impl Resolve<ReadArgs> for ListSchedules {
let (actions, procedures) = tokio::try_join!(
list_full_for_user::<Action>(
ResourceQuery {
names: Default::default(),
templates: TemplatesQueryBehavior::Include,
tag_behavior: self.tag_behavior,
tags: self.tags.clone(),
terms: self.terms.clone(),
specific: ActionQuerySpecifics {
scheduled: Some(true),
..Default::default()
},
..Default::default()
specific: Default::default(),
},
None,
None,
&args.user,
PermissionLevel::Read.into(),
&all_tags,
),
list_full_for_user::<Procedure>(
ResourceQuery {
names: Default::default(),
templates: TemplatesQueryBehavior::Include,
tag_behavior: self.tag_behavior,
tags: self.tags.clone(),
terms: self.terms,
specific: ProcedureQuerySpecifics {
scheduled: Some(true),
..Default::default()
},
..Default::default()
specific: Default::default(),
},
None,
None,
&args.user,
PermissionLevel::Read.into(),
&all_tags,
@@ -110,51 +96,12 @@ impl Resolve<ReadArgs> for ListSchedules {
let (actions, procedures) =
tokio::join!(join_all(actions), join_all(procedures));
// The terms / scheduled filters are already applied
// at the db level by the queries above.
let mut schedules =
actions.into_iter().chain(procedures).collect::<Vec<_>>();
// The schedules are composed in memory across resource types,
// so all matching schedules are collected and sorted
// before applying pagination.
// All comparators fall back to name based sorting for equal
// sort keys, inside `compare`, so descending sorts are fully
// descending, matching the List<Resource> apis.
let compare: fn(&Schedule, &Schedule) -> Ordering =
match self.sort_by {
ScheduleSortBy::Name => |a, b| a.name.cmp(&b.name),
ScheduleSortBy::Schedule => |a, b| {
a.schedule
.cmp(&b.schedule)
.then_with(|| a.name.cmp(&b.name))
},
// Order unscheduled (None) last, matching the UI.
ScheduleSortBy::NextRun => |a, b| {
(a.next_scheduled_run.is_none(), a.next_scheduled_run)
.cmp(&(
b.next_scheduled_run.is_none(),
b.next_scheduled_run,
))
.then_with(|| a.name.cmp(&b.name))
},
ScheduleSortBy::Enabled => |a, b| {
a.enabled.cmp(&b.enabled).then_with(|| a.name.cmp(&b.name))
},
};
if self.sort_desc {
schedules.sort_by(|a, b| compare(b, a));
} else {
schedules.sort_by(compare);
}
let limit = list_limit(self.limit);
let skip = limit.saturating_mul(self.page) as usize;
let take = if limit == 0 {
usize::MAX
} else {
limit as usize
};
Ok(schedules.into_iter().skip(skip).take(take).collect())
Ok(
actions
.into_iter()
.chain(procedures)
.filter(|s| !s.schedule.is_empty())
.collect(),
)
}
}
+546 -113
View File
@@ -1,5 +1,5 @@
use std::{
cmp::Ordering,
cmp,
collections::HashMap,
sync::{Arc, OnceLock},
};
@@ -15,31 +15,47 @@ use database::mungos::{
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{
Container, ContainerListItem, ContainerStateStatusEnum,
},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
},
permission::PermissionLevel,
server::{
Server, ServerActionState, ServerListItem, ServerSortBy,
Server, ServerActionState, ServerListItem, ServerQuery,
ServerState,
},
stats::{MinimalSystemStats, SystemInformation, SystemProcess},
stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
update::Log,
},
};
use mogh_error::AddStatusCode;
use mogh_resolver::Resolve;
use periphery_client::api as periphery;
use periphery_client::api::{
self as periphery,
container::InspectContainer,
docker::{
ImageHistory, InspectImage, InspectNetwork, InspectVolume,
},
};
use reqwest::StatusCode;
use tokio::sync::Mutex;
use crate::{
helpers::{
periphery_client,
query::{get_all_tags, get_cached_server_state},
},
permission::get_check_permissions,
helpers::{periphery_client, query::get_all_tags},
permission::{get_check_permissions, list_resources_for_user},
resource,
stack::compose_container_match_regex,
state::{action_states, db_client, server_status_cache},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetServersSummary {
async fn resolve(
@@ -48,8 +64,6 @@ impl Resolve<ReadArgs> for GetServersSummary {
) -> mogh_error::Result<GetServersSummaryResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
@@ -111,89 +125,15 @@ impl Resolve<ReadArgs> for ListServers {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<ServerListItem> = match self
.sort_by
{
ServerSortBy::Name => resource::ListItemSort::Name,
ServerSortBy::Region => {
resource::ListItemSort::DbField("config.region")
}
ServerSortBy::Version => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.version
.cmp(&b.info.version)
.then_with(|| a.name.cmp(&b.name))
}))
}
ServerSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
ServerSortBy::Cpu => stats_sort(|stats| stats.cpu_perc as f64),
ServerSortBy::Memory => stats_sort(|stats| {
usage_percent(stats.mem_used_gb, stats.mem_total_gb)
}),
ServerSortBy::Disk => stats_sort(|stats| {
usage_percent(stats.disk_used_gb, stats.disk_total_gb)
}),
ServerSortBy::LoadAverage => {
stats_sort(|stats| stats.load_average.one)
}
ServerSortBy::Network => stats_sort(|stats| {
stats.network_ingress_bytes + stats.network_egress_bytes
}),
};
let servers = resource::list_items_for_user::<Server>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|server| {
states.is_empty() || states.contains(&server.info.state)
},
Ok(
resource::list_for_user::<Server>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(servers)
}
}
/// Build an in memory sort on the list item stats,
/// matching the stats displayed on the server stats table.
/// Servers without stats (unreachable / disabled) order last.
fn stats_sort(
metric: fn(&MinimalSystemStats) -> f64,
) -> resource::ListItemSort<ServerListItem> {
resource::ListItemSort::InMemory(Box::new(move |a, b| {
match (a.info.stats.as_ref(), b.info.stats.as_ref()) {
(Some(a_stats), Some(b_stats)) => {
metric(a_stats).total_cmp(&metric(b_stats))
}
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => Ordering::Equal,
}
.then_with(|| a.name.cmp(&b.name))
}))
}
fn usage_percent(used: f64, total: f64) -> f64 {
if total > 0.0 {
100.0 * used / total
} else {
0.0
}
}
@@ -207,29 +147,12 @@ impl Resolve<ReadArgs> for ListFullServers {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Server, _>(
resource::list_full_for_user::<Server>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|server| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&get_cached_server_state(&server.id).await)
{
Some(server)
} else {
None
}
}
},
)
.await?,
)
@@ -416,7 +339,7 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
let curr_ts = unix_timestamp_ms() as i64;
let mut curr_ts = curr_ts
- curr_ts % granularity
- granularity * (page as i64).saturating_mul(STATS_PER_PAGE);
- granularity * STATS_PER_PAGE * page as i64;
for _ in 0..STATS_PER_PAGE {
ts_vec.push(curr_ts);
curr_ts -= granularity;
@@ -430,6 +353,7 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
},
FindOptions::builder()
.sort(doc! { "ts": -1 })
.skip(page as u64 * STATS_PER_PAGE as u64)
.limit(STATS_PER_PAGE)
.build(),
)
@@ -444,3 +368,512 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListDockerContainersResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.containers.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for ListAllDockerContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListAllDockerContainersResponse> {
let servers = resource::list_for_user::<Server>(
ServerQuery::builder().names(self.servers.clone()).build(),
user,
PermissionLevel::Read.into(),
&[],
)
.await?;
let mut containers = Vec::<ContainerListItem>::new();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
let Some(docker) = &cache.docker else {
continue;
};
let more = docker
.containers
.iter()
.filter(|container| {
self.containers.is_empty()
|| self.containers.contains(&container.name)
})
.cloned();
containers.extend(more);
}
Ok(containers)
}
}
impl Resolve<ReadArgs> for GetDockerContainersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetDockerContainersSummaryResponse> {
let servers = resource::list_full_for_user::<Server>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
.context("failed to get servers from db")?;
let mut res = GetDockerContainersSummaryResponse::default();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
for container in &docker.containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
| ContainerStateStatusEnum::Paused
| ContainerStateStatusEnum::Exited => res.stopped += 1,
ContainerStateStatusEnum::Running => res.running += 1,
ContainerStateStatusEnum::Empty => res.unknown += 1,
_ => res.unhealthy += 1,
}
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectDockerContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Container> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.inspect(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(InspectContainer {
name: self.container,
})
.await?;
Ok(res)
}
}
const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<ReadArgs> for GetContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Log> {
let GetContainerLog {
server,
container,
tail,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLog {
name: container,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for SearchContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Log> {
let SearchContainerLog {
server,
container,
terms,
combinator,
invert,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLogSearch {
name: container,
terms,
combinator,
invert,
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetResourceMatchingContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetResourceMatchingContainerResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
// first check deployments
if let Ok(deployment) =
resource::get::<Deployment>(&self.container).await
{
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(),
});
}
// then check stacks
let stacks = list_resources_for_user::<Stack>(
doc! { "config.server_id": &server.id },
user,
PermissionLevel::Read.into(),
)
.await?;
// check matching stack
for stack in stacks {
for StackServiceNames {
service_name,
container_name,
..
} in stack
.info
.deployed_services
.unwrap_or(stack.info.latest_services)
{
let is_match = match compose_container_match_regex(&container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Err(e) => {
warn!("{e:#}");
continue;
}
}.is_match(&self.container);
if is_match {
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Stack(stack.id).into(),
});
}
}
}
Ok(GetResourceMatchingContainerResponse { resource: None })
}
}
impl Resolve<ReadArgs> for ListDockerNetworks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListDockerNetworksResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.networks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectDockerNetwork {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Network> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect network: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(InspectNetwork { name: self.network })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerImages {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListDockerImagesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.images.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectDockerImage {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Image> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect image: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(InspectImage { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerImageHistory {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Vec<ImageHistoryResponseItem>> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot get image history: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(ImageHistory { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerVolumes {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListDockerVolumesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.volumes.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectDockerVolume {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Volume> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect volume: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(InspectVolume { name: self.volume })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListComposeProjects {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListComposeProjectsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.projects.clone())
} else {
Ok(Vec::new())
}
}
}
// impl Resolve<ReadArgs> for ListAllTerminals {
// async fn resolve(
// self,
// args: &ReadArgs,
// ) -> Result<Self::Response, Self::Error> {
// // match self.tar
// let mut terminals = resource::list_full_for_user::<Server>(
// self.query, &args.user, &all_tags,
// )
// .await?
// .into_iter()
// .map(|server| async move {
// (
// list_terminals_inner(&server, self.fresh).await,
// (server.id, server.name),
// )
// })
// .collect::<FuturesUnordered<_>>()
// .collect::<Vec<_>>()
// .await
// .into_iter()
// .flat_map(|(terminals, server)| {
// let terminals = terminals.ok()?;
// Some((terminals, server))
// })
// .flat_map(|(terminals, (server_id, server_name))| {
// terminals.into_iter().map(move |info| {
// TerminalInfoWithServer::from_terminal_info(
// &server_id,
// &server_name,
// info,
// )
// })
// })
// .collect::<Vec<_>>();
// terminals.sort_by(|a, b| {
// a.server_name.cmp(&b.server_name).then(a.name.cmp(&b.name))
// });
// Ok(terminals)
// }
// }
+61 -205
View File
@@ -9,10 +9,7 @@ use komodo_client::{
container::Container, service::SwarmService, stack::SwarmStack,
},
permission::PermissionLevel,
stack::{
Stack, StackActionState, StackListItem, StackQuery,
StackService, StackSortBy, StackState,
},
stack::{Stack, StackActionState, StackListItem, StackState},
},
};
use mogh_error::AddStatusCodeError as _;
@@ -25,9 +22,7 @@ use reqwest::StatusCode;
use crate::{
helpers::{
periphery_client,
query::{get_all_tags, get_cached_stack_state},
swarm::swarm_request,
periphery_client, query::get_all_tags, swarm::swarm_request,
},
permission::get_check_permissions,
resource,
@@ -35,7 +30,7 @@ use crate::{
state::{action_states, stack_status_cache},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetStack {
async fn resolve(
@@ -53,128 +48,6 @@ impl Resolve<ReadArgs> for GetStack {
}
}
impl Resolve<ReadArgs> for ListStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Vec<StackListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<StackListItem> =
match self.sort_by {
StackSortBy::Name => resource::ListItemSort::Name,
StackSortBy::Source => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.files_on_host
.cmp(&b.info.files_on_host)
.then_with(|| {
a.info.linked_repo_name.cmp(&b.info.linked_repo_name)
})
.then_with(|| a.info.repo.cmp(&b.info.repo))
.then_with(|| a.name.cmp(&b.name))
}))
}
StackSortBy::Host => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
let host_a = if a.info.swarm_id.is_empty() {
&a.info.server_name
} else {
&a.info.swarm_name
};
let host_b = if b.info.swarm_id.is_empty() {
&b.info.server_name
} else {
&b.info.swarm_name
};
host_a.cmp(host_b).then_with(|| a.name.cmp(&b.name))
}))
}
StackSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| {
// Use ! with update available to order 'true' first
(!a.info.update_available())
.cmp(&!b.info.update_available())
})
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let stacks = resource::list_items_for_user::<Stack>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|stack| {
(!only_update_available
|| stack
.info
.services
.iter()
.any(|service| service.update_available))
&& (states.is_empty() || states.contains(&stack.info.state))
},
)
.await?;
Ok(stacks)
}
}
impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListFullStacksResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Stack, _>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|stack| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&get_cached_stack_state(&stack.id).await)
{
Some(stack)
} else {
None
}
}
},
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListStackServices {
async fn resolve(
self,
@@ -199,75 +72,6 @@ impl Resolve<ReadArgs> for ListStackServices {
}
}
impl Resolve<ReadArgs> for ListAllStackServices {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListStackServicesResponse> {
let all_tags = if self.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks = resource::list_for_user::<Stack>(
StackQuery::builder()
.names(self.stacks.clone())
.tags(self.tags)
.build(),
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?;
let mut services = Vec::<StackService>::new();
let mut skipped = 0;
let limit = list_limit(self.limit);
let limit_usize = limit as usize;
// Eg. page 1 skips until after 100 services, page 2 after 200.
let skip = limit.saturating_mul(self.page);
// Match terms case insensitively.
let terms = self
.terms
.iter()
.map(|term| term.to_lowercase())
.collect::<Vec<_>>();
for stack in stacks {
let cache =
stack_status_cache().get_or_insert_default(&stack.id).await;
let more = cache.curr.services
.iter()
.filter(|service| {
// Apply state filter if defined.
(self.state.is_empty() || self.state.contains(&service.state)) &&
// Apply terms filter if defined
(terms.is_empty()
// Match when all terms contained within a name.
|| {
let name = service.service.to_lowercase();
terms.iter().all(|term| name.contains(term))
})
});
for service in more {
if skipped < skip {
skipped += 1;
} else {
// push and maybe early return
services.push(service.clone());
if limit > 0 && services.len() >= limit_usize {
return Ok(services);
}
}
}
}
Ok(services)
}
}
impl Resolve<ReadArgs> for GetStackLog {
async fn resolve(
self,
@@ -535,8 +339,6 @@ impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
};
let stacks = resource::list_full_for_user::<Stack>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -571,8 +373,6 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
};
let stacks = resource::list_full_for_user::<Stack>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -595,6 +395,64 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
}
}
impl Resolve<ReadArgs> for ListStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Vec<StackListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let stacks = resource::list_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?;
let stacks = if only_update_available {
stacks
.into_iter()
.filter(|stack| {
stack
.info
.services
.iter()
.any(|service| service.update_available)
})
.collect()
} else {
stacks
};
Ok(stacks)
}
}
impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListFullStacksResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetStackActionState {
async fn resolve(
self,
@@ -623,8 +481,6 @@ impl Resolve<ReadArgs> for GetStacksSummary {
) -> mogh_error::Result<GetStacksSummaryResponse> {
let stacks = resource::list_full_for_user::<Stack>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+3 -30
View File
@@ -3,9 +3,7 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
swarm::{
Swarm, SwarmActionState, SwarmListItem, SwarmSortBy, SwarmState,
},
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState},
},
};
use mogh_resolver::Resolve;
@@ -17,7 +15,7 @@ use crate::{
state::{action_states, server_status_cache, swarm_status_cache},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetSwarm {
async fn resolve(
@@ -45,32 +43,12 @@ impl Resolve<ReadArgs> for ListSwarms {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<SwarmListItem> =
match self.sort_by {
SwarmSortBy::Name => resource::ListItemSort::Name,
SwarmSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
Ok(
resource::list_items_for_user::<Swarm>(
resource::list_for_user::<Swarm>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|_| true,
)
.await?,
)
@@ -87,12 +65,9 @@ impl Resolve<ReadArgs> for ListFullSwarms {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user::<Swarm>(
self.query,
limit as i64,
self.page.saturating_mul(limit),
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -130,8 +105,6 @@ impl Resolve<ReadArgs> for GetSwarmsSummary {
) -> mogh_error::Result<GetSwarmsSummaryResponse> {
let swarms = resource::list_full_for_user::<Swarm>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+2 -48
View File
@@ -5,7 +5,6 @@ use komodo_client::{
permission::PermissionLevel,
sync::{
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
ResourceSyncSortBy,
},
},
};
@@ -16,7 +15,7 @@ use crate::{
resource, state::action_states,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetResourceSync {
async fn resolve(
@@ -44,52 +43,12 @@ impl Resolve<ReadArgs> for ListResourceSyncs {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<ResourceSyncListItem> =
match self.sort_by {
ResourceSyncSortBy::Name => resource::ListItemSort::Name,
ResourceSyncSortBy::Source => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.files_on_host
.cmp(&b.info.files_on_host)
.then_with(|| {
a.info.linked_repo_name.cmp(&b.info.linked_repo_name)
})
.then_with(|| a.info.repo.cmp(&b.info.repo))
.then_with(|| a.name.cmp(&b.name))
}))
}
ResourceSyncSortBy::Branch => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.branch
.cmp(&b.info.branch)
.then_with(|| a.name.cmp(&b.name))
}))
}
ResourceSyncSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
Ok(
resource::list_items_for_user::<ResourceSync>(
resource::list_for_user::<ResourceSync>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|_| true,
)
.await?,
)
@@ -106,12 +65,9 @@ impl Resolve<ReadArgs> for ListFullResourceSyncs {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user::<ResourceSync>(
self.query,
limit as i64,
self.page.saturating_mul(limit),
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -150,8 +106,6 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
let resource_syncs =
resource::list_full_for_user::<ResourceSync>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+56 -118
View File
@@ -1,5 +1,3 @@
use std::cmp::Ordering;
use anyhow::Context as _;
use futures_util::{
FutureExt, StreamExt as _, stream::FuturesUnordered,
@@ -11,7 +9,7 @@ use komodo_client::{
permission::PermissionLevel,
server::Server,
stack::Stack,
terminal::{Terminal, TerminalSortBy, TerminalTarget},
terminal::{Terminal, TerminalTarget},
user::User,
},
};
@@ -24,7 +22,7 @@ use crate::{
resource,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
//
@@ -33,111 +31,57 @@ impl Resolve<ReadArgs> for ListTerminals {
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListTerminalsResponse> {
let mut terminals = match self.target {
None => {
list_all_terminals_for_user(user, self.use_names).await?
let Some(target) = self.target else {
return list_all_terminals_for_user(user, self.use_names).await;
};
match &target {
TerminalTarget::Server { server } => {
let server = server
.as_ref()
.context("Must provide 'target.params.server'")
.status_code(StatusCode::BAD_REQUEST)?;
let server = get_check_permissions::<Server>(
server,
user,
PermissionLevel::Read.terminal(),
)
.await?;
list_terminals_on_server(&server, Some(target)).await
}
TerminalTarget::Container { server, .. } => {
let server = get_check_permissions::<Server>(
server,
user,
PermissionLevel::Read.terminal(),
)
.await?;
list_terminals_on_server(&server, Some(target)).await
}
TerminalTarget::Stack { stack, .. } => {
let server = get_check_permissions::<Stack>(
stack,
user,
PermissionLevel::Read.terminal(),
)
.await?
.config
.server_id;
let server = resource::get::<Server>(&server).await?;
list_terminals_on_server(&server, Some(target)).await
}
TerminalTarget::Deployment { deployment } => {
let server = get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Read.terminal(),
)
.await?
.config
.server_id;
let server = resource::get::<Server>(&server).await?;
list_terminals_on_server(&server, Some(target)).await
}
Some(target) => match &target {
TerminalTarget::Server { server } => {
let server = server
.as_ref()
.context("Must provide 'target.params.server'")
.status_code(StatusCode::BAD_REQUEST)?;
let server = get_check_permissions::<Server>(
server,
user,
PermissionLevel::Read.terminal(),
)
.await?;
list_terminals_on_server(&server, Some(target)).await?
}
TerminalTarget::Container { server, .. } => {
let server = get_check_permissions::<Server>(
server,
user,
PermissionLevel::Read.terminal(),
)
.await?;
list_terminals_on_server(&server, Some(target)).await?
}
TerminalTarget::Stack { stack, .. } => {
let server = get_check_permissions::<Stack>(
stack,
user,
PermissionLevel::Read.terminal(),
)
.await?
.config
.server_id;
let server = resource::get::<Server>(&server).await?;
list_terminals_on_server(&server, Some(target)).await?
}
TerminalTarget::Deployment { deployment } => {
let server = get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Read.terminal(),
)
.await?
.config
.server_id;
let server = resource::get::<Server>(&server).await?;
list_terminals_on_server(&server, Some(target)).await?
}
},
};
// The terminals come from Periphery agents rather than the db,
// so the terms filter / sort / pagination are applied in memory.
if !self.terms.is_empty() {
let terms = self
.terms
.iter()
.map(|term| term.to_lowercase())
.collect::<Vec<_>>();
terminals.retain(|terminal| {
let name = terminal.name.to_lowercase();
terms.iter().all(|term| name.contains(term))
});
}
// All comparators fall back to name based sorting for equal
// sort keys, inside `compare`, so descending sorts are fully
// descending, matching the List<Resource> apis.
let compare: fn(&Terminal, &Terminal) -> Ordering =
match self.sort_by {
TerminalSortBy::Name => |a, b| a.name.cmp(&b.name),
TerminalSortBy::Target => |a, b| {
a.target.cmp(&b.target).then_with(|| a.name.cmp(&b.name))
},
TerminalSortBy::Command => |a, b| {
a.command.cmp(&b.command).then_with(|| a.name.cmp(&b.name))
},
TerminalSortBy::Size => |a, b| {
a.stored_size_kb
.total_cmp(&b.stored_size_kb)
.then_with(|| a.name.cmp(&b.name))
},
TerminalSortBy::Created => |a, b| {
a.created_at
.cmp(&b.created_at)
.then_with(|| a.name.cmp(&b.name))
},
};
if self.sort_desc {
terminals.sort_by(|a, b| compare(b, a));
} else {
terminals.sort_by(compare);
}
let limit = list_limit(self.limit);
let skip = limit.saturating_mul(self.page) as usize;
let take = if limit == 0 {
usize::MAX
} else {
limit as usize
};
Ok(terminals.into_iter().skip(skip).take(take).collect())
}
}
@@ -148,8 +92,6 @@ async fn list_all_terminals_for_user(
let (mut servers, stacks, deployments) = tokio::try_join!(
resource::list_full_for_user::<Server>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.terminal(),
&[]
@@ -161,16 +103,12 @@ async fn list_all_terminals_for_user(
.collect::<Vec<_>>())),
resource::list_full_for_user::<Stack>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.terminal(),
&[]
),
resource::list_full_for_user::<Deployment>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.terminal(),
&[]
@@ -201,7 +139,7 @@ async fn list_all_terminals_for_user(
}
}
let terminals = servers
let mut terminals = servers
.into_iter()
.map(|(server, server_permission)| async move {
(
@@ -230,7 +168,6 @@ async fn list_all_terminals_for_user(
server_id.clone()
}),
};
terminal.target_name = Some(server_name.clone());
terminal
}),
TerminalTarget::Container { container, .. } => {
@@ -243,7 +180,6 @@ async fn list_all_terminals_for_user(
},
container,
};
terminal.target_name = Some(server_name.clone());
terminal
})
}
@@ -257,7 +193,6 @@ async fn list_all_terminals_for_user(
},
service,
};
terminal.target_name = Some(s.name.clone());
terminal
})
}
@@ -271,7 +206,6 @@ async fn list_all_terminals_for_user(
d.id.clone()
},
};
terminal.target_name = Some(d.name.clone());
terminal
},
)
@@ -286,6 +220,10 @@ async fn list_all_terminals_for_user(
.flatten()
.collect::<Vec<_>>();
terminals.sort_by(|a, b| {
a.target.cmp(&b.target).then(a.name.cmp(&b.name))
});
Ok(terminals)
}
+2 -5
View File
@@ -28,7 +28,6 @@ use crate::{
resource,
state::db_client,
sync::{
replace_ids::ReplaceIds,
toml::{ToToml, convert_resource},
user_groups::{convert_user_groups, user_group_to_toml},
variables::variable_to_toml,
@@ -55,8 +54,6 @@ async fn get_all_targets(
targets.extend(
resource::list_full_for_user::<$Type>(
ResourceQuery::builder().tags(tags).build(),
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -145,7 +142,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
PermissionLevel::Read.into(),
)
.await?;
$Type::replace_ids(&mut resource.config);
$Type::replace_ids(&mut resource);
let (deploy, after) = existing
.as_ref()
.and_then(|e| {
@@ -203,7 +200,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
{
ResourceSync::replace_ids(&mut sync.config);
ResourceSync::replace_ids(&mut sync);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
+1 -3
View File
@@ -47,9 +47,7 @@ impl Resolve<ReadArgs> for ListUpdates {
query,
FindOptions::builder()
.sort(doc! { "start_ts": -1 })
.skip(
(self.page as u64).saturating_mul(UPDATES_PER_PAGE as u64),
)
.skip(self.page as u64 * UPDATES_PER_PAGE as u64)
.limit(UPDATES_PER_PAGE)
.build(),
)
+49 -12
View File
@@ -1,8 +1,10 @@
use std::path::PathBuf;
use std::{path::PathBuf, time::Duration};
use anyhow::{Context, anyhow};
use database::mongo_indexed::doc;
use database::mungos::mongodb::bson::to_document;
use database::{
mongo_indexed::doc, mungos::mongodb::bson::oid::ObjectId,
};
use formatting::format_serror;
use komodo_client::{
api::write::*,
@@ -13,6 +15,7 @@ use komodo_client::{
builder::{Builder, BuilderConfig},
permission::PermissionLevel,
repo::Repo,
server::ServerState,
update::Update,
},
};
@@ -22,11 +25,12 @@ use periphery_client::api::build::{
};
use tokio::fs;
use crate::helpers::builder::connect_builder_periphery;
use crate::{
config::core_config,
connection::PeripheryConnectionArgs,
helpers::{
git_token,
git_token, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
periphery::PeripheryClient,
@@ -472,15 +476,48 @@ async fn get_on_host_periphery(
.await
.context("Failed to get builder")?;
if let BuilderConfig::Aws(_) = builder.config {
return Err(anyhow!(
"Files on host doesn't work with AWS builder"
));
match builder.config {
BuilderConfig::Aws(_) => {
Err(anyhow!("Files on host doesn't work with AWS builder"))
}
BuilderConfig::Url(config) => {
// TODO: Ensure connection is actually established.
// Builder id no good because it may be active for multiple connections.
let periphery = PeripheryClient::new(
PeripheryConnectionArgs::from_url_builder(
&ObjectId::new().to_hex(),
&config,
),
config.insecure_tls,
)
.await?;
// Poll for connection to be estalished
let mut err = None;
for _ in 0..10 {
tokio::time::sleep(Duration::from_secs(1)).await;
match periphery.health_check().await {
Ok(_) => return Ok(periphery),
Err(e) => err = Some(e),
};
}
Err(err.context("Missing error")?)
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
return Err(anyhow!(
"Builder is type server, but has no server attached"
));
}
let (server, state) =
get_server_with_state(&config.server_id).await?;
if state != ServerState::Ok {
return Err(anyhow!(
"Builder server is disabled or not reachable"
));
};
periphery_client(&server).await
}
}
connect_builder_periphery(build.name.clone(), None, builder, None)
.await
.map(|(periphery, _)| periphery)
}
/// The successful case will be included as Some(remote_contents).
+11 -65
View File
@@ -15,7 +15,6 @@ use komodo_client::{
docker::container::RestartPolicyNameEnum,
komodo_timestamp, optional_string,
permission::PermissionLevel,
resource::ResourceQuery,
server::{Server, ServerState},
to_container_compatible_name,
update::Update,
@@ -31,9 +30,7 @@ use crate::{
api::execute::{self, ExecuteRequest, ExecutionResult},
helpers::{
periphery_client,
query::{
get_all_tags, get_deployment_state, get_swarm_or_server,
},
query::{get_deployment_state, get_swarm_or_server},
registry_token,
update::{add_update, make_update, poll_update_until_complete},
},
@@ -303,24 +300,10 @@ impl Resolve<WriteArgs> for RenameDeployment {
let name = to_container_compatible_name(&self.name);
let state = get_deployment_state(&deployment.id).await?;
let container_state =
get_deployment_state(&deployment.id).await?;
// When no custom name is configured, the container / service
// follows the Deployment name, and must be kept matched:
// - Server mode: rename the container to the new name.
// - Swarm mode: services cannot be renamed, so the current
// service name is pinned in info.deployed_name, and stays
// in use until the next deploy recreates the service.
// A configured custom name is unaffected by Deployment rename.
let follows_name =
deployment.config.custom_name.trim().is_empty();
let deployed = state != DeploymentState::NotDeployed;
let rename_container = follows_name
&& deployed
&& !deployment.config.server_id.is_empty();
if rename_container && state == DeploymentState::Unknown {
if container_state == DeploymentState::Unknown {
return Err(
anyhow!(
"Cannot rename Deployment when container status is unknown"
@@ -329,51 +312,27 @@ impl Resolve<WriteArgs> for RenameDeployment {
);
}
// Includes the Unknown state, in case a service
// is still running under the current name.
let pin_deployed_name = follows_name
&& deployed
&& !deployment.config.swarm_id.is_empty()
&& deployment.info.deployed_name.is_empty();
let mut update =
make_update(&deployment, Operation::RenameDeployment, user);
let mut set =
doc! { "name": &name, "updated_at": komodo_timestamp() };
if rename_container {
// Container is renamed to the new name below.
set.insert("info.deployed_name", name.as_str());
} else if pin_deployed_name {
set.insert("info.deployed_name", deployment.deployed_name());
}
update_one_by_id(
&db_client().deployments,
&deployment.id,
database::mungos::update::Update::Set(set),
database::mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("Failed to update Deployment name on db")?;
if pin_deployed_name {
update.push_simple_log(
"Pin Service Name",
format!(
"Swarm services cannot be renamed, so the current service name '{}' stays in use until the next deploy recreates the service under the new name.",
deployment.deployed_name()
),
);
}
if rename_container {
if container_state != DeploymentState::NotDeployed {
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
let log = periphery_client(&server)
.await?
.request(api::container::RenameContainer {
curr_name: deployment.deployed_name().to_string(),
curr_name: deployment.name.clone(),
new_name: name.clone(),
})
.await
@@ -506,7 +465,6 @@ pub async fn check_deployment_for_update_inner(
&deployment.id,
&DeploymentInfo {
latest_image_digest: latest_digest.clone(),
deployed_name: deployment.info.deployed_name.clone(),
},
)
.await?;
@@ -663,7 +621,6 @@ impl Resolve<WriteArgs> for BatchCheckDeploymentForUpdate {
fields(
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
skip_auto_update = self.skip_auto_update,
wait_for_auto_update = self.wait_for_auto_update,
)
@@ -672,23 +629,12 @@ impl Resolve<WriteArgs> for BatchCheckDeploymentForUpdate {
self,
WriteArgs { user }: &WriteArgs,
) -> Result<Self::Response, Self::Error> {
let all_tags = if self.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let deployments = list_full_for_user_using_pattern::<Deployment>(
&self.pattern,
ResourceQuery {
tags: self.tags,
..Default::default()
},
None,
None,
Default::default(),
user,
PermissionLevel::Execute.into(),
&all_tags,
&[],
)
.await?;
+3 -6
View File
@@ -211,12 +211,9 @@ pub enum WriteRequest {
CreateGitProviderAccount(CreateGitProviderAccount),
UpdateGitProviderAccount(UpdateGitProviderAccount),
DeleteGitProviderAccount(DeleteGitProviderAccount),
#[serde(alias = "CreateDockerRegistryAccount")]
CreateImageRegistryAccount(CreateImageRegistryAccount),
#[serde(alias = "UpdateDockerRegistryAccount")]
UpdateImageRegistryAccount(UpdateImageRegistryAccount),
#[serde(alias = "DeleteDockerRegistryAccount")]
DeleteImageRegistryAccount(DeleteImageRegistryAccount),
CreateDockerRegistryAccount(CreateDockerRegistryAccount),
UpdateDockerRegistryAccount(UpdateDockerRegistryAccount),
DeleteDockerRegistryAccount(DeleteDockerRegistryAccount),
// ==== ALERT ====
CloseAlert(CloseAlert),
+2 -2
View File
@@ -347,9 +347,9 @@ async fn extract_resource_target_with_validation(
.find_one(filter)
.await
.context("Failed to query db for swarms")?
.context("No matching swarm found")?
.context("No matching server found")?
.id;
Ok((ResourceTargetVariant::Swarm, id))
Ok((ResourceTargetVariant::Server, id))
}
ResourceTarget::Server(ident) => {
let filter = match ObjectId::from_str(ident) {
+8 -8
View File
@@ -7,7 +7,7 @@ use komodo_client::{
api::write::*,
entities::{
Operation, ResourceTarget,
provider::{GitProviderAccount, ImageRegistryAccount},
provider::{DockerRegistryAccount, GitProviderAccount},
},
};
use mogh_error::AddStatusCodeError;
@@ -248,7 +248,7 @@ impl Resolve<WriteArgs> for DeleteGitProviderAccount {
}
}
impl Resolve<WriteArgs> for CreateImageRegistryAccount {
impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
#[instrument(
"CreateDockerRegistryAccount",
skip_all,
@@ -261,7 +261,7 @@ impl Resolve<WriteArgs> for CreateImageRegistryAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<CreateImageRegistryAccountResponse> {
) -> mogh_error::Result<CreateDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!(
@@ -271,7 +271,7 @@ impl Resolve<WriteArgs> for CreateImageRegistryAccount {
);
}
let mut account: ImageRegistryAccount = self.account.into();
let mut account: DockerRegistryAccount = self.account.into();
if account.domain.is_empty() {
return Err(
@@ -326,7 +326,7 @@ impl Resolve<WriteArgs> for CreateImageRegistryAccount {
}
}
impl Resolve<WriteArgs> for UpdateImageRegistryAccount {
impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
#[instrument(
"UpdateDockerRegistryAccount",
skip_all,
@@ -340,7 +340,7 @@ impl Resolve<WriteArgs> for UpdateImageRegistryAccount {
async fn resolve(
mut self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<UpdateImageRegistryAccountResponse> {
) -> mogh_error::Result<UpdateDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can update docker registry accounts")
@@ -423,7 +423,7 @@ impl Resolve<WriteArgs> for UpdateImageRegistryAccount {
}
}
impl Resolve<WriteArgs> for DeleteImageRegistryAccount {
impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
#[instrument(
"DeleteDockerRegistryAccount",
skip_all,
@@ -435,7 +435,7 @@ impl Resolve<WriteArgs> for DeleteImageRegistryAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<DeleteImageRegistryAccountResponse> {
) -> mogh_error::Result<DeleteDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can delete docker registry accounts")
+3 -16
View File
@@ -19,7 +19,6 @@ use komodo_client::{
all_logs_success, komodo_timestamp,
permission::PermissionLevel,
repo::Repo,
resource::ResourceQuery,
stack::{Stack, StackInfo, StackServiceWithUpdate, StackState},
update::Update,
user::{auto_redeploy_user, stack_user, system_user},
@@ -37,7 +36,7 @@ use crate::{
api::execute::{self, ExecuteRequest, ExecutionResult},
config::core_config,
helpers::{
query::{get_all_tags, get_swarm_or_server},
query::get_swarm_or_server,
stack_git_token, swarm_or_server_request,
update::{add_update, make_update, poll_update_until_complete},
},
@@ -1041,7 +1040,6 @@ impl Resolve<WriteArgs> for BatchCheckStackForUpdate {
fields(
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
skip_auto_update = self.skip_auto_update,
wait_for_auto_update = self.wait_for_auto_update,
)
@@ -1050,23 +1048,12 @@ impl Resolve<WriteArgs> for BatchCheckStackForUpdate {
self,
WriteArgs { user }: &WriteArgs,
) -> Result<Self::Response, Self::Error> {
let all_tags = if self.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks = list_full_for_user_using_pattern::<Stack>(
&self.pattern,
ResourceQuery {
tags: self.tags,
..Default::default()
},
None,
None,
Default::default(),
user,
PermissionLevel::Execute.into(),
&all_tags,
&[],
)
.await?;
+16 -22
View File
@@ -171,10 +171,10 @@ impl Resolve<WriteArgs> for DeleteTerminal {
)
)]
async fn resolve(
mut self,
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<NoData> {
let server = match &mut self.target {
let server = match &self.target {
TerminalTarget::Server { server } => {
let server = server
.as_ref()
@@ -195,31 +195,27 @@ impl Resolve<WriteArgs> for DeleteTerminal {
)
.await?
}
TerminalTarget::Stack {
stack: stack_id, ..
} => {
let stack = get_check_permissions::<Stack>(
stack_id,
TerminalTarget::Stack { stack, .. } => {
let server = get_check_permissions::<Stack>(
stack,
user,
PermissionLevel::Read.terminal(),
)
.await?;
// Must fix any incoming stack name to id
*stack_id = stack.id;
resource::get::<Server>(&stack.config.server_id).await?
.await?
.config
.server_id;
resource::get::<Server>(&server).await?
}
TerminalTarget::Deployment {
deployment: deployment_id,
} => {
let deployment = get_check_permissions::<Deployment>(
deployment_id,
TerminalTarget::Deployment { deployment } => {
let server = get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Read.terminal(),
)
.await?;
// Must fix any incoming deployment name to id
*deployment_id = deployment.id;
resource::get::<Server>(&deployment.config.server_id).await?
.await?
.config
.server_id;
resource::get::<Server>(&server).await?
}
};
@@ -293,8 +289,6 @@ impl Resolve<WriteArgs> for BatchDeleteAllTerminals {
resource::list_full_for_user::<Server>(
self.query,
None,
None,
user,
PermissionLevel::Read.terminal(),
&all_tags,
-3
View File
@@ -24,9 +24,6 @@ use crate::{
mod terminal;
mod update;
pub(super) const WS_KEEP_ALIVE_INTERVAL: std::time::Duration =
std::time::Duration::from_secs(30);
pub fn router() -> Router {
Router::new()
// Periphery facing
+1 -17
View File
@@ -117,25 +117,9 @@ async fn forward_ws_channel(
};
let periphery_to_core = async {
let mut keep_alive =
tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL);
loop {
// Already adheres to cancellation token
let recv = tokio::select! {
// Websocket keep-alive ping
_ = keep_alive.tick() => {
if client_send
.send(ws::Message::Ping(Bytes::new()))
.await
.is_err()
{
break;
}
continue;
}
recv = periphery_receiver.recv() => recv,
};
match recv {
match periphery_receiver.recv().await {
Ok(Ok(bytes)) => {
if let Err(e) =
client_send.send(ws::Message::Binary(bytes.into())).await
+1 -11
View File
@@ -3,7 +3,6 @@ use axum::{
extract::{WebSocketUpgrade, ws::Message},
response::IntoResponse,
};
use bytes::Bytes;
use futures_util::{SinkExt, StreamExt};
use komodo_client::entities::{
ResourceTarget, permission::PermissionLevel, user::User,
@@ -39,19 +38,10 @@ pub async fn handler(
let cancel_clone = cancel.clone();
tokio::spawn(async move {
let mut keep_alive =
tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL);
loop {
// poll for updates off the receiver / await cancel / send keep-alive ping.
// poll for updates off the receiver / await cancel.
let update = select! {
_ = cancel_clone.cancelled() => break,
_ = keep_alive.tick() => {
if ws_sender.send(Message::Ping(Bytes::new())).await.is_err() {
cancel_clone.cancel();
break;
}
continue;
}
update = receiver.recv() => {update.expect("failed to recv update msg")}
};
+1 -1
View File
@@ -88,7 +88,7 @@ pub async fn launch_ec2_instance(
port: _,
use_https: _,
git_providers: _,
image_registries: _,
docker_registries: _,
secrets: _,
} = config;
let instance_type = handle_unknown_instance_type(
+2 -3
View File
@@ -2,9 +2,8 @@ pub mod aws;
#[derive(Debug)]
pub enum BuildCleanupData {
/// Store the builder id if it needs
/// to be removed from builder_usage_cache.
Server(Option<String>),
/// Nothing to clean up
Server,
/// Cleanup Periphery connection
Url,
/// Clean up AWS instance
+1 -10
View File
@@ -348,9 +348,6 @@ pub fn core_config() -> &'static CoreConfig {
transparent_mode: env
.komodo_transparent_mode
.unwrap_or(config.transparent_mode),
default_pagination_limit: env
.komodo_default_pagination_limit
.unwrap_or(config.default_pagination_limit),
ui_write_disabled: env
.komodo_ui_write_disabled
.unwrap_or(config.ui_write_disabled),
@@ -433,12 +430,6 @@ pub fn core_config() -> &'static CoreConfig {
ssl_cert_file: env
.komodo_ssl_cert_file
.unwrap_or(config.ssl_cert_file),
reporting_enabled: env
.komodo_reporting_enabled
.unwrap_or(config.reporting_enabled),
reporting_private_key: env
.komodo_reporting_private_key
.unwrap_or(config.reporting_private_key),
ui_path: env.komodo_ui_path.unwrap_or(config.ui_path),
ui_index_force_no_cache: env
.komodo_ui_index_force_no_cache
@@ -456,7 +447,7 @@ pub fn core_config() -> &'static CoreConfig {
// These can't be overridden on env
secrets: config.secrets,
git_providers: config.git_providers,
image_registries: config.image_registries,
docker_registries: config.docker_registries,
}
})
}
+1 -1
View File
@@ -503,7 +503,7 @@ async fn create_server_maybe_builder(
name: server_query,
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_ids: Some(vec![server.id.clone()]),
server_id: Some(server.id.clone()),
},
),
})
+27 -112
View File
@@ -1,18 +1,16 @@
use std::{collections::HashMap, time::Duration};
use std::time::Duration;
use anyhow::{Context, anyhow};
use database::mungos::mongodb::bson::oid::ObjectId;
use formatting::muted;
use futures_util::{StreamExt, stream::FuturesOrdered};
use komodo_client::entities::{
Version,
builder::{AwsBuilderConfig, Builder, BuilderConfig},
komodo_timestamp,
server::{Server, ServerState},
server::Server,
update::{Log, Update},
};
use periphery_client::api::{self, GetVersionResponse};
use tokio::sync::Mutex;
use crate::{
cloud::{
@@ -26,7 +24,6 @@ use crate::{
helpers::update::update_update,
periphery::PeripheryClient,
resource,
state::{builder_usage_cache, server_status_cache},
};
use super::periphery_client;
@@ -40,26 +37,25 @@ const BUILDER_POLL_MAX_TRIES: usize = 60;
fields(
resource_name,
builder_id = builder.id,
update_id = update.as_ref().map(|u| u.id.as_str())
update_id = update.id
)
)]
pub async fn connect_builder_periphery(
// build: &Build,
resource_name: String,
version: Option<Version>,
builder: Builder,
update: Option<&mut Update>,
update: &mut Update,
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
match builder.config {
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
}
BuilderConfig::Url(config) => {
if config.address.is_empty() {
return Err(anyhow!(
"Builder has not yet configured an address"
));
}
// Builder id no good because it may be active for multiple connections.
// TODO: Dont use builder id, or will be problems
// with simultaneous spawned builders.
let periphery = PeripheryClient::new(
PeripheryConnectionArgs::from_url_builder(
&ObjectId::new().to_hex(),
@@ -68,66 +64,22 @@ pub async fn connect_builder_periphery(
config.insecure_tls,
)
.await?;
// Poll for connection to be estalished
let mut err = None;
for _ in 0..10 {
tokio::time::sleep(Duration::from_secs(1)).await;
match periphery
.health_check()
.await
.context("Url Builder failed health check")
{
Ok(_) => return Ok((periphery, BuildCleanupData::Url)),
Err(e) => err = Some(e),
};
}
Err(err.context("Missing error")?)
periphery
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((periphery, BuildCleanupData::Url))
}
BuilderConfig::Server(config) => {
if config.server_ids.is_empty() {
return Err(anyhow!(
"Server Builder has no configured Servers"
));
if config.server_id.is_empty() {
return Err(anyhow!("Builder has not configured a server"));
}
// Short path for single configured builder
if config.server_ids.len() == 1 {
let server =
resource::get::<Server>(&config.server_ids[0]).await?;
let periphery = periphery_client(&server).await?;
return Ok((periphery, BuildCleanupData::Server(None)));
}
// Get filtered list of available Servers
let server_status_cache = server_status_cache();
let available_server_ids = config
.server_ids
.iter()
.map(|server_id| async move {
server_status_cache
.get(server_id)
.await
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
.then_some(server_id)
})
.collect::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.flatten()
.collect::<Vec<_>>();
let selected = builder_usage_cache()
.get_or_insert_default(&builder.id)
.await
.select(&available_server_ids)
.await
.context("Server builder has no available servers")?;
let server = resource::get::<Server>(selected).await?;
let server = resource::get::<Server>(&config.server_id).await?;
let periphery = periphery_client(&server).await?;
Ok((periphery, BuildCleanupData::Server(Some(server.id))))
Ok((periphery, BuildCleanupData::Server))
}
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
}
}
}
@@ -137,14 +89,14 @@ pub async fn connect_builder_periphery(
skip_all,
fields(
resource_name,
update_id = update.as_ref().map(|u| u.id.as_str()),
update_id = update.id,
)
)]
async fn get_aws_builder(
resource_name: &str,
version: Option<Version>,
config: AwsBuilderConfig,
mut update: Option<&mut Update>,
update: &mut Update,
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
let start_create_ts = komodo_timestamp();
@@ -162,10 +114,9 @@ async fn get_aws_builder(
..Default::default()
};
if let Some(update) = &mut update {
update.logs.push(log);
update_update((*update).clone()).await?;
}
update.logs.push(log);
update_update(update.clone()).await?;
let protocol = if config.use_https { "wss" } else { "ws" };
@@ -202,10 +153,8 @@ async fn get_aws_builder(
end_ts: komodo_timestamp(),
..Default::default()
};
if let Some(update) = update {
update.logs.push(connect_log);
update_update(update.clone()).await?;
}
update.logs.push(connect_log);
update_update(update.clone()).await?;
return Ok((
periphery,
BuildCleanupData::Aws {
@@ -245,17 +194,9 @@ pub async fn cleanup_builder_instance(
update: &mut Update,
) {
match cleanup_data {
BuildCleanupData::Server(None) => {
BuildCleanupData::Server => {
// Nothing to clean up
}
BuildCleanupData::Server(Some(builder_id)) => {
// Release periphery (server) id from builder
builder_usage_cache()
.get_or_insert_default(&builder_id)
.await
.release(&periphery.id)
.await
}
BuildCleanupData::Url => {
periphery.cleanup().await;
}
@@ -311,29 +252,3 @@ pub fn start_aws_builder_log(
]
.join("\n")
}
#[derive(Default)]
pub struct BuilderUsage(Mutex<HashMap<String, usize>>);
impl BuilderUsage {
pub async fn select<'a>(
&self,
available: &'a [&String],
) -> Option<&'a str> {
let mut lock = self.0.lock().await;
let selected = *available.iter().min_by_key(|key| {
lock.get(key.as_str()).copied().unwrap_or(0)
})?;
*lock.entry(selected.clone()).or_insert(0) += 1;
Some(selected.as_str())
}
pub async fn release(&self, key: &str) {
let mut lock = self.0.lock().await;
if let Some(count) = lock.get_mut(key)
&& *count > 0
{
*count -= 1;
}
}
}
+1 -1
View File
@@ -169,7 +169,7 @@ pub async fn registry_token(
}
Ok(
core_config()
.image_registries
.docker_registries
.iter()
.find(|provider| provider.domain == provider_domain)
.and_then(|provider| {
+6 -31
View File
@@ -14,13 +14,12 @@ use komodo_client::{
procedure::{Procedure, ProcedureStage},
repo::Repo,
stack::Stack,
update::Update,
update::{Log, Update},
user::procedure_user,
},
};
use mogh_resolver::Resolve;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::{
@@ -28,7 +27,6 @@ use crate::{
execute::{ExecuteArgs, ExecuteRequest},
write::WriteArgs,
},
config::core_config,
resource::{KomodoResource, list_full_for_user_using_pattern},
state::{all_resources_cache, db_client},
};
@@ -38,21 +36,8 @@ use super::update::{init_execution_update, update_update};
pub async fn execute_procedure(
procedure: &Procedure,
update: &Mutex<Update>,
cancel: CancellationToken,
) -> anyhow::Result<()> {
for stage in &procedure.config.stages {
if cancel.is_cancelled() {
add_line_to_update(
update,
&format!(
"{}: Cancelled before stage: '{}'",
muted("ERROR"),
bold(&stage.name)
),
)
.await;
return Err(anyhow!("Procedure cancelled before completion"));
}
if !stage.enabled {
continue;
}
@@ -289,9 +274,6 @@ async fn execute_execution(
}
resolve_execute!(RunProcedure, req)
}
Execution::CancelProcedure(req) => {
resolve_execute!(CancelProcedure, req)
}
// Special: write operation
Execution::CommitSync(req) => req
.resolve(&WriteArgs { user })
@@ -344,9 +326,6 @@ async fn execute_execution(
}
// Standard executions
Execution::RunAction(req) => resolve_execute!(RunAction, req),
Execution::CancelAction(req) => {
resolve_execute!(CancelAction, req)
}
Execution::RunBuild(req) => resolve_execute!(RunBuild, req),
Execution::CancelBuild(req) => resolve_execute!(CancelBuild, req),
Execution::Deploy(req) => resolve_execute!(Deploy, req),
@@ -506,10 +485,9 @@ async fn execute_execution(
Ok(())
} else {
Err(anyhow!(
"{}: Execution not successful, see update: '{}/updates/{}'",
"{}: execution not successful. see update '{}'",
colored("ERROR", Color::Red),
core_config().host,
update.id,
bold(&update.id),
))
}
}
@@ -523,13 +501,14 @@ async fn handle_resolve_result(
match res {
Ok(res) => Ok(res),
Err(e) => {
let log =
Log::error("execution error", format_serror(&e.into()));
let mut update =
find_one_by_id(&db_client().updates, update_id)
.await
.context("Failed to query to db")?
.context("no update exists with given id")?;
update
.push_error_log("Execution error", format_serror(&e.into()));
update.logs.push(log);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
@@ -557,8 +536,6 @@ async fn extend_batch_exection<E: ExtendBatch>(
let more = list_full_for_user_using_pattern::<E::Resource>(
pattern,
Default::default(),
None,
None,
procedure_user(),
PermissionLevel::Read.into(),
&[],
@@ -744,9 +721,7 @@ pub fn replace_procedure_stage_ids_with_names(
replace_id_with_name!(
RunProcedure => procedure, procedures;
CancelProcedure => procedure, procedures;
RunAction => action, actions;
CancelAction => action, actions;
RunBuild => build, builds;
CancelBuild => build, builds;
Deploy => deployment, deployments;
+1 -1
View File
@@ -55,7 +55,7 @@ async fn prune_images() -> anyhow::Result<()> {
while let Some((res, server)) = futures.next().await {
if let Err(e) = res {
warn!(
"Failed to prune images on Server {} ({}) | {e:#}",
"failed to prune images on Server {} ({}) | {e:#}",
server.name, server.id
)
}
+1 -33
View File
@@ -45,7 +45,7 @@ use crate::{
state::{
action_state_cache, action_states, db_client,
deployment_status_cache, procedure_state_cache,
server_status_cache, stack_status_cache,
stack_status_cache,
},
};
@@ -97,38 +97,6 @@ pub async fn get_server_state(server: &Server) -> ServerState {
}
}
/// The Server state as computed for the server list items,
/// from the in memory status cache.
pub async fn get_cached_server_state(id: &String) -> ServerState {
server_status_cache()
.get(id)
.await
.map(|status| status.state)
.unwrap_or_default()
}
/// The Stack state as computed for the stack list items,
/// from the in memory action states / status cache.
pub async fn get_cached_stack_state(id: &String) -> StackState {
if action_states()
.stack
.get(id)
.await
.map(|s| s.get().map(|s| s.deploying))
.transpose()
.ok()
.flatten()
.unwrap_or_default()
{
return StackState::Deploying;
}
stack_status_cache()
.get(id)
.await
.map(|status| status.curr.state)
.unwrap_or_default()
}
pub async fn get_deployment_state(
id: &String,
) -> anyhow::Result<DeploymentState> {
+23 -1
View File
@@ -1,3 +1,5 @@
use std::time::Duration;
use anyhow::anyhow;
use komodo_client::entities::server::{Server, ServerState};
use mogh_resolver::HasResponse;
@@ -11,6 +13,23 @@ pub async fn swarm_request<T>(
server_ids: &[String],
request: T,
) -> anyhow::Result<T::Response>
where
T: std::fmt::Debug + Clone + Serialize + HasResponse,
T::Response: DeserializeOwned,
{
swarm_request_custom_timeout(
server_ids,
request,
Duration::from_secs(10),
)
.await
}
pub async fn swarm_request_custom_timeout<T>(
server_ids: &[String],
request: T,
timeout: Duration,
) -> anyhow::Result<T::Response>
where
T: std::fmt::Debug + Clone + Serialize + HasResponse,
T::Response: DeserializeOwned,
@@ -35,7 +54,10 @@ where
else {
continue;
};
match periphery.request(request.clone()).await {
match periphery
.request_custom_timeout(request.clone(), timeout)
.await
{
Ok(res) => return Ok(res),
Err(e) => err = Some(e),
}
+1 -1
View File
@@ -332,7 +332,7 @@ pub async fn get_deployment_periphery_container(
let periphery = periphery_client(&server).await?;
let container = deployment.deployed_name().to_string();
let container = deployment.name.clone();
Ok((
TerminalTarget::Deployment {
-2
View File
@@ -226,10 +226,8 @@ pub async fn init_execution_update(
(CancelRepoBuild, Repo, repo),
// Procedure
(RunProcedure, Procedure, procedure),
(CancelProcedure, Procedure, procedure),
// Action
(RunAction, Action, action),
(CancelAction, Action, action),
// Resource Sync
(RunSync, ResourceSync, sync),
// Stack (simple)
-2
View File
@@ -19,7 +19,6 @@ mod monitor;
mod network;
mod periphery;
mod permission;
mod reporting;
mod resource;
mod schedule;
mod stack;
@@ -72,7 +71,6 @@ async fn app() -> anyhow::Result<()> {
resource::spawn_action_state_refresh_loop();
schedule::spawn_schedule_executor();
helpers::prune::spawn_prune_loop();
reporting::spawn_reporting_loop();
}
.instrument(startup_span)
.await;
-4
View File
@@ -36,8 +36,6 @@ async fn get_all_swarms_map()
-> anyhow::Result<(HashMap<String, Swarm>, HashMap<String, String>)> {
let swarms = resource::list_full_for_user::<Swarm>(
ResourceQuery::default(),
None,
None,
system_user(),
PermissionLevel::Read.into(),
&[],
@@ -63,8 +61,6 @@ async fn get_all_servers_map()
{
let servers = resource::list_full_for_user::<Server>(
ResourceQuery::default(),
None,
None,
system_user(),
PermissionLevel::Read.into(),
&[],
+5 -6
View File
@@ -52,18 +52,17 @@ pub fn spawn_monitoring_loops() {
fn spawn_server_monitoring_loop() {
tokio::spawn(async move {
refresh_all_server_cache().await;
refresh_all_server_cache(komodo_timestamp()).await;
let interval = monitoring_interval();
loop {
let ts = (wait_until_timelength(interval, ADDITIONAL_MS).await
- ADDITIONAL_MS) as i64;
refresh_all_server_cache().await;
tokio::join!(check_alerts(ts), record_server_stats(ts));
refresh_all_server_cache(ts).await;
}
});
}
async fn refresh_all_server_cache() {
async fn refresh_all_server_cache(ts: i64) {
let servers =
match find_collect(&db_client().servers, None, None).await {
Ok(servers) => servers,
@@ -78,6 +77,7 @@ async fn refresh_all_server_cache() {
refresh_server_cache(&server, false).await;
});
join_all(futures).await;
tokio::join!(check_alerts(ts), record_server_stats(ts));
}
/// Makes sure cache for server doesn't update too frequently / simultaneously.
@@ -193,8 +193,7 @@ pub fn refresh_server_cache(
if let Some(docker) = &mut docker {
docker.containers.iter_mut().for_each(|container| {
container.server_id = Some(server.id.clone());
container.server_name = Some(server.name.clone());
container.server_id = Some(server.id.clone())
});
}
+1 -5
View File
@@ -20,13 +20,9 @@ pub async fn record_server_stats(ts: i64) {
ts,
sid: status.id.clone(),
cpu_perc: stats.cpu_perc,
load_average: stats.load_average,
load_average: stats.load_average.clone(),
mem_total_gb: stats.mem_total_gb,
mem_used_gb: stats.mem_used_gb,
mem_buff_cache_gb: stats.mem_buff_cache_gb,
mem_zfs_arc_gb: stats.mem_zfs_arc_gb,
swap_total_gb: stats.swap_total_gb,
swap_used_gb: stats.swap_used_gb,
disk_total_gb,
disk_used_gb,
disks: stats.disks.clone(),
+3 -22
View File
@@ -6,10 +6,7 @@ use komodo_client::entities::{
container::ContainerListItem, image::ImageListItem,
service::SwarmServiceListItem, stack::SwarmStackListItem,
},
stack::{
Stack, StackService, StackServiceNames, StackServiceState,
StackState,
},
stack::{Stack, StackService, StackServiceNames, StackState},
swarm::SwarmState,
};
@@ -98,18 +95,10 @@ pub async fn update_swarm_stack_cache(
None,
));
let state = swarm_service
.as_ref()
.map(|c| c.state.into())
.unwrap_or(StackServiceState::Unknown);
StackService {
stack_id: stack.id.clone(),
stack_name: stack.name.clone(),
service: service_name.clone(),
container: None,
swarm_service,
state,
image,
image_digests,
}
@@ -187,19 +176,11 @@ pub async fn update_server_stack_cache(
None
));
let state = container
.as_ref()
.map(|c| c.state.into())
.unwrap_or(StackServiceState::Unknown);
StackService {
stack_id: stack.id.clone(),
stack_name: stack.name.clone(),
service: service_name.clone(),
image: image.clone(),
container,
swarm_service: None,
state,
image_digests,
}
}).collect::<Vec<_>>();
@@ -247,7 +228,7 @@ pub async fn update_swarm_deployment_cache(
service
.name
.as_ref()
.map(|name| name == deployment.deployed_name())
.map(|name| name == &deployment.name)
.unwrap_or_default()
})
.cloned();
@@ -301,7 +282,7 @@ pub async fn update_server_deployment_cache(
for deployment in deployments {
let container = containers
.iter()
.find(|container| container.name == deployment.deployed_name())
.find(|container| container.name == deployment.name)
.cloned();
let image_digests = container
+11 -4
View File
@@ -1,4 +1,7 @@
use std::sync::{Arc, OnceLock};
use std::{
sync::{Arc, OnceLock},
time::Duration,
};
use anyhow::anyhow;
use async_timing_util::wait_until_timelength;
@@ -17,7 +20,7 @@ use tokio::sync::Mutex;
use crate::{
config::monitoring_interval,
helpers::swarm::swarm_request,
helpers::swarm::swarm_request_custom_timeout,
monitor::{
RefreshCacheResources,
resources::{
@@ -115,8 +118,12 @@ pub async fn refresh_swarm_cache(swarm: &Swarm, force: bool) {
}
let PollSwarmStatusResponse { inspect, lists } =
match swarm_request(&swarm.config.server_ids, PollSwarmStatus {})
.await
match swarm_request_custom_timeout(
&swarm.config.server_ids,
PollSwarmStatus {},
Duration::from_secs(1),
)
.await
{
Ok(info) => info,
Err(e) => {
+17 -9
View File
@@ -85,6 +85,20 @@ impl PeripheryClient {
&self,
request: T,
) -> anyhow::Result<T::Response>
where
T: std::fmt::Debug + Serialize + HasResponse,
T::Response: DeserializeOwned,
{
self
.request_custom_timeout(request, Duration::from_secs(10))
.await
}
pub async fn request_custom_timeout<T>(
&self,
request: T,
timeout: Duration,
) -> anyhow::Result<T::Response>
where
T: std::fmt::Debug + Serialize + HasResponse,
T::Response: DeserializeOwned,
@@ -120,17 +134,11 @@ impl PeripheryClient {
let res = async {
// Poll for the associated response
loop {
let message = response_receiever
.recv()
// Periphery request handler sends pings every 4s
// *on this channel specifically* so Core knows
// request is being processed. Hardcoded 11s
// allows for missed 5s ping due to network reconnect.
.with_timeout(Duration::from_secs(10))
.await?;
let message =
response_receiever.recv().with_timeout(timeout).await?;
// Still in progress, sent to avoid timeout.
let Some(message) = message.decode()? else {
// Just a ping from periphery request handler
continue;
};
+87 -200
View File
@@ -1,10 +1,10 @@
use std::{collections::HashMap, future::Future};
use std::collections::HashMap;
use anyhow::{Context, anyhow};
use database::{
bson::Document, mongo_indexed::doc, mungos::find::find_collect,
};
use futures_util::{TryStreamExt, future::BoxFuture};
use futures_util::{FutureExt, future::BoxFuture};
use indexmap::IndexSet;
use komodo_client::{
api::read::GetPermission,
@@ -16,9 +16,7 @@ use komodo_client::{
builder::Builder,
deployment::Deployment,
permission::SpecificPermission,
permission::{
Permission, PermissionLevel, PermissionLevelAndSpecifics,
},
permission::{PermissionLevel, PermissionLevelAndSpecifics},
procedure::Procedure,
repo::Repo,
resource::Resource,
@@ -178,34 +176,14 @@ pub fn get_user_permission_on_resource<'a, T: KomodoResource>(
})
}
/// Precomputed user permissions for listing resources of a type.
/// Load with [load_list_permits], then check visibility of
/// each resource with [ListPermits::permitted].
pub enum ListPermits {
/// The user can see all resources of the type,
/// eg. admin, transparent mode, or 'all' access on the variant.
Unrestricted,
/// Visibility must be checked per-resource
/// against the permissions table.
Fine(Box<FineListPermits>),
}
pub struct FineListPermits {
required: PermissionLevelAndSpecifics,
base: PermissionLevelAndSpecifics,
permission_by_resource_id: HashMap<String, Permission>,
additional_specific_cache:
HashMap<ResourceTarget, IndexSet<SpecificPermission>>,
user: User,
}
pub async fn load_list_permits<T: KomodoResource>(
pub async fn list_resources_for_user<T: KomodoResource>(
filters: impl Into<Option<Document>>,
user: &User,
required: PermissionLevelAndSpecifics,
) -> anyhow::Result<ListPermits> {
permission: PermissionLevelAndSpecifics,
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {
// Check admin
if user.admin {
return Ok(ListPermits::Unrestricted);
return list_all_resources::<T>(filters).await;
}
let mut base = PermissionLevelAndSpecifics {
@@ -218,8 +196,8 @@ pub async fn load_list_permits<T: KomodoResource>(
};
// 'transparent_mode' early return.
if base.fulfills(&required) {
return Ok(ListPermits::Unrestricted);
if base.fulfills(&permission) {
return list_all_resources::<T>(filters).await;
}
let resource_type = T::resource_type();
@@ -228,8 +206,8 @@ pub async fn load_list_permits<T: KomodoResource>(
if let Some(all_permission) = user.all.get(&resource_type) {
base.elevate(all_permission);
// 'user.all' early return.
if base.fulfills(&required) {
return Ok(ListPermits::Unrestricted);
if base.fulfills(&permission) {
return list_all_resources::<T>(filters).await;
}
}
@@ -239,23 +217,25 @@ pub async fn load_list_permits<T: KomodoResource>(
if let Some(all_permission) = group.all.get(&resource_type) {
base.elevate(all_permission);
// 'group.all' early return.
if base.fulfills(&required) {
return Ok(ListPermits::Unrestricted);
if base.fulfills(&permission) {
return list_all_resources::<T>(filters).await;
}
}
}
// Pull any permissions on the variant using the permissions table
let permissions = find_collect(
&db_client().permissions,
doc! {
"$or": user_target_query(&user.id, &groups)?,
"resource_target.type": resource_type.as_ref(),
},
None,
)
.await
.context("failed to query permissions on db")?;
let (all, permissions) = tokio::try_join!(
list_all_resources::<T>(filters),
// And any ids using the permissions table
find_collect(
&db_client().permissions,
doc! {
"$or": user_target_query(&user.id, &groups)?,
"resource_target.type": resource_type.as_ref(),
},
None,
)
.map(|res| res.context("failed to query permissions on db"))
)?;
let permission_by_resource_id = permissions
.into_iter()
@@ -267,54 +247,34 @@ pub async fn load_list_permits<T: KomodoResource>(
})
.collect::<HashMap<_, _>>();
Ok(ListPermits::Fine(
FineListPermits {
required,
base,
permission_by_resource_id,
additional_specific_cache: Default::default(),
user: user.clone(),
}
.into(),
))
}
impl ListPermits {
/// Check whether the user can see the given resource.
pub async fn permitted<T: KomodoResource>(
&mut self,
resource: &Resource<T::Config, T::Info>,
) -> anyhow::Result<bool> {
let ListPermits::Fine(fine) = self else {
return Ok(true);
};
let mut resources = Vec::new();
let mut additional_specific_cache =
HashMap::<ResourceTarget, IndexSet<SpecificPermission>>::new();
for resource in all {
let mut perm = if let Some(perm) =
fine.permission_by_resource_id.get(&resource.id)
permission_by_resource_id.get(&resource.id)
{
fine.base.join(perm)
base.join(perm)
} else {
fine.base.clone()
base.clone()
};
// Add in the resource level base permissions,
// matching [get_user_permission_on_resource].
perm.elevate(&resource.base_permission);
// Check if already fulfils
if perm.fulfills(&fine.required) {
return Ok(true);
if perm.fulfills(&permission) {
resources.push(resource);
continue;
}
// Also check if fulfills with inherited specific
let additional_target = if let Some(additional_target) =
T::inherit_specific_permissions_from(resource)
T::inherit_specific_permissions_from(&resource)
&& !additional_target.is_empty()
{
additional_target
} else {
return Ok(false);
continue;
};
let additional_specific = match fine
.additional_specific_cache
let additional_specific = match additional_specific_cache
.get(&additional_target)
.cloned()
{
@@ -323,99 +283,21 @@ impl ListPermits {
let specific = GetPermission {
target: additional_target.clone(),
}
.resolve(&ReadArgs {
user: fine.user.clone(),
})
.resolve(&ReadArgs { user: user.clone() })
.await
.map_err(|e| e.error)
.context(
"failed to get user permission on additional target",
)?
.specific;
fine
.additional_specific_cache
additional_specific_cache
.insert(additional_target, specific.clone());
specific
}
};
perm.specific.extend(additional_specific);
Ok(perm.fulfills(&fine.required))
}
}
pub async fn list_resources_for_user<T: KomodoResource>(
filters: impl Into<Option<Document>>,
limit: impl Into<Option<i64>>,
skip: impl Into<Option<u64>>,
user: &User,
permission: PermissionLevelAndSpecifics,
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {
let mut permits = load_list_permits::<T>(user, permission).await?;
if let ListPermits::Unrestricted = permits {
return list_all_resources::<T>(filters, limit, skip).await;
}
list_resources_with_permits::<T, _>(
&mut permits,
filters.into(),
limit.into(),
skip.into(),
|resource| async move { Some(resource) },
)
.await
}
/// Drive the cursor directly, checking each resource against the
/// user permissions and the additional `filter` (eg. by state
/// computed from the in memory caches), with limit / skip applied
/// in memory after the filters. Stops pulling from the cursor as
/// soon as the limit is reached, and avoids collecting resources
/// the user cannot see.
///
/// `filter` receives each resource by value, and returns
/// `Some(resource)` to keep it in the results, or `None` to drop it.
pub async fn list_resources_with_permits<T: KomodoResource, F>(
permits: &mut ListPermits,
filters: Option<Document>,
limit: Option<i64>,
skip: Option<u64>,
filter: impl Fn(Resource<T::Config, T::Info>) -> F,
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>>
where
F: Future<Output = Option<Resource<T::Config, T::Info>>> + Send,
{
let mut cursor = T::coll()
.find(filters.unwrap_or_default())
.sort(doc! { "name": 1 })
.await
.with_context(|| {
format!("Failed to query db for {}s", T::resource_type())
})?;
let limit = limit.unwrap_or_default().max(0) as usize;
let skip = skip.unwrap_or_default();
let mut skipped = 0;
let mut resources = Vec::new();
while let Some(resource) = cursor
.try_next()
.await
.context("Failed to pull next resource from db cursor")?
{
if !permits.permitted::<T>(&resource).await? {
continue;
}
let Some(resource) = filter(resource).await else {
continue;
};
if skipped < skip {
skipped += 1;
continue;
}
resources.push(resource);
if limit != 0 && resources.len() >= limit {
break;
if perm.fulfills(&permission) {
resources.push(resource);
}
}
@@ -425,28 +307,55 @@ where
/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access).
pub async fn list_resource_ids_for_user<T: KomodoResource>(
filters: Option<Document>,
limit: impl Into<Option<i64>>,
skip: impl Into<Option<u64>>,
user: &User,
permission: PermissionLevelAndSpecifics,
) -> anyhow::Result<Option<Vec<String>>> {
let mut permits = load_list_permits::<T>(user, permission).await?;
if let ListPermits::Unrestricted = permits {
// Check admin
if user.admin {
return Ok(None);
}
let ids = list_resources_with_permits::<T, _>(
&mut permits,
filters,
limit.into(),
skip.into(),
|resource| async move { Some(resource) },
)
.await?
.into_iter()
.map(|resource| resource.id)
.collect();
let mut base = PermissionLevelAndSpecifics {
level: if core_config().transparent_mode {
PermissionLevel::Read
} else {
PermissionLevel::None
},
specific: Default::default(),
};
// 'transparent_mode' early return.
if base.fulfills(&permission) {
return Ok(None);
}
let resource_type = T::resource_type();
if let Some(all) = user.all.get(&resource_type) {
base.elevate(all);
// 'user.all' early return.
if base.fulfills(&permission) {
return Ok(None);
}
}
// Check user groups 'all' on variant
let groups = get_user_user_groups(&user.id).await?;
for group in &groups {
if let Some(all) = group.all.get(&resource_type) {
base.elevate(all);
// 'group.all' early return.
if base.fulfills(&permission) {
return Ok(None);
}
}
}
let ids = list_resources_for_user::<T>(filters, user, permission)
.await?
.into_iter()
.map(|resource| resource.id)
.collect();
Ok(Some(ids))
}
@@ -460,8 +369,6 @@ pub async fn user_resource_target_query(
Ok(incoming_query)
} else {
let swarm_query = list_resource_ids_for_user::<Swarm>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -477,8 +384,6 @@ pub async fn user_resource_target_query(
.unwrap_or_else(|| doc! { "target.type": "Swarm" });
let server_query = list_resource_ids_for_user::<Server>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -492,8 +397,6 @@ pub async fn user_resource_target_query(
.unwrap_or_else(|| doc! { "target.type": "Server" });
let stack_query = list_resource_ids_for_user::<Stack>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -507,8 +410,6 @@ pub async fn user_resource_target_query(
.unwrap_or_else(|| doc! { "target.type": "Stack" });
let deployment_query = list_resource_ids_for_user::<Deployment>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -522,8 +423,6 @@ pub async fn user_resource_target_query(
.unwrap_or_else(|| doc! { "target.type": "Deployment" });
let build_query = list_resource_ids_for_user::<Build>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -537,8 +436,6 @@ pub async fn user_resource_target_query(
.unwrap_or_else(|| doc! { "target.type": "Build" });
let repo_query = list_resource_ids_for_user::<Repo>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -552,8 +449,6 @@ pub async fn user_resource_target_query(
.unwrap_or_else(|| doc! { "target.type": "Repo" });
let procedure_query = list_resource_ids_for_user::<Procedure>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -567,8 +462,6 @@ pub async fn user_resource_target_query(
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query = list_resource_ids_for_user::<Action>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -582,8 +475,6 @@ pub async fn user_resource_target_query(
.unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query = list_resource_ids_for_user::<Builder>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -597,8 +488,6 @@ pub async fn user_resource_target_query(
.unwrap_or_else(|| doc! { "target.type": "Builder" });
let alerter_query = list_resource_ids_for_user::<Alerter>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
@@ -613,8 +502,6 @@ pub async fn user_resource_target_query(
let resource_sync_query =
list_resource_ids_for_user::<ResourceSync>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
-153
View File
@@ -1,153 +0,0 @@
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Context;
use async_timing_util::{Timelength, wait_until_timelength};
use komodo_client::entities::{
ResourceTargetVariant, core_report::CoreReport,
};
use mogh_pki::{
PkiKind, RotatableKeyPair, SpkiPublicKey,
one_way::OneWayNoiseHandshake,
};
use crate::{config::core_config, state::db_client};
pub fn spawn_reporting_loop() {
let config = core_config();
if !config.reporting_enabled {
return;
}
let keys = match RotatableKeyPair::from_private_key_spec(
PkiKind::OneWay,
&config.reporting_private_key,
) {
Ok(keys) => keys,
Err(e) => {
warn!(
"Failed to initialize reporting key pair. Reporting is disabled. {e:#}"
);
return;
}
};
let client = reqwest::Client::default();
tokio::spawn(async move {
loop {
wait_until_timelength(Timelength::OneDay, 1100).await;
if let Err(e) = report(&client, &keys).await {
warn!("Reporting failed. {e:#}");
}
}
});
}
async fn report(
client: &reqwest::Client,
keys: &RotatableKeyPair,
) -> anyhow::Result<()> {
let db = db_client();
let keys = keys.load();
let private_key_bytes = keys
.private
.as_raw_bytes()
.context("Invalid reporting private key.")?;
let endpoint_public_key = client
.get("https://mogh.tech/report/public_key")
.send()
.await
.context("Failed to query for reporting endpoint public key for signature.")?
.error_for_status()
.context("Failed response for reporting endpoint public key for signature.")?
.text()
.await
.context("Failed to get reporting endpoint public key for signature.")?;
let endpoint_public_key =
SpkiPublicKey::maybe_pem_to_raw_bytes(&endpoint_public_key)
.context("Invalid reporting endpoint public key.")?;
let users = db
.users
.count_documents(Default::default())
.await
.context("Failed to query database for users")?;
let servers = db
.servers
.count_documents(Default::default())
.await
.context("Failed to query database for servers")?;
let swarms = db
.swarms
.count_documents(Default::default())
.await
.context("Failed to query database for swarms")?;
let stacks = db
.stacks
.count_documents(Default::default())
.await
.context("Failed to query database for stacks")?;
let deployments = db
.deployments
.count_documents(Default::default())
.await
.context("Failed to query database for deployments")?;
let builds = db
.builds
.count_documents(Default::default())
.await
.context("Failed to query database for builds")?;
let count = [
(ResourceTargetVariant::Server, servers),
(ResourceTargetVariant::Swarm, swarms),
(ResourceTargetVariant::Stack, stacks),
(ResourceTargetVariant::Deployment, deployments),
(ResourceTargetVariant::Build, builds),
]
.into_iter()
.collect();
let report = CoreReport {
report_public_key: keys.public().to_string(),
users,
count,
};
let serialized = serde_json::to_string(&report)
.context("Failed to serialize report JSON")?;
let timestamp =
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i64;
let prologue =
format!("POST|/report/komodo|{serialized}|{timestamp}");
let mut handshake = OneWayNoiseHandshake::new_initiator(
&private_key_bytes,
&endpoint_public_key,
prologue.as_bytes(),
)?;
let signature = handshake
.generate_signature()
.context("Failed to generate report signature")?;
client
.post("https://mogh.tech/report/komodo")
.header("x-api-signature", signature)
.header("x-api-timestamp", timestamp)
.header("content-type", "application/json")
.body(serialized)
.send()
.await
.context("Failed to post report.")?
.error_for_status()
.context("Failed response for report post.")?;
Ok(())
}
+2 -7
View File
@@ -74,9 +74,8 @@ impl super::KomodoResource for Build {
build.config.repo,
build.config.branch,
build.config.git_https,
String::new(),
);
let (git_provider, repo, branch, git_https, linked_repo_name) =
let (git_provider, repo, branch, git_https) =
if build.config.linked_repo.is_empty() {
default_git
} else {
@@ -90,7 +89,6 @@ impl super::KomodoResource for Build {
r.config.repo.clone(),
r.config.branch.clone(),
r.config.git_https,
r.name.clone(),
)
})
.unwrap_or(default_git)
@@ -109,7 +107,6 @@ impl super::KomodoResource for Build {
files_on_host: build.config.files_on_host,
dockerfile_contents: !build.config.dockerfile.is_empty(),
linked_repo: build.config.linked_repo,
linked_repo_name,
repo_link: repo_link(
&git_provider,
&repo,
@@ -300,9 +297,7 @@ async fn validate_config(
Ok(())
}
/// The Build state as computed for the build list items,
/// from the in memory action states / state cache.
pub async fn get_build_state(id: &String) -> BuildState {
async fn get_build_state(id: &String) -> BuildState {
if action_states()
.build
.get(id)
+6 -6
View File
@@ -54,7 +54,7 @@ impl super::KomodoResource for Builder {
}
BuilderConfig::Server(config) => (
BuilderConfigVariant::Server.to_string(),
Some(config.server_ids.join(", ")),
Some(config.server_id),
),
BuilderConfig::Aws(config) => (
BuilderConfigVariant::Aws.to_string(),
@@ -184,11 +184,10 @@ async fn validate_config(
config: &mut PartialBuilderConfig,
user: &User,
) -> anyhow::Result<()> {
if let PartialBuilderConfig::Server(PartialServerBuilderConfig {
server_ids: Some(server_ids),
}) = config
{
for server_id in server_ids {
match config {
PartialBuilderConfig::Server(PartialServerBuilderConfig {
server_id: Some(server_id),
}) if !server_id.is_empty() => {
let server = super::get_check_permissions::<Server>(
server_id,
user,
@@ -197,6 +196,7 @@ async fn validate_config(
.await?;
*server_id = server.id;
}
_ => {}
}
Ok(())
}
+31 -130
View File
@@ -1,7 +1,5 @@
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::update_one_by_id, mongodb::Collection, mongodb::bson::doc,
};
use anyhow::Context;
use database::mungos::mongodb::Collection;
use formatting::format_serror;
use indexmap::IndexSet;
use komodo_client::entities::{
@@ -13,7 +11,7 @@ use komodo_client::entities::{
DeploymentListItemInfo, DeploymentQuerySpecifics,
DeploymentState, PartialDeploymentConfig, conversions_from_str,
},
environment_vars_from_str, optional_string,
environment_vars_from_str,
permission::{
PermissionLevel, PermissionLevelAndSpecifics, SpecificPermission,
},
@@ -25,8 +23,7 @@ use komodo_client::entities::{
user::User,
};
use periphery_client::api::{
container::{RemoveContainer, RenameContainer},
swarm::RemoveSwarmServices,
container::RemoveContainer, swarm::RemoveSwarmServices,
};
use crate::{
@@ -37,10 +34,7 @@ use crate::{
swarm::swarm_request,
},
monitor::{refresh_server_cache, refresh_swarm_cache},
state::{
action_states, all_resources_cache, db_client,
deployment_status_cache,
},
state::{action_states, db_client, deployment_status_cache},
};
use super::get_check_permissions;
@@ -96,31 +90,31 @@ impl super::KomodoResource for Deployment {
deployment: Resource<Self::Config, Self::Info>,
) -> Self::ListItem {
let status = deployment_status_cache().get(&deployment.id).await;
let state = get_deployment_state(&deployment.id)
let state = if action_states()
.deployment
.get(&deployment.id)
.await
.unwrap_or_default();
let all = all_resources_cache().load();
let server_name = all
.servers
.get(&deployment.config.server_id)
.map(|server| server.name.clone())
.unwrap_or_default();
let swarm_name = all
.swarms
.get(&deployment.config.swarm_id)
.map(|swarm| swarm.name.clone())
.unwrap_or_default();
.map(|s| s.get().map(|s| s.deploying))
.transpose()
.ok()
.flatten()
.unwrap_or_default()
{
DeploymentState::Deploying
} else {
status.as_ref().map(|s| s.curr.state).unwrap_or_default()
};
let (build_image, build_id) = match deployment.config.image {
DeploymentImage::Build { build_id, version } => {
let (build_name, build_id, build_version) = all
.builds
.get(&build_id)
.map(|b| (b.name.clone(), b.id.clone(), b.config.version))
.unwrap_or((
String::from("unknown"),
String::new(),
Default::default(),
));
let (build_name, build_id, build_version) =
super::get::<Build>(&build_id)
.await
.map(|b| (b.name, b.id, b.config.version))
.unwrap_or((
String::from("unknown"),
String::new(),
Default::default(),
));
let version = if version.is_none() {
build_version.to_string()
} else {
@@ -174,14 +168,10 @@ impl super::KomodoResource for Deployment {
status: status.as_ref().and_then(|s| {
s.curr.container.as_ref().and_then(|c| c.status.to_owned())
}),
custom_name: optional_string(deployment.info.deployed_name)
.unwrap_or(deployment.config.custom_name),
image,
update_available,
swarm_id: deployment.config.swarm_id,
swarm_name,
server_id: deployment.config.server_id,
server_name,
build_id,
},
}
@@ -254,12 +244,11 @@ impl super::KomodoResource for Deployment {
}
async fn validate_update_config(
id: &str,
_id: &str,
config: &mut Self::PartialConfig,
user: &User,
) -> anyhow::Result<()> {
validate_config(config, user).await?;
handle_custom_name_update(id, config).await
validate_config(config, user).await
}
async fn post_update(
@@ -325,7 +314,7 @@ impl super::KomodoResource for Deployment {
SwarmOrServer::Swarm(swarm) => match swarm_request(
&swarm.config.server_ids,
RemoveSwarmServices {
services: vec![deployment.deployed_name().to_string()],
services: vec![deployment.name.clone()],
},
)
.await
@@ -363,7 +352,7 @@ impl super::KomodoResource for Deployment {
};
match periphery
.request(RemoveContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name.clone(),
signal: deployment.config.termination_signal.into(),
time: deployment.config.termination_timeout.into(),
})
@@ -447,100 +436,12 @@ async fn validate_config(
environment_vars_from_str(environment)
.context("Invalid environment")?;
}
if let Some(custom_name) = &config.custom_name
&& !custom_name.trim().is_empty()
{
config.custom_name =
Some(to_container_compatible_name(custom_name));
}
if let Some(extra_args) = &mut config.extra_args {
extra_args.retain(|v| !empty_or_only_spaces(v))
}
Ok(())
}
/// Handles a config update which changes the custom container name:
/// - Server mode: the container is renamed to keep the Deployment
/// matched to it. This runs before the update is written to the
/// database, so a failed rename rejects the update and nothing
/// is left mismatched.
/// - Swarm mode: services cannot be renamed, so the current
/// service name is pinned in info.deployed_name, and stays
/// in use until the next deploy recreates the service.
async fn handle_custom_name_update(
id: &str,
config: &PartialDeploymentConfig,
) -> anyhow::Result<()> {
let Some(custom_name) = &config.custom_name else {
return Ok(());
};
let deployment = super::get::<Deployment>(id).await?;
// custom_name is already made container compatible by validate_config.
let new_container_name = if custom_name.is_empty() {
deployment.name.as_str()
} else {
custom_name.as_str()
};
if deployment.custom_name() == new_container_name {
return Ok(());
}
if deployment.config.swarm_id.is_empty()
&& !deployment.config.server_id.is_empty()
{
let container_state =
get_deployment_state(&deployment.id).await?;
if container_state == DeploymentState::Unknown {
return Err(anyhow!(
"Cannot change custom container name when container status is unknown"
));
}
if container_state == DeploymentState::NotDeployed {
return Ok(());
}
let server =
super::get::<Server>(&deployment.config.server_id).await?;
periphery_client(&server)
.await?
.request(RenameContainer {
curr_name: deployment.deployed_name().to_string(),
new_name: new_container_name.to_string(),
})
.await
.context("Failed to rename container to the new custom name")?;
// Keep the tracked deployed name matched to the renamed container.
update_one_by_id(
&db_client().deployments,
&deployment.id,
database::mungos::update::Update::Set(
doc! { "info.deployed_name": new_container_name },
),
None,
)
.await
.context(
"Failed to update deployed name on db after container rename",
)?;
} else if !deployment.config.swarm_id.is_empty()
&& deployment.info.deployed_name.is_empty()
// Includes the Unknown state, in case a service
// is still running under the current name.
&& get_deployment_state(&deployment.id).await?
!= DeploymentState::NotDeployed
{
update_one_by_id(
&db_client().deployments,
&deployment.id,
database::mungos::update::Update::Set(
doc! { "info.deployed_name": deployment.deployed_name() },
),
None,
)
.await
.context("Failed to pin deployed service name on db")?;
}
Ok(())
}
pub async fn setup_deployment_execution(
deployment: &str,
user: &User,
+41 -214
View File
@@ -1,6 +1,5 @@
use std::{
collections::{HashMap, HashSet},
future::Future,
str::FromStr,
};
@@ -18,7 +17,7 @@ use database::{
},
};
use formatting::format_serror;
use futures_util::{TryStreamExt, future::join_all};
use futures_util::future::join_all;
use indexmap::IndexSet;
use komodo_client::{
api::{read::ExportResourcesToToml, write::CreateTag},
@@ -71,8 +70,7 @@ pub use action::{
refresh_action_state_cache, spawn_action_state_refresh_loop,
};
pub use build::{
get_build_state, refresh_build_state_cache,
spawn_build_state_refresh_loop,
refresh_build_state_cache, spawn_build_state_refresh_loop,
};
pub use deployment::setup_deployment_execution;
pub use procedure::{
@@ -84,8 +82,7 @@ pub use refresh::{
spawn_resource_refresh_loop,
};
pub use repo::{
get_repo_state, refresh_repo_state_cache,
spawn_repo_state_refresh_loop,
refresh_repo_state_cache, spawn_repo_state_refresh_loop,
};
pub use server::{rotate_server_keys, update_server_public_key};
@@ -265,156 +262,20 @@ pub async fn get<T: KomodoResource>(
/// Get full resource list with no permissions check.
pub async fn list_all_resources<T: KomodoResource>(
filters: impl Into<Option<Document>>,
limit: impl Into<Option<i64>>,
skip: impl Into<Option<u64>>,
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {
let options = FindOptions::builder()
.sort(doc! { "name": 1 })
.limit(limit)
.skip(skip);
find_collect(T::coll(), filters, options.build())
.await
.with_context(|| {
format!("Failed to pull {}s from mongo", T::resource_type())
})
find_collect(
T::coll(),
filters,
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
.await
.with_context(|| {
format!("Failed to pull {}s from mongo", T::resource_type())
})
}
pub struct ListItemsQueryOptions<T: KomodoResource> {
pub limit: u64,
pub page: u64,
pub sort_desc: bool,
pub sort_by: ListItemSort<T::ListItem>,
}
/// List item pagination for the `List<Resource>` apis, driving the
/// mongo cursor directly instead of collecting the full resource
/// list in memory. Each resource pulled from the cursor is checked
/// against the user permissions and converted to its list item, and
/// `filter` is applied before the item counts toward `limit` / `page`.
/// This is required because some list item fields (eg. state,
/// update available) are computed from in-memory caches rather than
/// stored on the database, so they cannot be part of the db query.
/// Stops pulling from the cursor as soon as the page is full.
pub async fn list_items_for_user<T: KomodoResource>(
mut query: ResourceQuery<T::QuerySpecifics>,
ListItemsQueryOptions {
limit,
page,
sort_desc,
sort_by,
}: ListItemsQueryOptions<T>,
user: &User,
permission: PermissionLevelAndSpecifics,
all_tags: &[Tag],
filter: impl Fn(&T::ListItem) -> bool,
) -> anyhow::Result<Vec<T::ListItem>> {
validate_resource_query_tags(&mut query, all_tags)?;
let mut filters = Document::new();
query.add_filters(&mut filters);
let mut permits =
crate::permission::load_list_permits::<T>(user, permission)
.await?;
let direction = if sort_desc { -1 } else { 1 };
let sort = match &sort_by {
ListItemSort::Name => doc! { "name": direction },
// Db field sorts use ascending name as the secondary sort.
ListItemSort::DbField(field) => {
doc! { *field: direction, "name": 1 }
}
// In-memory sorts pull items in ascending name db order,
// keeping name as the stable tiebreak for equal sort keys.
ListItemSort::InMemory(_) => doc! { "name": 1 },
};
let mut cursor =
T::coll().find(filters).sort(sort).await.with_context(|| {
format!("Failed to query db for {}s", T::resource_type())
})?;
let skip = page.saturating_mul(limit) as usize;
let take = if limit == 0 {
usize::MAX
} else {
limit as usize
};
let mut items = Vec::new();
if let ListItemSort::InMemory(compare) = sort_by {
// The sort can only be applied after all matching items
// are collected, so pagination also happens after the sort.
while let Some(resource) = cursor
.try_next()
.await
.context("Failed to pull next resource from db cursor")?
{
if !permits.permitted::<T>(&resource).await? {
continue;
}
let item = T::to_list_item(resource).await;
if !filter(&item) {
continue;
}
items.push(item);
}
// Stable sort keeps equal sort keys in ascending
// name order for both directions.
if sort_desc {
items.sort_by(|a, b| compare(b, a));
} else {
items.sort_by(|a, b| compare(a, b));
}
Ok(items.into_iter().skip(skip).take(take).collect())
} else {
let mut skipped = 0;
while let Some(resource) = cursor
.try_next()
.await
.context("Failed to pull next resource from db cursor")?
{
if !permits.permitted::<T>(&resource).await? {
continue;
}
let item = T::to_list_item(resource).await;
if !filter(&item) {
continue;
}
if skipped < skip {
skipped += 1;
continue;
}
items.push(item);
if take != usize::MAX && items.len() >= take {
break;
}
}
Ok(items)
}
}
/// How the `List<Resource>` apis sort the list items.
pub enum ListItemSort<I> {
/// Sort by name at the db level. Default.
Name,
/// Sort on a db stored field, keeping db level sort
/// and streaming pagination. Only usable when the db field
/// exactly matches the displayed list item field.
DbField(&'static str),
/// Compare list items in memory, required for fields computed
/// from the in memory caches (eg. state), or which diverge
/// from the db field (eg. linked repo sources).
/// Collects all matching items before applying pagination.
InMemory(InMemoryListItemSortFn<I>),
}
pub type InMemoryListItemSortFn<I> =
Box<dyn Fn(&I, &I) -> std::cmp::Ordering + Send>;
pub async fn list_for_user<T: KomodoResource>(
mut query: ResourceQuery<T::QuerySpecifics>,
limit: impl Into<Option<i64>>,
skip: impl Into<Option<u64>>,
user: &User,
permission: PermissionLevelAndSpecifics,
all_tags: &[Tag],
@@ -422,25 +283,38 @@ pub async fn list_for_user<T: KomodoResource>(
validate_resource_query_tags(&mut query, all_tags)?;
let mut filters = Document::new();
query.add_filters(&mut filters);
list_for_user_using_document::<T>(
filters, limit, skip, user, permission,
)
.await
list_for_user_using_document::<T>(filters, user, permission).await
}
// // pub async fn list_for_user_using_pattern<T: KomodoResource>(
// pattern: &str,
// query: ResourceQuery<T::QuerySpecifics>,
// user: &User,
// permissions: PermissionLevelAndSpecifics,
// all_tags: &[Tag],
// ) -> anyhow::Result<Vec<T::ListItem>> {
// let list = list_full_for_user_using_pattern::<T>(
// pattern,
// query,
// user,
// permissions,
// all_tags,
// )
// .await?
// .into_iter()
// .map(|resource| T::to_list_item(resource));
// Ok(join_all(list).await)
// }
pub async fn list_for_user_using_document<T: KomodoResource>(
filters: Document,
limit: impl Into<Option<i64>>,
skip: impl Into<Option<u64>>,
user: &User,
permission: PermissionLevelAndSpecifics,
) -> anyhow::Result<Vec<T::ListItem>> {
let list = list_resources_for_user::<T>(
filters, limit, skip, user, permission,
)
.await?
.into_iter()
.map(|resource| T::to_list_item(resource));
let list = list_resources_for_user::<T>(filters, user, permission)
.await?
.into_iter()
.map(|resource| T::to_list_item(resource));
Ok(join_all(list).await)
}
@@ -455,16 +329,13 @@ pub async fn list_for_user_using_document<T: KomodoResource>(
pub async fn list_full_for_user_using_pattern<T: KomodoResource>(
pattern: &str,
query: ResourceQuery<T::QuerySpecifics>,
limit: impl Into<Option<i64>>,
skip: impl Into<Option<u64>>,
user: &User,
permission: PermissionLevelAndSpecifics,
all_tags: &[Tag],
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {
let resources = list_full_for_user::<T>(
query, limit, skip, user, permission, all_tags,
)
.await?;
let resources =
list_full_for_user::<T>(query, user, permission, all_tags)
.await?;
let patterns = parse_string_list(pattern);
let mut names = HashSet::<String>::new();
@@ -497,45 +368,8 @@ pub async fn list_full_for_user_using_pattern<T: KomodoResource>(
)
}
/// Same as [list_full_for_user], but applies an additional in-memory
/// `filter` (eg. by state computed from the in memory caches), with
/// limit / page applied after the filter. Required because such
/// filters cannot be expressed in the db level query.
///
/// `filter` receives each resource by value, and returns
/// `Some(resource)` to keep it in the results, or `None` to drop it.
pub async fn list_full_for_user_filtered<T: KomodoResource, F>(
mut query: ResourceQuery<T::QuerySpecifics>,
limit: u64,
page: u64,
user: &User,
permission: PermissionLevelAndSpecifics,
all_tags: &[Tag],
filter: impl Fn(Resource<T::Config, T::Info>) -> F,
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>>
where
F: Future<Output = Option<Resource<T::Config, T::Info>>> + Send,
{
validate_resource_query_tags(&mut query, all_tags)?;
let mut filters = Document::new();
query.add_filters(&mut filters);
let mut permits =
crate::permission::load_list_permits::<T>(user, permission)
.await?;
crate::permission::list_resources_with_permits::<T, _>(
&mut permits,
Some(filters),
Some(limit as i64),
Some(page.saturating_mul(limit)),
filter,
)
.await
}
pub async fn list_full_for_user<T: KomodoResource>(
mut query: ResourceQuery<T::QuerySpecifics>,
limit: impl Into<Option<i64>>,
skip: impl Into<Option<u64>>,
user: &User,
permissions: PermissionLevelAndSpecifics,
all_tags: &[Tag],
@@ -543,14 +377,7 @@ pub async fn list_full_for_user<T: KomodoResource>(
validate_resource_query_tags(&mut query, all_tags)?;
let mut filters = Document::new();
query.add_filters(&mut filters);
list_resources_for_user::<T>(
filters,
limit,
skip,
user,
permissions,
)
.await
list_resources_for_user::<T>(filters, user, permissions).await
}
pub type IdResourceMap<T> = HashMap<
-3
View File
@@ -272,9 +272,6 @@ async fn validate_config(
execute: [
// Action
(RunAction, Action, action),
(CancelAction, Action, action),
// Procedure
(CancelProcedure, Procedure, procedure),
// Build
(RunBuild, Build, build),
(CancelBuild, Build, build),
+2 -12
View File
@@ -26,8 +26,7 @@ use crate::{
config::core_config,
helpers::{periphery_client, repo_link},
state::{
action_states, all_resources_cache, db_client, repo_state_cache,
repo_status_cache,
action_states, db_client, repo_state_cache, repo_status_cache,
},
};
@@ -64,12 +63,6 @@ impl super::KomodoResource for Repo {
let state = get_repo_state(&repo.id).await;
let status =
repo_status_cache().get(&repo.id).await.unwrap_or_default();
let server_name = all_resources_cache()
.load()
.servers
.get(&repo.config.server_id)
.map(|server| server.name.clone())
.unwrap_or_default();
RepoListItem {
name: repo.name,
id: repo.id,
@@ -78,7 +71,6 @@ impl super::KomodoResource for Repo {
resource_type: ResourceTargetVariant::Repo,
info: RepoListItemInfo {
server_id: repo.config.server_id,
server_name,
builder_id: repo.config.builder_id,
last_pulled_at: repo.info.last_pulled_at,
last_built_at: repo.info.last_built_at,
@@ -271,9 +263,7 @@ async fn validate_config(
Ok(())
}
/// The Repo state as computed for the repo list items,
/// from the in memory action states / state cache.
pub async fn get_repo_state(id: &String) -> RepoState {
async fn get_repo_state(id: &String) -> RepoState {
if let Some(state) = action_states()
.repo
.get(id)
+2 -15
View File
@@ -68,23 +68,14 @@ impl super::KomodoResource for Server {
server: Resource<Self::Config, Self::Info>,
) -> Self::ListItem {
let status = server_status_cache().get(&server.id).await;
let (periphery_info, system_info, system_stats) = status
.as_ref()
.map(|s| {
(
s.periphery_info.as_ref(),
s.system_info.as_ref(),
s.system_stats.as_ref(),
)
})
.unwrap_or_default();
let (
version,
public_key,
public_ip,
terminals_disabled,
container_terminals_disabled,
) = match periphery_info {
) = match status.as_ref().and_then(|s| s.periphery_info.as_ref())
{
Some(info) => (
Some(info.version.clone()),
Some(info.public_key.clone()),
@@ -103,10 +94,6 @@ impl super::KomodoResource for Server {
info: ServerListItemInfo {
state: status.as_ref().map(|s| s.state).unwrap_or_default(),
err: status.as_ref().and_then(|s| s.err.clone()),
stats: system_stats.map(Into::into),
core_count: system_info.and_then(|i| i.core_count),
logical_core_count: system_info
.and_then(|i| i.logical_core_count),
region: server.config.region,
address: optional_string(server.config.address),
external_address: optional_string(
+16 -22
View File
@@ -31,9 +31,7 @@ use crate::{
config::core_config,
helpers::{
periphery_client,
query::{
get_cached_stack_state, get_stack_state, get_swarm_or_server,
},
query::{get_stack_state, get_swarm_or_server},
repo_link,
swarm::swarm_request,
},
@@ -97,7 +95,20 @@ impl super::KomodoResource for Stack {
stack: Resource<Self::Config, Self::Info>,
) -> Self::ListItem {
let status = stack_status_cache().get(&stack.id).await;
let state = get_cached_stack_state(&stack.id).await;
let state = if action_states()
.stack
.get(&stack.id)
.await
.map(|s| s.get().map(|s| s.deploying))
.transpose()
.ok()
.flatten()
.unwrap_or_default()
{
StackState::Deploying
} else {
status.as_ref().map(|s| s.curr.state).unwrap_or_default()
};
let project_name = stack.project_name(false);
let services = status
.as_ref()
@@ -145,9 +156,8 @@ impl super::KomodoResource for Stack {
stack.config.repo,
stack.config.branch,
stack.config.git_https,
String::new(),
);
let (git_provider, repo, branch, git_https, linked_repo_name) =
let (git_provider, repo, branch, git_https) =
if stack.config.linked_repo.is_empty() {
default_git
} else {
@@ -161,7 +171,6 @@ impl super::KomodoResource for Stack {
r.config.repo.clone(),
r.config.branch.clone(),
r.config.git_https,
r.name.clone(),
)
})
.unwrap_or(default_git)
@@ -196,18 +205,6 @@ impl super::KomodoResource for Stack {
(false, None)
};
let all = all_resources_cache().load();
let server_name = all
.servers
.get(&stack.config.server_id)
.map(|server| server.name.clone())
.unwrap_or_default();
let swarm_name = all
.swarms
.get(&stack.config.swarm_id)
.map(|swarm| swarm.name.clone())
.unwrap_or_default();
StackListItem {
name: stack.name,
id: stack.id,
@@ -221,11 +218,8 @@ impl super::KomodoResource for Stack {
project_missing,
file_contents: !stack.config.file_contents.is_empty(),
swarm_id: stack.config.swarm_id,
swarm_name,
server_id: stack.config.server_id,
server_name,
linked_repo: stack.config.linked_repo,
linked_repo_name,
missing_files: stack.info.missing_files,
files_on_host: stack.config.files_on_host,
repo_link: repo_link(
+1 -4
View File
@@ -57,9 +57,8 @@ impl super::KomodoResource for ResourceSync {
resource_sync.config.repo,
resource_sync.config.branch,
resource_sync.config.git_https,
String::new(),
);
let (git_provider, repo, branch, git_https, linked_repo_name) =
let (git_provider, repo, branch, git_https) =
if resource_sync.config.linked_repo.is_empty() {
default_git
} else {
@@ -73,7 +72,6 @@ impl super::KomodoResource for ResourceSync {
r.config.repo.clone(),
r.config.branch.clone(),
r.config.git_https,
r.name.clone(),
)
})
.unwrap_or(default_git)
@@ -90,7 +88,6 @@ impl super::KomodoResource for ResourceSync {
files_on_host: resource_sync.config.files_on_host,
managed: resource_sync.config.managed,
linked_repo: resource_sync.config.linked_repo,
linked_repo_name,
repo_link: repo_link(
&git_provider,
&repo,
+1 -5
View File
@@ -80,7 +80,7 @@ pub async fn execute_compose_with_stack_and_server<
// Will check to ensure stack not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update(set_in_progress)?;
let _action_guard = action_state.update(set_in_progress)?;
// Send update here for UI to recheck action state
update_update(update.clone()).await?;
@@ -105,10 +105,6 @@ pub async fn execute_compose_with_stack_and_server<
refresh_server_cache(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
+1 -1
View File
@@ -281,7 +281,7 @@ async fn ensure_first_server_and_builder() {
name: name.to_string(),
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_ids: Some(vec![server.id]),
server_id: Some(server.id),
},
),
})
+1 -30
View File
@@ -19,14 +19,13 @@ use komodo_client::entities::{
swarm::SwarmState,
};
use mogh_cache::CloneCache;
use tokio_util::sync::CancellationToken;
use crate::{
config::core_config,
connection::PeripheryConnections,
helpers::{
action_state::ActionStates, all_resources::AllResourcesById,
builder::BuilderUsage, image_digest::ImageDigestCache,
image_digest::ImageDigestCache,
},
};
@@ -209,7 +208,6 @@ pub fn action_state_cache() -> &'static ActionStateCache {
ACTION_STATE_CACHE.get_or_init(Default::default)
}
/// Store all resources in local cache for fast lookup
pub fn all_resources_cache() -> &'static ArcSwap<AllResourcesById> {
static ALL_RESOURCES: OnceLock<ArcSwap<AllResourcesById>> =
OnceLock::new();
@@ -224,30 +222,3 @@ pub fn image_digest_cache() -> &'static ImageDigestCache {
OnceLock::new();
IMAGE_DIGEST_CACHE.get_or_init(ImageDigestCache::new)
}
/// Maps Builder id => downstream count map (eg server id => active count)
type BuilderUsageCache = CloneCache<String, Arc<BuilderUsage>>;
/// For builders with multiple downstream machines to choose.
/// Stores active build count for each downstream.
/// Maps Builder id => downstream count map
pub fn builder_usage_cache() -> &'static BuilderUsageCache {
static BUILDER_USAGE_CACHE: OnceLock<BuilderUsageCache> =
OnceLock::new();
BUILDER_USAGE_CACHE.get_or_init(Default::default)
}
type CancelCache = CloneCache<String, CancellationToken>;
/// Maps procedure id => CancellationToken
pub fn procedure_cancel_cache() -> &'static CancelCache {
static PROCEDURE_CANCEL_CACHE: OnceLock<CancelCache> =
OnceLock::new();
PROCEDURE_CANCEL_CACHE.get_or_init(Default::default)
}
/// Maps update id => CancellationToken
pub fn action_cancel_cache() -> &'static CancelCache {
static ACTION_CANCEL_CACHE: OnceLock<CancelCache> = OnceLock::new();
ACTION_CANCEL_CACHE.get_or_init(Default::default)
}
+6 -2
View File
@@ -338,7 +338,9 @@ fn build_cache_for_deployment<'a>(
// Makes sure things that aren't defined in toml (come through as None) actually get removed.
let config: DeploymentConfig =
deployment.config.clone().into();
let config: PartialDeploymentConfig = config.into();
let mut config: PartialDeploymentConfig = config.into();
Deployment::validate_partial_config(&mut config);
let mut diff =
Deployment::get_diff(original.config.clone(), config)?;
@@ -570,7 +572,9 @@ fn build_cache_for_stack<'a>(
// Merge toml resource config (partial) onto default resource config.
// Makes sure things that aren't defined in toml (come through as None) actually get removed.
let config: StackConfig = stack.config.clone().into();
let config: PartialStackConfig = config.into();
let mut config: PartialStackConfig = config.into();
Stack::validate_partial_config(&mut config);
let mut diff =
Stack::get_diff(original.config.clone(), config)?;
+1 -1
View File
@@ -74,7 +74,7 @@ pub async fn get_updates_for_execution<
let config: Resource::Config = resource.config.into();
resource.config = config.into();
// Resource::validate_partial_config(&mut resource.config);
Resource::validate_partial_config(&mut resource.config);
let mut diff = Resource::get_diff(
original.config.clone(),
+5 -10
View File
@@ -7,7 +7,6 @@ use komodo_client::entities::{
tag::Tag,
toml::{ResourceToml, ResourcesToml},
};
use partial_derive2::PartialDiff as _;
use toml::ToToml;
use crate::resource::KomodoResource;
@@ -16,7 +15,6 @@ pub mod deploy;
pub mod execute;
pub mod file;
pub mod remote;
pub mod replace_ids;
pub mod resources;
pub mod toml;
pub mod user_groups;
@@ -89,19 +87,16 @@ pub trait ResourceSyncTrait: ToToml + Sized {
)
}
// /// Apply any changes to incoming toml partial config
// /// before it is diffed against existing config
// fn validate_partial_config(_config: &mut Self::PartialConfig) {}
/// Apply any changes to incoming toml partial config
/// before it is diffed against existing config
fn validate_partial_config(_config: &mut Self::PartialConfig) {}
/// Diffs the declared toml (partial) against the full existing config.
/// Removes all fields from toml (partial) that haven't changed.
fn get_diff(
mut original: Self::Config,
original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
Self::replace_ids(&mut original);
Ok(original.partial_diff(update))
}
) -> anyhow::Result<Self::ConfigDiff>;
/// Apply any changes to computed config diff
/// before logging
-252
View File
@@ -1,252 +0,0 @@
use komodo_client::entities::{
action::Action,
alerter::Alerter,
build::Build,
builder::{Builder, BuilderConfig},
deployment::{Deployment, DeploymentImage},
procedure::Procedure,
repo::Repo,
server::Server,
stack::Stack,
swarm::Swarm,
sync::ResourceSync,
};
use crate::{
helpers::procedure::replace_procedure_stage_ids_with_names,
resource::KomodoResource, state::all_resources_cache,
};
/// Trait implementing replacing of linked resource ids with names for Resource Sync TOML.
pub trait ReplaceIds: KomodoResource {
/// Replace linked ids (server_id, build_id, etc) with the resource name.
fn replace_ids(_config: &mut Self::Config) {}
}
// These have no linked resource ids to replace
impl ReplaceIds for Server {}
impl ReplaceIds for Action {}
impl ReplaceIds for ResourceSync {
fn replace_ids(config: &mut Self::Config) {
let all = all_resources_cache().load();
config.linked_repo.clone_from(
all
.repos
.get(&config.linked_repo)
.map(|r| &r.name)
.unwrap_or(&String::new()),
);
}
}
impl ReplaceIds for Swarm {
fn replace_ids(config: &mut Self::Config) {
let all = all_resources_cache().load();
config.server_ids.iter_mut().for_each(|server_id| {
*server_id = all
.servers
.get(server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
});
let mut res = Vec::with_capacity(config.server_ids.capacity());
for server_id in &config.server_ids {
res.push(
all
.servers
.get(server_id)
.map(|s| s.name.clone())
.unwrap_or_default(),
);
}
}
}
impl ReplaceIds for Stack {
fn replace_ids(config: &mut Self::Config) {
let all = all_resources_cache().load();
config.swarm_id.clone_from(
all
.swarms
.get(&config.swarm_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
config.server_id.clone_from(
all
.servers
.get(&config.server_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
config.linked_repo.clone_from(
all
.repos
.get(&config.linked_repo)
.map(|r| &r.name)
.unwrap_or(&String::new()),
);
}
}
impl ReplaceIds for Deployment {
fn replace_ids(config: &mut Self::Config) {
let all = all_resources_cache().load();
config.swarm_id.clone_from(
all
.swarms
.get(&config.swarm_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
config.server_id.clone_from(
all
.servers
.get(&config.server_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
if let DeploymentImage::Build { build_id, .. } = &mut config.image
{
build_id.clone_from(
all
.builds
.get(build_id)
.map(|b| &b.name)
.unwrap_or(&String::new()),
);
}
}
}
impl ReplaceIds for Build {
fn replace_ids(config: &mut Self::Config) {
let all = all_resources_cache().load();
config.builder_id.clone_from(
all
.builders
.get(&config.builder_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
config.linked_repo.clone_from(
all
.repos
.get(&config.linked_repo)
.map(|r| &r.name)
.unwrap_or(&String::new()),
);
}
}
impl ReplaceIds for Repo {
fn replace_ids(config: &mut Self::Config) {
let all = all_resources_cache().load();
config.server_id.clone_from(
all
.servers
.get(&config.server_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
config.builder_id.clone_from(
all
.builders
.get(&config.builder_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
}
}
impl ReplaceIds for Builder {
fn replace_ids(config: &mut Self::Config) {
if let BuilderConfig::Server(config) = config {
let all = all_resources_cache().load();
for server_id in &mut config.server_ids {
*server_id = all
.servers
.get(server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
}
}
}
}
impl ReplaceIds for Procedure {
fn replace_ids(config: &mut Self::Config) {
replace_procedure_stage_ids_with_names(&mut config.stages);
}
}
/// Replaces the inner ids of [ResourceTarget] variants with the
/// referenced resource's name, looking each up in [AllResourcesById].
/// The `System` variant carries no id and is a no-op.
macro_rules! replace_resource_target_ids {
($target:expr, $all:expr, { $( $variant:ident => $field:ident ),* $(,)? }) => {
match $target {
komodo_client::entities::ResourceTarget::System(_) => {}
$(
komodo_client::entities::ResourceTarget::$variant(id) => {
*id = $all
.$field
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default();
}
)*
}
};
}
impl ReplaceIds for Alerter {
fn replace_ids(config: &mut Self::Config) {
if config.resources.is_empty()
&& config.except_resources.is_empty()
{
return;
}
let all = all_resources_cache().load();
for resource in &mut config.resources {
replace_resource_target_ids!(resource, all, {
Swarm => swarms,
Server => servers,
Stack => stacks,
Deployment => deployments,
Build => builds,
Repo => repos,
Procedure => procedures,
Action => actions,
Builder => builders,
Alerter => alerters,
ResourceSync => syncs,
});
}
for resource in &mut config.except_resources {
replace_resource_target_ids!(resource, all, {
Swarm => swarms,
Server => servers,
Stack => stacks,
Deployment => deployments,
Build => builds,
Repo => repos,
Procedure => procedures,
Action => actions,
Builder => builders,
Alerter => alerters,
ResourceSync => syncs,
});
}
}
}
+214 -14
View File
@@ -2,16 +2,28 @@ use std::collections::HashMap;
use formatting::{Color, bold, colored, muted};
use komodo_client::entities::{
ResourceTargetVariant, action::Action, alerter::Alerter,
build::Build, builder::Builder, deployment::Deployment,
procedure::Procedure, repo::Repo, server::Server, stack::Stack,
swarm::Swarm, sync::ResourceSync, tag::Tag, update::Log,
ResourceTargetVariant,
action::Action,
alerter::Alerter,
build::Build,
builder::{Builder, BuilderConfig},
deployment::{Deployment, DeploymentImage},
procedure::Procedure,
repo::Repo,
server::Server,
stack::Stack,
swarm::Swarm,
sync::ResourceSync,
tag::Tag,
update::Log,
user::sync_user,
};
use partial_derive2::MaybeNone;
use partial_derive2::{MaybeNone, PartialDiff};
use crate::{
helpers::procedure::replace_procedure_stage_ids_with_names,
resource::{KomodoResource, ResourceMetaUpdate},
state::all_resources_cache,
sync::{ToUpdateItem, execute::run_update_meta},
};
@@ -21,19 +33,127 @@ use super::{
include_resource_by_tags,
};
impl ResourceSyncTrait for Server {}
impl ResourceSyncTrait for Server {
fn get_diff(
original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Server {}
impl ResourceSyncTrait for Swarm {}
impl ResourceSyncTrait for Swarm {
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
let all = all_resources_cache().load();
original.server_ids.iter_mut().for_each(|server_id| {
*server_id = all
.servers
.get(server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
});
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Swarm {}
impl ResourceSyncTrait for Deployment {}
impl ResourceSyncTrait for Deployment {
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
let all = all_resources_cache().load();
original.swarm_id = all
.swarms
.get(&original.swarm_id)
.map(|s| s.name.clone())
.unwrap_or_default();
original.server_id = all
.servers
.get(&original.server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
if let DeploymentImage::Build { build_id, version } =
&original.image
{
original.image = DeploymentImage::Build {
build_id: all
.builds
.get(build_id)
.map(|b| b.name.clone())
.unwrap_or_default(),
version: *version,
};
}
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Deployment {}
impl ResourceSyncTrait for Stack {}
impl ResourceSyncTrait for Stack {
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
let all = all_resources_cache().load();
original.swarm_id = all
.swarms
.get(&original.swarm_id)
.map(|s| s.name.clone())
.unwrap_or_default();
original.server_id = all
.servers
.get(&original.server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
original.linked_repo = all
.repos
.get(&original.linked_repo)
.map(|r| r.name.clone())
.unwrap_or_default();
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Stack {}
impl ResourceSyncTrait for Build {
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
let all = all_resources_cache().load();
original.builder_id = all
.builders
.get(&original.builder_id)
.map(|b| b.name.clone())
.unwrap_or_default();
original.linked_repo = all
.repos
.get(&original.linked_repo)
.map(|r| r.name.clone())
.unwrap_or_default();
Ok(original.partial_diff(update))
}
fn validate_diff(diff: &mut Self::ConfigDiff) {
if let Some((_, to)) = &diff.version {
// When setting a build back to "latest" version,
@@ -45,18 +165,76 @@ impl ResourceSyncTrait for Build {
}
}
}
impl ExecuteResourceSync for Build {}
impl ResourceSyncTrait for Repo {}
impl ResourceSyncTrait for Repo {
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
let all = all_resources_cache().load();
// Need to replace server id with name
original.server_id = all
.servers
.get(&original.server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
// Need to replace builder id with name
original.builder_id = all
.builders
.get(&original.builder_id)
.map(|s| s.name.clone())
.unwrap_or_default();
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Repo {}
impl ResourceSyncTrait for Alerter {}
impl ResourceSyncTrait for Alerter {
fn get_diff(
original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Alerter {}
impl ResourceSyncTrait for Builder {}
impl ResourceSyncTrait for Builder {
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
// need to replace server builder id with name
if let BuilderConfig::Server(config) = &mut original {
let all = all_resources_cache().load();
config.server_id = all
.servers
.get(&config.server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
}
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Builder {}
impl ResourceSyncTrait for Action {}
impl ResourceSyncTrait for Action {
fn get_diff(
original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Action {}
impl ResourceSyncTrait for ResourceSync {
@@ -138,11 +316,33 @@ impl ResourceSyncTrait for ResourceSync {
// The sync must be files on host mode OR git repo mode
(files_on_host || !config.repo.as_deref().unwrap_or_default().is_empty() || !config.linked_repo.as_deref().unwrap_or_default().is_empty())
}
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
let all = all_resources_cache().load();
original.linked_repo = all
.repos
.get(&original.linked_repo)
.map(|r| r.name.clone())
.unwrap_or_default();
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for ResourceSync {}
impl ResourceSyncTrait for Procedure {}
impl ResourceSyncTrait for Procedure {
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
replace_procedure_stage_ids_with_names(&mut original.stages);
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Procedure {
async fn execute_sync_updates(
+164 -8
View File
@@ -6,7 +6,7 @@ use komodo_client::entities::{
action::Action,
alerter::Alerter,
build::Build,
builder::{Builder, PartialBuilderConfig},
builder::{Builder, BuilderConfig, PartialBuilderConfig},
deployment::{Deployment, DeploymentImage},
procedure::Procedure,
repo::Repo,
@@ -21,7 +21,8 @@ use komodo_client::entities::{
use partial_derive2::{MaybeNone, PartialDiff};
use crate::{
resource::KomodoResource, sync::replace_ids::ReplaceIds,
helpers::procedure::replace_procedure_stage_ids_with_names,
resource::KomodoResource, state::all_resources_cache,
};
pub const TOML_PRETTY_OPTIONS: toml_pretty::Options =
@@ -34,7 +35,11 @@ pub const TOML_PRETTY_OPTIONS: toml_pretty::Options =
inline_array: false,
};
pub trait ToToml: ReplaceIds {
pub trait ToToml: KomodoResource {
/// Replace linked ids (server_id, build_id, etc) with the resource name.
fn replace_ids(_resource: &mut Resource<Self::Config, Self::Info>) {
}
fn edit_config_object(
_resource: &ResourceToml<Self::PartialConfig>,
config: IndexMap<String, serde_json::Value>,
@@ -103,7 +108,7 @@ pub fn resource_push_to_toml<R: ToToml>(
toml: &mut String,
all_tags: &HashMap<String, Tag>,
) -> anyhow::Result<()> {
R::replace_ids(&mut resource.config);
R::replace_ids(&mut resource);
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
@@ -152,12 +157,48 @@ pub fn convert_resource<R: KomodoResource>(
}
}
// These have no linked resource ids to replace
impl ToToml for Alerter {}
impl ToToml for Server {}
impl ToToml for Action {}
impl ToToml for Alerter {}
impl ToToml for ResourceSync {}
impl ToToml for ResourceSync {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
let all = all_resources_cache().load();
resource.config.linked_repo.clone_from(
all
.repos
.get(&resource.config.linked_repo)
.map(|r| &r.name)
.unwrap_or(&String::new()),
);
}
}
impl ToToml for Swarm {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
let all = all_resources_cache().load();
resource.config.server_ids.iter_mut().for_each(|server_id| {
*server_id = all
.servers
.get(server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
});
let mut res =
Vec::with_capacity(resource.config.server_ids.capacity());
for server_id in &resource.config.server_ids {
res.push(
all
.servers
.get(server_id)
.map(|s| s.name.clone())
.unwrap_or_default(),
);
}
}
fn edit_config_object(
_resource: &ResourceToml<Self::PartialConfig>,
config: IndexMap<String, serde_json::Value>,
@@ -179,6 +220,34 @@ impl ToToml for Swarm {
}
impl ToToml for Stack {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
let all = all_resources_cache().load();
resource.config.swarm_id.clone_from(
all
.swarms
.get(&resource.config.swarm_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
resource.config.server_id.clone_from(
all
.servers
.get(&resource.config.server_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
resource.config.linked_repo.clone_from(
all
.repos
.get(&resource.config.linked_repo)
.map(|r| &r.name)
.unwrap_or(&String::new()),
);
}
fn edit_config_object(
_resource: &ResourceToml<Self::PartialConfig>,
config: IndexMap<String, serde_json::Value>,
@@ -199,6 +268,38 @@ impl ToToml for Stack {
}
impl ToToml for Deployment {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
let all = all_resources_cache().load();
resource.config.swarm_id.clone_from(
all
.swarms
.get(&resource.config.swarm_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
resource.config.server_id.clone_from(
all
.servers
.get(&resource.config.server_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
if let DeploymentImage::Build { build_id, .. } =
&mut resource.config.image
{
build_id.clone_from(
all
.builds
.get(build_id)
.map(|b| &b.name)
.unwrap_or(&String::new()),
);
}
}
fn edit_config_object(
resource: &ResourceToml<Self::PartialConfig>,
config: IndexMap<String, serde_json::Value>,
@@ -240,6 +341,24 @@ impl ToToml for Deployment {
}
impl ToToml for Build {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
let all = all_resources_cache().load();
resource.config.builder_id.clone_from(
all
.builders
.get(&resource.config.builder_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
resource.config.linked_repo.clone_from(
all
.repos
.get(&resource.config.linked_repo)
.map(|r| &r.name)
.unwrap_or(&String::new()),
);
}
fn edit_config_object(
resource: &ResourceToml<Self::PartialConfig>,
config: IndexMap<String, serde_json::Value>,
@@ -272,6 +391,24 @@ impl ToToml for Build {
}
impl ToToml for Repo {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
let all = all_resources_cache().load();
resource.config.server_id.clone_from(
all
.servers
.get(&resource.config.server_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
resource.config.builder_id.clone_from(
all
.builders
.get(&resource.config.builder_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
);
}
fn edit_config_object(
_resource: &ResourceToml<Self::PartialConfig>,
config: IndexMap<String, serde_json::Value>,
@@ -293,6 +430,19 @@ impl ToToml for Repo {
}
impl ToToml for Builder {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
if let BuilderConfig::Server(config) = &mut resource.config {
let all = all_resources_cache().load();
config.server_id.clone_from(
all
.servers
.get(&config.server_id)
.map(|s| &s.name)
.unwrap_or(&String::new()),
)
}
}
fn push_additional(
resource: ResourceToml<Self::PartialConfig>,
toml: &mut String,
@@ -326,8 +476,8 @@ impl ToToml for Builder {
.into_iter()
.map(|(key, value)| {
match key.as_str() {
"server_ids" => {
return (String::from("servers"), value);
"server_id" => {
return (String::from("server"), value);
}
_ => {}
}
@@ -346,6 +496,12 @@ impl ToToml for Builder {
}
impl ToToml for Procedure {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
replace_procedure_stage_ids_with_names(
&mut resource.config.stages,
);
}
fn push_to_toml_string(
mut resource: ResourceToml<Self::PartialConfig>,
toml: &mut String,
+3 -3
View File
@@ -84,9 +84,9 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
proposed_resource.config.into();
proposed_resource.config = propsed_config.into();
// Resource::validate_partial_config(
// &mut proposed_resource.config,
// );
Resource::validate_partial_config(
&mut proposed_resource.config,
);
let proposed = super::toml::resource_toml_to_toml_string::<
Resource,
+1 -1
View File
@@ -2,6 +2,7 @@
name = "komodo_periphery"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
@@ -33,7 +34,6 @@ git.workspace = true
mogh_error = { workspace = true, features = ["axum"] }
async_timing_util.workspace = true
mogh_resolver.workspace = true
svi.workspace = true
# external
hickory-resolver.workspace = true
serde_yaml_ng.workspace = true
+1 -1
View File
@@ -1,6 +1,6 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
FROM rust:1.97.1-trixie AS builder
FROM rust:1.95.0-trixie AS builder
RUN cargo install cargo-strip
WORKDIR /builder
+38 -103
View File
@@ -5,13 +5,13 @@ use std::{
use anyhow::{Context, anyhow};
use command::{
CommandOptions, KomodoCommandMode,
run_komodo_command_with_sanitization, run_komodo_standard_command,
KomodoCommandMode, run_komodo_command_with_sanitization,
run_komodo_standard_command,
};
use formatting::format_serror;
use interpolate::Interpolator;
use komodo_client::entities::{
EnvironmentVar, NoData, all_logs_success,
EnvironmentVar, all_logs_success,
build::{Build, BuildConfig},
environment_vars_from_str, optional_string,
to_path_compatible_name,
@@ -19,19 +19,17 @@ use komodo_client::entities::{
};
use mogh_resolver::Resolve;
use periphery_client::api::build::{
self, CancelBuild, GetDockerfileContentsOnHost,
self, GetDockerfileContentsOnHost,
GetDockerfileContentsOnHostResponse, PruneBuilders, PruneBuildx,
WriteDockerfileContentsToHost,
};
use tokio::fs;
use tokio_util::sync::CancellationToken;
use tracing::Instrument;
use crate::{
config::periphery_config,
docker::docker_login,
helpers::{format_extra_args, format_labels},
state::build_cancel_cache,
};
mod helpers;
@@ -135,8 +133,7 @@ impl Resolve<crate::api::Args> for build::Build {
fields(
id = args.id.to_string(),
core = args.core,
build_id = self.build.id,
build_name = self.build.name,
build = self.build.name,
repo = self.repo.as_ref().map(|repo| &repo.name),
)
)]
@@ -266,21 +263,14 @@ impl Resolve<crate::api::Args> for build::Build {
}
};
let cancel = CancellationToken::new();
build_cancel_cache()
.insert(build.id.clone(), cancel.clone())
.await;
// Pre Build
if !pre_build.is_none() {
let pre_build_path = build_path.join(&pre_build.path);
let span = info_span!("RunPreBuild");
if let Some(log) = run_komodo_command_with_sanitization(
"Pre Build",
pre_build_path.as_path(),
&pre_build.command,
CommandOptions::default()
.path(pre_build_path.as_path())
.cancel(cancel.clone()),
if pre_build.shell_mode {
KomodoCommandMode::Shell
} else {
@@ -294,73 +284,53 @@ impl Resolve<crate::api::Args> for build::Build {
let success = log.success;
logs.push(log);
if !success {
build_cancel_cache().remove(&build.id).await;
return Ok(logs);
}
}
}
// Get command parts
// Do this in fallible block to ensure
// cancel token removed from cache on failure.
let command = async {
// Add VERSION to build args (if not already there)
let mut build_args = environment_vars_from_str(build_args)
.context("Invalid build_args")?;
if !build_args.iter().any(|a| a.variable == "VERSION") {
build_args.push(EnvironmentVar {
variable: String::from("VERSION"),
value: build.config.version.to_string(),
});
}
let build_args = parse_build_args(&build_args);
let secret_args = environment_vars_from_str(secret_args)
.context("Invalid secret_args")?;
let command_secret_args =
parse_secret_args(&secret_args, &build_path).await?;
// Add VERSION to build args (if not already there)
let mut build_args = environment_vars_from_str(build_args)
.context("Invalid build_args")?;
if !build_args.iter().any(|a| a.variable == "VERSION") {
build_args.push(EnvironmentVar {
variable: String::from("VERSION"),
value: build.config.version.to_string(),
});
}
let build_args = parse_build_args(&build_args);
let labels = format_labels(
&environment_vars_from_str(labels)
.context("Invalid labels")?,
);
let secret_args = environment_vars_from_str(secret_args)
.context("Invalid secret_args")?;
let command_secret_args =
parse_secret_args(&secret_args, &build_path).await?;
let extra_args = format_extra_args(extra_args);
let labels = format_labels(
&environment_vars_from_str(labels).context("Invalid labels")?,
);
let buildx = if *use_buildx { " buildx" } else { "" };
let extra_args = format_extra_args(extra_args);
let image_tags = build
.get_image_tags_as_arg(
commit_hash.as_deref(),
&additional_tags,
)
.context("Failed to parse image tags into command")?;
let buildx = if *use_buildx { " buildx" } else { "" };
let maybe_push = if should_push { " --push" } else { "" };
let image_tags = build
.get_image_tags_as_arg(commit_hash.as_deref(), &additional_tags)
.context("Failed to parse image tags into command")?;
// Construct command
let command = format!(
"docker{buildx} build{build_args}{command_secret_args}{extra_args}{labels}{image_tags}{maybe_push} -f {dockerfile_path} .",
);
let maybe_push = if should_push { " --push" } else { "" };
anyhow::Ok(command)
}.await;
let command = match command {
Ok(command) => command,
Err(e) => {
build_cancel_cache().remove(&build.id).await;
return Err(e);
}
};
// Construct command
let command = format!(
"docker{buildx} build{build_args}{command_secret_args}{extra_args}{labels}{image_tags}{maybe_push} -f {dockerfile_path} .",
);
let span = info_span!("RunDockerBuild");
if let Some(build_log) = run_komodo_command_with_sanitization(
"Docker Build",
build_path.as_ref(),
command,
CommandOptions::default()
.path(build_path.as_path())
.cancel(cancel),
KomodoCommandMode::Shell,
&replacers,
)
@@ -370,39 +340,12 @@ impl Resolve<crate::api::Args> for build::Build {
logs.push(build_log);
};
build_cancel_cache().remove(&build.id).await;
Ok(logs)
}
}
//
impl Resolve<crate::api::Args> for CancelBuild {
#[instrument(
"CancelBuild",
skip_all,
fields(
build_id = self.id,
id = args.id.to_string(),
core = args.core,
)
)]
async fn resolve(
self,
args: &crate::api::Args,
) -> anyhow::Result<NoData> {
build_cancel_cache()
.get(&self.id)
.await
.context("No running build found")?
.cancel();
Ok(NoData {})
}
}
//
impl Resolve<crate::api::Args> for PruneBuilders {
#[instrument(
"PruneBuilders",
@@ -418,12 +361,8 @@ impl Resolve<crate::api::Args> for PruneBuilders {
) -> anyhow::Result<Log> {
let command = String::from("docker builder prune -a -f");
Ok(
run_komodo_standard_command(
"Prune Builders",
command,
CommandOptions::default(),
)
.await,
run_komodo_standard_command("Prune Builders", None, command)
.await,
)
}
}
@@ -445,12 +384,8 @@ impl Resolve<crate::api::Args> for PruneBuildx {
) -> anyhow::Result<Log> {
let command = String::from("docker buildx prune -a -f");
Ok(
run_komodo_standard_command(
"Prune Buildx",
command,
CommandOptions::default(),
)
.await,
run_komodo_standard_command("Prune Buildx", None, command)
.await,
)
}
}
+95 -109
View File
@@ -1,13 +1,9 @@
use std::{
borrow::Cow, fmt::Write, path::PathBuf, sync::LazyLock,
time::Duration,
};
use std::{borrow::Cow, fmt::Write, path::PathBuf};
use anyhow::{Context, anyhow};
use command::{
CommandOptions, KomodoCommandMode,
run_komodo_command_with_sanitization, run_komodo_shell_command,
run_komodo_standard_command,
KomodoCommandMode, run_komodo_command_with_sanitization,
run_komodo_shell_command, run_komodo_standard_command,
};
use formatting::format_serror;
use git::write_commit_file;
@@ -15,7 +11,11 @@ use interpolate::Interpolator;
use komodo_client::{
entities::{
FileContents, RepoExecutionResponse, all_logs_success,
stack::{AdditionalEnvFile, StackRemoteFileContents},
stack::{
AdditionalEnvFile, ComposeFile, ComposeService,
ComposeServiceDeploy, StackRemoteFileContents,
StackServiceNames,
},
to_path_compatible_name,
update::Log,
},
@@ -28,7 +28,7 @@ use tracing::Instrument;
use crate::{
config::periphery_config,
docker::compose::{docker_compose, parse_compose_services},
docker::compose::docker_compose,
helpers::{format_extra_args, format_log_grep},
stack::{
maybe_login_registry, pull_or_clone_stack, validate_files,
@@ -54,16 +54,12 @@ impl Resolve<crate::api::Args> for GetComposeLog {
Default::default()
};
let command = format!(
"{docker_compose} -p {project} logs --tail {tail}{timestamps} -- {}",
"{docker_compose} -p {project} logs --tail {tail}{timestamps} {}",
services.join(" ")
);
Ok(
run_komodo_standard_command(
"Get Stack Log",
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
)
.await,
run_komodo_standard_command("Get Stack Log", None, command)
.await,
)
}
}
@@ -89,21 +85,12 @@ impl Resolve<crate::api::Args> for GetComposeLogSearch {
Default::default()
};
let command = format!(
"{docker_compose} -p {} logs --tail 5000{timestamps} -- {} 2>&1 | {grep}",
escape(project.into()),
services
.iter()
.map(|service| escape(service.into()))
.collect::<Vec<_>>()
.join(" ")
"{docker_compose} -p {project} logs --tail 5000{timestamps} {} 2>&1 | {grep}",
services.join(" ")
);
Ok(
run_komodo_shell_command(
"Search Stack Log",
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
)
.await,
run_komodo_shell_command("Search Stack Log", None, command)
.await,
)
}
}
@@ -353,8 +340,7 @@ impl Resolve<crate::api::Args> for ComposePull {
let service_args = if services.is_empty() {
String::new()
} else {
// `--` so a service beginning with `-` is not parsed as a flag.
format!(" -- {}", services.join(" "))
format!(" {}", services.join(" "))
};
let file_args = stack.compose_file_paths().join(" -f ");
@@ -404,8 +390,8 @@ impl Resolve<crate::api::Args> for ComposePull {
};
let Some(log) = run_komodo_command_with_sanitization(
"Compose Pull",
run_directory.as_path(),
pull_command,
CommandOptions::default().path(run_directory.as_path()),
mode,
&replacers,
)
@@ -502,8 +488,8 @@ impl Resolve<crate::api::Args> for ComposeUp {
let span = info_span!("ExecutePreDeploy");
if let Some(log) = run_komodo_command_with_sanitization(
"Pre Deploy",
pre_deploy_path.as_path(),
&stack.config.pre_deploy.command,
CommandOptions::default().path(pre_deploy_path.as_path()),
if stack.config.pre_deploy.shell_mode {
KomodoCommandMode::Shell
} else {
@@ -526,8 +512,7 @@ impl Resolve<crate::api::Args> for ComposeUp {
let service_args = if services.is_empty() {
String::new()
} else {
// `--` so a service beginning with `-` is not parsed as a flag.
format!(" -- {}", services.join(" "))
format!(" {}", services.join(" "))
};
let file_args = stack.compose_file_paths().join(" -f ");
@@ -574,55 +559,70 @@ impl Resolve<crate::api::Args> for ComposeUp {
return Ok(res);
}
};
let span = info_span!("GetComposeConfig", command);
let mut config_log = if wrapped {
run_komodo_shell_command(
"Compose Config",
command,
CommandOptions::default().path(run_directory.as_path()),
)
.instrument(span)
.await
let mode = if wrapped {
KomodoCommandMode::Shell
} else {
run_komodo_standard_command(
"Compose Config",
command,
CommandOptions::default().path(run_directory.as_path()),
)
.instrument(span)
.await
KomodoCommandMode::Standard
};
let span = info_span!("GetComposeConfig", command);
let Some(config_log) = run_komodo_command_with_sanitization(
"Compose Config",
run_directory.as_path(),
command,
mode,
&replacers,
)
.instrument(span)
.await
else {
unreachable!()
};
if !config_log.success {
config_log.sanitize(&replacers);
res.logs.push(config_log);
return Ok(res);
}
// attach sanitized merged config in any case.
res.merged_config =
svi::replace_in_string(&config_log.stdout, &replacers).into();
if let Err(e) = parse_compose_services(
&config_log.stdout,
&project_name,
&mut res.services,
) {
config_log.sanitize(&replacers);
res.logs.push(config_log);
res.logs.push(Log::error(
"Parse Compose Services",
format_serror(&e.into()),
));
// early return with error log
// including sanitized config log
// and parse error for clear view
// of what the issue might be.
return Ok(res);
let compose =
serde_yaml_ng::from_str::<ComposeFile>(&config_log.stdout)
.context("Failed to parse compose contents")?;
// Store sanitized compose config output
res.merged_config = Some(config_log.stdout);
for (
service_name,
ComposeService {
container_name,
deploy,
image,
},
) in compose.services
{
let image = image.unwrap_or_default();
match deploy {
Some(ComposeServiceDeploy {
replicas: Some(replicas),
}) if replicas > 1 => {
for i in 1..1 + replicas {
res.services.push(StackServiceNames {
container_name: format!(
"{project_name}-{service_name}-{i}"
),
service_name: format!("{service_name}-{i}"),
image: image.clone(),
image_digest: None,
});
}
}
_ => {
res.services.push(StackServiceNames {
container_name: container_name.unwrap_or_else(|| {
format!("{project_name}-{service_name}")
}),
service_name,
image,
image_digest: None,
});
}
}
}
config_log.sanitize(&replacers);
res.logs.push(config_log);
}
if stack.config.run_build {
@@ -651,8 +651,8 @@ impl Resolve<crate::api::Args> for ComposeUp {
let span = info_span!("ExecuteComposeBuild");
let Some(log) = run_komodo_command_with_sanitization(
"Compose Build",
run_directory.as_path(),
command,
CommandOptions::default().path(run_directory.as_path()),
mode,
&replacers,
)
@@ -694,8 +694,8 @@ impl Resolve<crate::api::Args> for ComposeUp {
let span = info_span!("RunComposePull");
let Some(log) = run_komodo_command_with_sanitization(
"Compose Pull",
run_directory.as_path(),
command,
CommandOptions::default().path(run_directory.as_path()),
mode,
&replacers,
)
@@ -742,8 +742,8 @@ impl Resolve<crate::api::Args> for ComposeUp {
let span = info_span!("ExecuteComposeUp");
let Some(log) = run_komodo_command_with_sanitization(
"Compose Up",
run_directory.as_path(),
command,
CommandOptions::default().path(run_directory.as_path()),
KomodoCommandMode::Shell,
&replacers,
)
@@ -762,8 +762,8 @@ impl Resolve<crate::api::Args> for ComposeUp {
let span = info_span!("ExecutePostDeploy");
if let Some(log) = run_komodo_command_with_sanitization(
"Post Deploy",
post_deploy_path.as_path(),
&stack.config.post_deploy.command,
CommandOptions::default().path(post_deploy_path.as_path()),
if stack.config.post_deploy.shell_mode {
KomodoCommandMode::Shell
} else {
@@ -803,8 +803,8 @@ impl Resolve<crate::api::Args> for ComposeExecution {
let docker_compose = docker_compose();
let log = run_komodo_standard_command(
"Compose Command",
None,
format!("{docker_compose} -p {project} {command}"),
CommandOptions::default(),
)
.await;
Ok(log)
@@ -897,28 +897,23 @@ impl Resolve<crate::api::Args> for ComposeRun {
let project_name = stack.project_name(true);
// Comes straight off the request and interpolated into a
// command string which is handed to `bash -c`, so must be escaped.
let service = escape(Cow::Borrowed(&service));
// Parse wrapper configuration
let compose_cmd_wrapper =
parse_multiline_command(&stack.config.compose_cmd_wrapper);
// If wrapper_include is empty but wrapper is set, use default ["up"] for backward compatibility
static DEFAULT_INCLUDE: LazyLock<Vec<String>> =
LazyLock::new(|| vec![String::from("up")]);
let default_include = vec![String::from("up")];
let wrapper_include =
if stack.config.compose_cmd_wrapper_include.is_empty()
&& !compose_cmd_wrapper.is_empty()
{
&DEFAULT_INCLUDE
&default_include
} else {
&stack.config.compose_cmd_wrapper_include
};
if pull.unwrap_or_default() {
let pull_command = format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull -- {service}",
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull {service}",
);
let (pull_command, wrapped) = match maybe_wrap_command(
pull_command,
@@ -936,8 +931,8 @@ impl Resolve<crate::api::Args> for ComposeRun {
};
let Some(pull_log) = run_komodo_command_with_sanitization(
"Compose Pull",
run_directory.as_path(),
pull_command,
CommandOptions::default().path(run_directory.as_path()),
mode,
&replacers,
)
@@ -964,25 +959,17 @@ impl Resolve<crate::api::Args> for ComposeRun {
run_flags.push_str(" --service-ports");
}
if let Some(dir) = workdir.as_ref() {
run_flags
.push_str(&format!(" --workdir {}", escape(dir.into())));
run_flags.push_str(&format!(" --workdir {dir}"));
}
if let Some(user) = user.as_ref() {
run_flags.push_str(&format!(" --user {}", escape(user.into())));
run_flags.push_str(&format!(" --user {user}"));
}
if let Some(entrypoint) = entrypoint.as_ref() {
run_flags.push_str(&format!(
" --entrypoint {}",
escape(entrypoint.into())
));
run_flags.push_str(&format!(" --entrypoint {entrypoint}"));
}
if let Some(env) = env {
for (k, v) in env {
// Escaped as a single KEY=VALUE token, which is what compose expects.
run_flags.push_str(&format!(
" -e {}",
escape(format!("{k}={v}").into())
));
run_flags.push_str(&format!(" -e {}={} ", k, v));
}
}
@@ -992,7 +979,7 @@ impl Resolve<crate::api::Args> for ComposeRun {
.map(|argv| {
let joined = argv
.iter()
.map(|s| escape(s.into()).into_owned())
.map(|s| escape(Cow::Borrowed(s)).into_owned())
.collect::<Vec<_>>()
.join(" ");
format!(" {joined}")
@@ -1000,7 +987,7 @@ impl Resolve<crate::api::Args> for ComposeRun {
.unwrap_or_default();
let run_command = format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} run{run_flags} -- {service}{command_args}",
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} run{run_flags} {service}{command_args}",
);
let (run_command, _) = match maybe_wrap_command(
run_command,
@@ -1015,8 +1002,8 @@ impl Resolve<crate::api::Args> for ComposeRun {
let span = info_span!("RunComposeRun", run_command);
let Some(log) = run_komodo_command_with_sanitization(
"Compose Run",
run_directory.as_path(),
run_command,
CommandOptions::default().path(run_directory.as_path()),
KomodoCommandMode::Shell,
&replacers,
)
@@ -1094,13 +1081,12 @@ async fn compose_down(
let service_args = if services.is_empty() {
String::new()
} else {
// `--` so a service beginning with `-` is not parsed as a flag.
format!(" -- {}", services.join(" "))
format!(" {}", services.join(" "))
};
let log = run_komodo_standard_command(
"Compose Down",
None,
format!("{docker_compose} -p {project} down{service_args}"),
CommandOptions::default(),
)
.await;
let success = log.success;
+40 -82
View File
@@ -1,9 +1,6 @@
use std::time::Duration;
use anyhow::Context;
use command::{
CommandOptions, run_komodo_shell_command,
run_komodo_standard_command,
run_komodo_shell_command, run_komodo_standard_command,
};
use futures_util::future::join_all;
use komodo_client::entities::{
@@ -15,7 +12,6 @@ use komodo_client::entities::{
};
use mogh_resolver::Resolve;
use periphery_client::api::container::*;
use shell_escape::unix::escape;
use crate::{
docker::{stats::get_container_stats, stop_container_command},
@@ -62,16 +58,11 @@ impl Resolve<crate::api::Args> for GetContainerLog {
} else {
Default::default()
};
// `--` so a name beginning with `-` is not parsed as a flag.
let command =
format!("docker logs --tail {tail}{timestamps} -- {name}");
format!("docker logs {name} --tail {tail}{timestamps}");
Ok(
run_komodo_standard_command(
"Get container log",
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
)
.await,
run_komodo_standard_command("Get container log", None, command)
.await,
)
}
}
@@ -96,15 +87,14 @@ impl Resolve<crate::api::Args> for GetContainerLogSearch {
} else {
Default::default()
};
let name = escape(name.into());
let command = format!(
"docker logs {name} --tail 5000{timestamps} 2>&1 | {grep}"
);
Ok(
run_komodo_shell_command(
"Get container log grep",
None,
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
)
.await,
)
@@ -173,8 +163,8 @@ impl Resolve<crate::api::Args> for StartContainer {
Ok(
run_komodo_standard_command(
"Docker Start",
format!("docker start -- {}", self.name),
CommandOptions::default(),
None,
format!("docker start {}", self.name),
)
.await,
)
@@ -200,8 +190,8 @@ impl Resolve<crate::api::Args> for RestartContainer {
Ok(
run_komodo_standard_command(
"Docker Restart",
format!("docker restart -- {}", self.name),
CommandOptions::default(),
None,
format!("docker restart {}", self.name),
)
.await,
)
@@ -227,8 +217,8 @@ impl Resolve<crate::api::Args> for PauseContainer {
Ok(
run_komodo_standard_command(
"Docker Pause",
format!("docker pause -- {}", self.name),
CommandOptions::default(),
None,
format!("docker pause {}", self.name),
)
.await,
)
@@ -252,8 +242,8 @@ impl Resolve<crate::api::Args> for UnpauseContainer {
Ok(
run_komodo_standard_command(
"Docker Unpause",
format!("docker unpause -- {}", self.name),
CommandOptions::default(),
None,
format!("docker unpause {}", self.name),
)
.await,
)
@@ -278,20 +268,13 @@ impl Resolve<crate::api::Args> for StopContainer {
) -> anyhow::Result<Log> {
let StopContainer { name, signal, time } = self;
let command = stop_container_command(&name, signal, time);
let log = run_komodo_standard_command(
"Docker Stop",
command,
CommandOptions::default(),
)
.await;
let log =
run_komodo_standard_command("Docker Stop", None, command).await;
if log.stderr.contains("unknown flag: --signal") {
let command = stop_container_command(&name, None, time);
let mut log = run_komodo_standard_command(
"Docker Stop",
command,
CommandOptions::default(),
)
.await;
let mut log =
run_komodo_standard_command("Docker Stop", None, command)
.await;
log.stderr = format!(
"old docker version: unable to use --signal flag{}",
if !log.stderr.is_empty() {
@@ -324,24 +307,23 @@ impl Resolve<crate::api::Args> for RemoveContainer {
args: &crate::api::Args,
) -> anyhow::Result<Log> {
let RemoveContainer { name, signal, time } = self;
let name = escape(name.into());
let stop_command = stop_container_command(&name, signal, time);
let command =
format!("{stop_command} && docker container rm -- {name}");
format!("{stop_command} && docker container rm {name}");
let log = run_komodo_shell_command(
"Docker Stop and Remove",
None,
command,
CommandOptions::default(),
)
.await;
if log.stderr.contains("unknown flag: --signal") {
let stop_command = stop_container_command(&name, None, time);
let command =
format!("{stop_command} && docker container rm -- {name}");
format!("{stop_command} && docker container rm {name}");
let mut log = run_komodo_shell_command(
"Docker Stop and Remove",
None,
command,
CommandOptions::default(),
)
.await;
log.stderr = format!(
@@ -380,14 +362,10 @@ impl Resolve<crate::api::Args> for RenameContainer {
curr_name,
new_name,
} = self;
let command = format!("docker rename -- {curr_name} {new_name}");
let command = format!("docker rename {curr_name} {new_name}");
Ok(
run_komodo_standard_command(
"Docker Rename",
command,
CommandOptions::default(),
)
.await,
run_komodo_standard_command("Docker Rename", None, command)
.await,
)
}
}
@@ -409,12 +387,8 @@ impl Resolve<crate::api::Args> for PruneContainers {
) -> anyhow::Result<Log> {
let command = String::from("docker container prune -f");
Ok(
run_komodo_standard_command(
"Prune Containers",
command,
CommandOptions::default(),
)
.await,
run_komodo_standard_command("Prune Containers", None, command)
.await,
)
}
}
@@ -450,14 +424,10 @@ impl Resolve<crate::api::Args> for StartAllContainers {
{
return None;
}
let command = format!("docker start -- {name}");
let command = format!("docker start {name}");
Some(async move {
run_komodo_standard_command(
&command.clone(),
command,
CommandOptions::default(),
)
.await
run_komodo_standard_command(&command.clone(), None, command)
.await
})
},
);
@@ -496,14 +466,10 @@ impl Resolve<crate::api::Args> for RestartAllContainers {
{
return None;
}
let command = format!("docker restart -- {name}");
let command = format!("docker restart {name}");
Some(async move {
run_komodo_standard_command(
&command.clone(),
command,
CommandOptions::default(),
)
.await
run_komodo_standard_command(&command.clone(), None, command)
.await
})
},
);
@@ -542,14 +508,10 @@ impl Resolve<crate::api::Args> for PauseAllContainers {
{
return None;
}
let command = format!("docker pause -- {name}");
let command = format!("docker pause {name}");
Some(async move {
run_komodo_standard_command(
&command.clone(),
command,
CommandOptions::default(),
)
.await
run_komodo_standard_command(&command.clone(), None, command)
.await
})
},
);
@@ -588,14 +550,10 @@ impl Resolve<crate::api::Args> for UnpauseAllContainers {
{
return None;
}
let command = format!("docker unpause -- {name}");
let command = format!("docker unpause {name}");
Some(async move {
run_komodo_standard_command(
&command.clone(),
command,
CommandOptions::default(),
)
.await
run_komodo_standard_command(&command.clone(), None, command)
.await
})
},
);
@@ -637,8 +595,8 @@ impl Resolve<crate::api::Args> for StopAllContainers {
Some(async move {
run_komodo_standard_command(
&format!("Docker stop {name}"),
None,
stop_container_command(name, None, None),
CommandOptions::default(),
)
.await
})

Some files were not shown because too many files have changed in this diff Show More