Files
komodo/bin/core/src/helpers/query.rs
Maxwell Becker 5fc0a87dea 1.14 - Rename to Komodo - Docker Management (#56)
* setup network page

* add Network, Image, Container

* Docker ListItems and Inspects

* frontend build

* dev0

* network info working

* fix cargo lock

* dev1

* pages for the things

* implement Active in dashboard

* RunBuild update trigger list refresh

* rename deployment executions to StartDeployment etc

* add server level container control

* dev2

* add Config field to Image

* can get image labels from Config.Labels

* mount container page

* server show resource count

* add GetContainerLog api

* add _AllContainers api

* dev3

* move ResourceTarget to entities mod

* GetResourceMatchingContainer api

* connect container to resource

* dev4 add volume names to container list items

* ts types

* volume / image / network unused management

* add image history to image page

* fix PruneContainers incorret Operation

* update cache for server for server after server actions

* dev5

* add singapore to Hetzner

* implement delete single network / image / volume api

* dev6

* include "in use" on Docker Lists

* add docker resource delete buttons

* is nice

* fix volume all in use

* remove google font dependency

* use host networking in test compose

* implement Secret Variables (hidden in logs)

* remove unneeded borrow

* interpolate variables / secrets into extra args / onclone / onpull / command etc

* validate empty strings before SelectItem

* rename everything to Komodo

* rename workspace to komodo

* rc1
2024-09-01 15:38:40 -07:00

379 lines
9.8 KiB
Rust

use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, Context};
use komodo_client::entities::{
alerter::Alerter,
build::Build,
builder::Builder,
deployment::{Deployment, DeploymentState},
docker::container::{ContainerListItem, ContainerStateStatusEnum},
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
server::{Server, ServerState},
server_template::ServerTemplate,
stack::{Stack, StackServiceNames, StackState},
sync::ResourceSync,
tag::Tag,
update::Update,
user::{admin_service_user, User},
user_group::UserGroup,
variable::Variable,
Operation, ResourceTarget, ResourceTargetVariant,
};
use mungos::{
find::find_collect,
mongodb::{
bson::{doc, oid::ObjectId, Document},
options::FindOneOptions,
},
};
use crate::{
config::core_config,
resource::{self, get_user_permission_on_resource},
state::{db_client, deployment_status_cache, stack_status_cache},
};
use super::stack::compose_container_match_regex;
// user: Id or username
#[instrument(level = "debug")]
pub async fn get_user(user: &str) -> anyhow::Result<User> {
if let Some(user) = admin_service_user(user) {
return Ok(user);
}
db_client()
.await
.users
.find_one(id_or_username_filter(user))
.await
.context("failed to query mongo for user")?
.with_context(|| format!("no user found with {user}"))
}
#[instrument(level = "debug")]
pub async fn get_server_with_state(
server_id_or_name: &str,
) -> anyhow::Result<(Server, ServerState)> {
let server = resource::get::<Server>(server_id_or_name).await?;
let state = get_server_state(&server).await;
Ok((server, state))
}
#[instrument(level = "debug")]
pub async fn get_server_state(server: &Server) -> ServerState {
if !server.config.enabled {
return ServerState::Disabled;
}
// Unwrap ok: Server disabled check above
match super::periphery_client(server)
.unwrap()
.request(periphery_client::api::GetHealth {})
.await
{
Ok(_) => ServerState::Ok,
Err(_) => ServerState::NotOk,
}
}
#[instrument(level = "debug")]
pub async fn get_deployment_state(
deployment: &Deployment,
) -> anyhow::Result<DeploymentState> {
let state = deployment_status_cache()
.get(&deployment.id)
.await
.unwrap_or_default()
.curr
.state;
Ok(state)
}
/// Can pass all the containers from the same server
pub fn get_stack_state_from_containers(
ignore_services: &[String],
services: &[StackServiceNames],
containers: &[ContainerListItem],
) -> StackState {
// first filter the containers to only ones which match the service
let services = services
.iter()
.filter(|service| {
!ignore_services.contains(&service.service_name)
})
.collect::<Vec<_>>();
let containers = containers.iter().filter(|container| {
services.iter().any(|StackServiceNames { service_name, container_name }| {
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:#}");
return false
}
}.is_match(&container.name)
})
}).collect::<Vec<_>>();
if containers.is_empty() {
return StackState::Down;
}
if services.len() != containers.len() {
return StackState::Unhealthy;
}
let running = containers.iter().all(|container| {
container.state == ContainerStateStatusEnum::Running
});
if running {
return StackState::Running;
}
let paused = containers.iter().all(|container| {
container.state == ContainerStateStatusEnum::Paused
});
if paused {
return StackState::Paused;
}
let stopped = containers.iter().all(|container| {
container.state == ContainerStateStatusEnum::Exited
});
if stopped {
return StackState::Stopped;
}
let restarting = containers.iter().all(|container| {
container.state == ContainerStateStatusEnum::Restarting
});
if restarting {
return StackState::Restarting;
}
let dead = containers.iter().all(|container| {
container.state == ContainerStateStatusEnum::Dead
});
if dead {
return StackState::Dead;
}
let removing = containers.iter().all(|container| {
container.state == ContainerStateStatusEnum::Removing
});
if removing {
return StackState::Removing;
}
StackState::Unhealthy
}
#[instrument(level = "debug")]
pub async fn get_stack_state(
stack: &Stack,
) -> anyhow::Result<StackState> {
if stack.config.server_id.is_empty() {
return Ok(StackState::Down);
}
let state = stack_status_cache()
.get(&stack.id)
.await
.unwrap_or_default()
.curr
.state;
Ok(state)
}
#[instrument(level = "debug")]
pub async fn get_tag(id_or_name: &str) -> anyhow::Result<Tag> {
let query = match ObjectId::from_str(id_or_name) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": id_or_name },
};
db_client()
.await
.tags
.find_one(query)
.await
.context("failed to query mongo for tag")?
.with_context(|| format!("no tag found matching {id_or_name}"))
}
#[instrument(level = "debug")]
pub async fn get_tag_check_owner(
id_or_name: &str,
user: &User,
) -> anyhow::Result<Tag> {
let tag = get_tag(id_or_name).await?;
if user.admin || tag.owner == user.id {
return Ok(tag);
}
Err(anyhow!("user must be tag owner or admin"))
}
pub async fn get_id_to_tags(
filter: impl Into<Option<Document>>,
) -> anyhow::Result<HashMap<String, Tag>> {
let res = find_collect(&db_client().await.tags, filter, None)
.await
.context("failed to query db for tags")?
.into_iter()
.map(|tag| (tag.id.clone(), tag))
.collect();
Ok(res)
}
#[instrument(level = "debug")]
pub async fn get_user_user_groups(
user_id: &str,
) -> anyhow::Result<Vec<UserGroup>> {
find_collect(
&db_client().await.user_groups,
doc! {
"users": user_id
},
None,
)
.await
.context("failed to query db for user groups")
}
#[instrument(level = "debug")]
pub async fn get_user_user_group_ids(
user_id: &str,
) -> anyhow::Result<Vec<String>> {
let res = get_user_user_groups(user_id)
.await?
.into_iter()
.map(|ug| ug.id)
.collect();
Ok(res)
}
pub fn user_target_query(
user_id: &str,
user_groups: &[UserGroup],
) -> anyhow::Result<Vec<Document>> {
let mut user_target_query = vec![
doc! { "user_target.type": "User", "user_target.id": user_id },
];
let user_groups = user_groups.iter().map(|ug| {
doc! {
"user_target.type": "UserGroup", "user_target.id": &ug.id,
}
});
user_target_query.extend(user_groups);
Ok(user_target_query)
}
pub async fn get_user_permission_on_target(
user: &User,
target: &ResourceTarget,
) -> anyhow::Result<PermissionLevel> {
match target {
ResourceTarget::System(_) => Ok(PermissionLevel::None),
ResourceTarget::Build(id) => {
get_user_permission_on_resource::<Build>(user, id).await
}
ResourceTarget::Builder(id) => {
get_user_permission_on_resource::<Builder>(user, id).await
}
ResourceTarget::Deployment(id) => {
get_user_permission_on_resource::<Deployment>(user, id).await
}
ResourceTarget::Server(id) => {
get_user_permission_on_resource::<Server>(user, id).await
}
ResourceTarget::Repo(id) => {
get_user_permission_on_resource::<Repo>(user, id).await
}
ResourceTarget::Alerter(id) => {
get_user_permission_on_resource::<Alerter>(user, id).await
}
ResourceTarget::Procedure(id) => {
get_user_permission_on_resource::<Procedure>(user, id).await
}
ResourceTarget::ServerTemplate(id) => {
get_user_permission_on_resource::<ServerTemplate>(user, id)
.await
}
ResourceTarget::ResourceSync(id) => {
get_user_permission_on_resource::<ResourceSync>(user, id).await
}
ResourceTarget::Stack(id) => {
get_user_permission_on_resource::<Stack>(user, id).await
}
}
}
pub fn id_or_name_filter(id_or_name: &str) -> Document {
match ObjectId::from_str(id_or_name) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": id_or_name },
}
}
pub fn id_or_username_filter(id_or_username: &str) -> Document {
match ObjectId::from_str(id_or_username) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "username": id_or_username },
}
}
pub async fn get_variable(name: &str) -> anyhow::Result<Variable> {
db_client()
.await
.variables
.find_one(doc! { "name": &name })
.await
.context("failed at call to db")?
.with_context(|| {
format!("no variable found with given name: {name}")
})
}
pub async fn get_latest_update(
resource_type: ResourceTargetVariant,
id: &str,
operation: Operation,
) -> anyhow::Result<Option<Update>> {
db_client()
.await
.updates
.find_one(doc! {
"target.type": resource_type.as_ref(),
"target.id": id,
"operation": operation.as_ref()
})
.with_options(
FindOneOptions::builder()
.sort(doc! { "start_ts": -1 })
.build(),
)
.await
.context("failed to query db for latest update")
}
pub struct VariablesAndSecrets {
pub variables: HashMap<String, String>,
pub secrets: HashMap<String, String>,
}
pub async fn get_variables_and_secrets(
) -> anyhow::Result<VariablesAndSecrets> {
let variables =
find_collect(&db_client().await.variables, None, None)
.await
.context("failed to get all variables from db")?;
let mut secrets = core_config().secrets.clone();
// extend secrets with secret variables
secrets.extend(
variables.iter().filter(|variable| variable.is_secret).map(
|variable| (variable.name.clone(), variable.value.clone()),
),
);
// collect non secret variables
let variables = variables
.into_iter()
.filter(|variable| !variable.is_secret)
.map(|variable| (variable.name, variable.value))
.collect();
Ok(VariablesAndSecrets { variables, secrets })
}