Compare commits

..
Author SHA1 Message Date
mbecker20 af8410ae64 fix invalid tokens JSON 2025-08-23 17:55:43 -07:00
mbecker20 0a2ee6ea43 deploy 1.19.1-dev-14 2025-08-23 17:18:47 -07:00
mbecker20 fafae5d9d1 build multi registry configuration 2025-08-23 17:18:25 -07:00
mbecker20 009221e288 deploy 1.19.1-dev-13 2025-08-23 15:51:25 -07:00
mbecker20 b8022f279f backend for build multi registry push support 2025-08-23 15:51:01 -07:00
2ca87c51f5 Add: Server Version Mismatch Warnings & Alert System (#748)
* start 1.19.1

* deploy 1.19.1-dev-1

* feat: implement version mismatch warnings in server UI
- Replace orange warning colors with yellow for better visibility
- Add version mismatch detection that shows warnings instead of OK status
Implement responsive "VERSION MISMATCH" badge layout
- Update server dashboard to include warning counts
- Add backend version comparison logic for GetServersSummary

* feat: add warning count to server summary and update backup documentation link

* feat: add server version mismatch alert handling and update server summary invalidation logic

* fix: correct version mismatch alert config and disabled server display

- Use send_version_mismatch_alerts instead of send_unreachable_alerts
- Show 'Unknown' instead of 'Disabled' for disabled server versions
- Remove commented VersionAlert and Alerts UI components
- Update version to 1.19.0

* cleanup

* Update TypeScript types after merge

* cleanup

* cleanup

* cleanup

* Add "ServerVersionMismatch" to alert types

* Adjust color classes for warning states and revert server update invalidation logic

---------

Co-authored-by: mbecker20 <max@mogh.tech>
2025-08-23 14:29:26 -07:00
mbecker20 12601d49f4 fmt 2025-08-23 13:11:59 -07:00
mbecker20 284452e674 stack file dependency toml parsing aliases 2025-08-23 12:22:10 -07:00
mbecker20 15ed767f65 deploy 1.19.1-dev-12 2025-08-23 12:19:53 -07:00
mbecker20 9ec9f6e4a8 fix skip serializing if None 2025-08-23 12:19:33 -07:00
mbecker20 ff7bd5c96e deploy 1.19.1-dev-11 2025-08-23 12:13:45 -07:00
mbecker20 4573881c9f rename additional_files => config_files for clarity 2025-08-23 12:13:14 -07:00
mbecker20 4623f3ead2 deploy 1.19.1-dev-10 2025-08-23 12:02:52 -07:00
mbecker20 3c2bbb0b52 default additional file requires is None 2025-08-23 12:02:21 -07:00
mbecker20 b0f89a1b84 UI default file dependency None 2025-08-23 11:59:15 -07:00
mbecker20 88c0e7e2e7 deploy 1.19.1-dev-9 2025-08-23 11:54:19 -07:00
mbecker20 70af4a0b86 implement additional file dependency configuration 2025-08-23 11:53:55 -07:00
mbecker20 530b066717 deploy 1.19.1-dev-8 2025-08-23 10:53:16 -07:00
mbecker20 7fbcee5dd1 get FE compile 2025-08-23 10:53:04 -07:00
mbecker20 bfef4725be Support complex file depency action resolution 2025-08-23 10:48:07 -07:00
05a9750f79 Add Enter Key Support for Dialog Confirmations (#750)
* start 1.19.1

* deploy 1.19.1-dev-1

* Implement usePromptHotkeys for enhanced dialog interactions and UX

* Refactor usePromptHotkeys to enhance confirm button detection and improve UX

* Remove forceConfirmDialog prop from ActionWithDialog and related logic for cleaner implementation

* Add dialog descriptions to ConfirmUpdate and ActionWithDialog for better clarity and resolve warnings

* fix

* Restore forceConfirmDialog prop to ActionWithDialog for enhanced confirmation handling

* cleanup

* Remove conditional className logic from ConfirmButton

---------

Co-authored-by: mbecker20 <max@mogh.tech>
2025-08-22 18:19:15 -07:00
mbecker20 b006bef72c deploy 1.19.1-dev-7 2025-08-22 01:08:57 -07:00
mbecker20 af17636137 env file args won't double pass env file 2025-08-22 01:08:32 -07:00
mbecker20 d0571dcce0 deploy 1.19.1-dev-6 2025-08-21 17:48:48 -07:00
mbecker20 cc9a534a8a clean up SendAlert doc 2025-08-21 15:48:42 -07:00
Ravi Wolter-KrishanandGitHub 112ae60264 Update configuration.md - fix typo: "affect" -> "effect" (#747) 2025-08-21 14:50:25 -07:00
mbecker20 fbc8fb3a58 bump deps 2025-08-21 14:48:56 -07:00
Brian BradleyandGitHub ba85a5526d Add RunStackService API implementing docker compose run (#732)
* Add RunStackService API implementing `docker compose run`

* Add working Procedure configuration

* Remove `km execute run` alias. Remove redundant ``#[serde(default)]` on `Option`.

* Refactor command from `String` to `Vec<String>`

* Implement proper shell escaping
2025-08-21 14:42:47 -07:00
mbecker20 c5bc94b4d8 gen types and fix responses formatting 2025-08-21 14:42:20 -07:00
mbecker20 17f0dd4209 improve cli ergonomics 2025-08-21 14:38:41 -07:00
mbecker20 8528e4c1f7 deploy 1.19.1-dev-5 2025-08-21 14:21:44 -07:00
mbecker20 10f82cdf2f fix clippy if let string 2025-08-21 14:21:04 -07:00
mbecker20 ae7fb7f87f SendAlert via Action / CLI 2025-08-21 14:18:24 -07:00
mbecker20 78451fc3e7 server enabled actually defaults false 2025-08-21 09:17:24 -07:00
mbecker20 8b06ffb140 simple configure action args as JSON 2025-08-20 01:36:56 -07:00
mbecker20 dab9aae61b deploy 1.19.1-dev-4 2025-08-20 00:56:18 -07:00
mbecker20 7407f62cc5 add .ini 2025-08-19 23:30:38 -07:00
mbecker20 627c56dbda deploy 1.19.1-dev-3 2025-08-19 18:42:07 -07:00
mbecker20 829e8f360b gen types 2025-08-19 18:41:08 -07:00
Marcel PfennigandGitHub 38857cbc0b Enhanced Server Stats Dashboard with Performance Optimizations (#746)
* Improve the layout of server mini stats in the dashboard.

- Server stats and tags made siblings for clearer responsibilities
- Changed margin to padding
- Unreachable indicator made into an overlay of the stats

* feat: optimize dashboard server stats with lazy loading and smart server availability checks

- Add enabled prop to ServerStatsMini for conditional data fetching
- Implement server availability check (only fetch stats for Ok servers, not NotOk/Disabled)
- Prevent 500 errors by avoiding API calls to offline servers
- Increase polling interval from 10s to 15s and add 5s stale time
- Add useMemo for expensive calculations to reduce re-renders
- Add conditional overlay rendering for unreachable servers
- Only render stats when showServerStats preference is enabled

* fix: show disabled servers with overlay instead of hiding component

- Maintain consistent layout by showing disabled state overlay
- Prevent UX inconsistency where disabled servers disappeared entirely

* fix: show button height

* feat: add enhance card animations

* cleanup
2025-08-19 18:33:07 -07:00
Marcel PfennigandGitHub 5bbd5510a1 Fix: Example code blocks got interpreted as rust code, leading to compilation errors (#743) 2025-08-19 18:31:23 -07:00
mbecker20 8b1a3230c3 fix tsc 2025-08-19 15:52:58 -07:00
mbecker20 4fe84c17fb Fe support additional file language detection 2025-08-19 15:51:11 -07:00
mbecker20 420c9c0569 deploy 1.19.1-dev-2 2025-08-19 15:36:38 -07:00
mbecker20 6ee7a29f51 support stack additional files 2025-08-19 15:36:00 -07:00
mbecker20 b7dab131fa Global Auto Update rustdoc 2025-08-18 16:39:05 -07:00
mbecker20 0c30229e3e deploy 1.19.1-dev-1 2025-08-18 16:39:05 -07:00
mbecker20 bcbe75ca5d start 1.19.1 2025-08-18 16:39:05 -07:00
118 changed files with 1269 additions and 4589 deletions
Generated
+353 -430
View File
File diff suppressed because it is too large Load Diff
+16 -19
View File
@@ -8,16 +8,13 @@ members = [
]
[workspace.package]
version = "1.19.5"
version = "1.19.1-dev-14"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
repository = "https://github.com/moghtech/komodo"
homepage = "https://komo.do"
[profile.release]
strip = "debuginfo"
[workspace.dependencies]
# LOCAL
komodo_client = { path = "client/core/rs" }
@@ -36,7 +33,7 @@ git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.5.1", default-features = false }
serror = { version = "0.5.0", default-features = false }
slack = { version = "0.4.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
@@ -46,7 +43,7 @@ derive_variants = "1.0.0"
mongo_indexed = "2.0.2"
resolver_api = "3.0.0"
toml_pretty = "1.2.0"
mungos = "3.2.2"
mungos = "3.2.1"
svi = "1.2.0"
# ASYNC
@@ -68,12 +65,11 @@ axum = { version = "0.8.4", features = ["ws", "json", "macros"] }
# SER/DE
ipnetwork = { version = "0.21.1", features = ["serde"] }
indexmap = { version = "2.11.1", features = ["serde"] }
indexmap = { version = "2.10.0", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
strum = { version = "0.27.2", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
serde_yaml_ng = "0.10.0"
serde_json = "1.0.145"
serde_json = "1.0.143"
serde_qs = "0.15.0"
toml = "0.9.5"
@@ -84,19 +80,19 @@ thiserror = "2.0.16"
# LOGGING
opentelemetry-otlp = { version = "0.30.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.20", features = ["json"] }
tracing-subscriber = { version = "0.3.19", features = ["json"] }
opentelemetry-semantic-conventions = "0.30.0"
tracing-opentelemetry = "0.31.0"
opentelemetry = "0.30.0"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.47", features = ["derive"] }
clap = { version = "4.5.45", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.18.1", features = ["v4", "fast-rng", "serde"] }
uuid = { version = "1.18.0", features = ["v4", "fast-rng", "serde"] }
jsonwebtoken = { version = "9.3.1", default-features = false }
openidconnect = "4.0.1"
urlencoding = "2.1.3"
@@ -115,25 +111,26 @@ bollard = "0.19.2"
sysinfo = "0.37.0"
# CLOUD
aws-config = "1.8.6"
aws-sdk-ec2 = "1.167.0"
aws-credential-types = "1.2.6"
aws-config = "1.8.5"
aws-sdk-ec2 = "1.160.0"
aws-credential-types = "1.2.5"
## CRON
english-to-cron = "0.1.6"
chrono-tz = "0.10.4"
chrono = "0.4.42"
chrono = "0.4.41"
croner = "3.0.0"
# MISC
async-compression = { version = "0.4.30", features = ["tokio", "gzip"] }
async-compression = { version = "0.4.27", features = ["tokio", "gzip"] }
derive_builder = "0.20.2"
comfy-table = "7.2.1"
comfy-table = "7.1.4"
typeshare = "1.0.4"
octorust = "0.10.0"
dashmap = "6.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.2"
regex = "1.11.1"
bytes = "1.10.1"
bson = "2.15.0"
shell-escape = "0.1.5"
+1 -3
View File
@@ -2,7 +2,6 @@
## for a specific architecture.
FROM rust:1.89.0-bullseye AS builder
RUN cargo install cargo-strip
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -17,8 +16,7 @@ COPY ./bin/cli ./bin/cli
RUN \
cargo build -p komodo_core --release && \
cargo build -p komodo_periphery --release && \
cargo build -p komodo_cli --release && \
cargo strip
cargo build -p komodo_cli --release
# Copy just the binaries to scratch image
FROM scratch
+1 -3
View File
@@ -12,7 +12,6 @@ COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
RUN cargo install cargo-strip
COPY --from=planner /builder/recipe.json recipe.json
# Build JUST dependencies - cached layer
RUN cargo chef cook --release --recipe-path recipe.json
@@ -21,8 +20,7 @@ COPY . .
RUN \
cargo build --release --bin core && \
cargo build --release --bin periphery && \
cargo build --release --bin km && \
cargo strip
cargo build --release --bin km
# Copy just the binaries to scratch image
FROM scratch
+1 -2
View File
@@ -1,5 +1,4 @@
FROM rust:1.89.0-bullseye AS builder
RUN cargo install cargo-strip
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -9,7 +8,7 @@ COPY ./client/periphery ./client/periphery
COPY ./bin/cli ./bin/cli
# Compile bin
RUN cargo build -p komodo_cli --release && cargo strip
RUN cargo build -p komodo_cli --release
# Copy binaries to distroless base
FROM gcr.io/distroless/cc
+3 -3
View File
@@ -549,20 +549,20 @@ async fn poll_update_until_complete(
} else {
format!("{}/updates/{}", cli_config().host, update.id)
};
println!("Link: '{}'", link.bold());
info!("Link: '{}'", link.bold());
let client = super::komodo_client().await?;
let timer = tokio::time::Instant::now();
let update = client.poll_update_until_complete(&update.id).await?;
if update.success {
println!(
info!(
"FINISHED in {}: {}",
format!("{:.1?}", timer.elapsed()).bold(),
"EXECUTION SUCCESSFUL".green(),
);
} else {
eprintln!(
warn!(
"FINISHED in {}: {}",
format!("{:.1?}", timer.elapsed()).bold(),
"EXECUTION FAILED".red(),
+1 -3
View File
@@ -2,7 +2,6 @@
# Build Core
FROM rust:1.89.0-bullseye AS core-builder
RUN cargo install cargo-strip
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -14,8 +13,7 @@ COPY ./bin/cli ./bin/cli
# Compile app
RUN cargo build -p komodo_core --release && \
cargo build -p komodo_cli --release && \
cargo strip
cargo build -p komodo_cli --release
# Build Frontend
FROM node:20.12-alpine AS frontend-builder
+2 -2
View File
@@ -29,12 +29,12 @@ pub async fn send_alert(
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}"
"{level} | **{name}** ({region}) | Server version now matches core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | **{name}**{region} | Version mismatch detected ⚠️\nPeriphery: **{server_version}** | Core: **{core_version}**\n{link}"
"{level} | **{name}** ({region}) | Version mismatch detected ⚠️\nServer: **{server_version}** | Core: **{core_version}**\n{link}"
)
}
}
+2 -2
View File
@@ -275,12 +275,12 @@ fn standard_alert_content(alert: &Alert) -> String {
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {name}{region} | Periphery version now matches Core version ✅\n{link}"
"{level} | {name} ({region}) | Server version now matches core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | {name}{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}\n{link}"
"{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}\n{link}"
)
}
}
+2 -2
View File
@@ -34,12 +34,12 @@ pub async fn send_alert(
let text = match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | *{name}*{region} | Periphery version now matches Core version ✅"
"{level} | {name} ({region}) | Server version now matches core version ✅"
)
}
_ => {
format!(
"{level} | *{name}*{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}"
"{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}"
)
}
};
+3 -8
View File
@@ -3,12 +3,11 @@ use std::{sync::OnceLock, time::Instant};
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::auth::*, entities::user::User};
use reqwest::StatusCode;
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::{AddStatusCode, Json};
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
@@ -153,11 +152,7 @@ impl Resolve<AuthArgs> for GetUser {
self,
AuthArgs { headers }: &AuthArgs,
) -> serror::Result<User> {
let user_id = get_user_id_from_headers(headers)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
get_user(&user_id)
.await
.status_code(StatusCode::UNAUTHORIZED)
let user_id = get_user_id_from_headers(headers).await?;
Ok(get_user(&user_id).await?)
}
}
+2 -5
View File
@@ -92,11 +92,8 @@ 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(
|state| state.running += 1,
|state| state.running -= 1,
false,
)?;
let _action_guard =
action_state.update(|state| state.running = true)?;
let mut update = update.clone();
+7 -5
View File
@@ -27,7 +27,7 @@ use komodo_client::{
build::{Build, BuildConfig},
builder::{Builder, BuilderConfig},
deployment::DeploymentState,
komodo_timestamp, optional_string,
komodo_timestamp,
permission::PermissionLevel,
repo::Repo,
update::{Log, Update},
@@ -290,10 +290,12 @@ impl Resolve<ExecuteArgs> for RunBuild {
repo,
registry_tokens,
replacers: secret_replacers.into_iter().collect(),
// To push a commit hash tagged image
commit_hash: optional_string(&update.commit_hash),
// Unused for now
additional_tags: Default::default(),
// Push a commit hash tagged image
additional_tags: if update.commit_hash.is_empty() {
Default::default()
} else {
vec![update.commit_hash.clone()]
},
}) => res.context("failed at call to periphery to build"),
_ = cancel.cancelled() => {
info!("build cancelled during build, cleaning up builder");
+11 -11
View File
@@ -12,7 +12,7 @@ use komodo_client::{
deployment::{
Deployment, DeploymentImage, extract_registry_domain,
},
komodo_timestamp, optional_string,
get_image_names, komodo_timestamp, optional_string,
permission::PermissionLevel,
server::Server,
update::{Log, Update},
@@ -115,7 +115,7 @@ 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_names = get_image_names(&build);
let image_name = image_names
.first()
.context("No image name could be created")
@@ -222,7 +222,7 @@ impl Resolve<ExecuteArgs> for Deploy {
}
};
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -249,7 +249,7 @@ pub async fn pull_deployment_inner(
let (image, account, 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_names = get_image_names(&build);
let image_name = image_names
.first()
.context("No image name could be created")
@@ -343,7 +343,7 @@ pub async fn pull_deployment_inner(
Err(e) => Log::error("Pull image", format_serror(&e.into())),
};
update_cache_for_server(server, true).await;
update_cache_for_server(server).await;
anyhow::Ok(log)
}
.await;
@@ -428,7 +428,7 @@ impl Resolve<ExecuteArgs> for StartDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -477,7 +477,7 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -524,7 +524,7 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -573,7 +573,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -628,7 +628,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -711,7 +711,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
update.logs.push(log);
update.finalize();
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update_update(update.clone()).await?;
Ok(update)
+3 -3
View File
@@ -49,7 +49,7 @@ impl Resolve<ExecuteArgs> for ClearRepoCache {
if !user.admin {
return Err(
anyhow!("This method is admin only.")
.status_code(StatusCode::FORBIDDEN),
.status_code(StatusCode::UNAUTHORIZED),
);
}
@@ -124,7 +124,7 @@ impl Resolve<ExecuteArgs> for BackupCoreDatabase {
if !user.admin {
return Err(
anyhow!("This method is admin only.")
.status_code(StatusCode::FORBIDDEN),
.status_code(StatusCode::UNAUTHORIZED),
);
}
@@ -173,7 +173,7 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
if !user.admin {
return Err(
anyhow!("This method is admin only.")
.status_code(StatusCode::FORBIDDEN),
.status_code(StatusCode::UNAUTHORIZED),
);
}
+21 -21
View File
@@ -66,7 +66,7 @@ impl Resolve<ExecuteArgs> for StartContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -122,7 +122,7 @@ impl Resolve<ExecuteArgs> for RestartContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -176,7 +176,7 @@ impl Resolve<ExecuteArgs> for PauseContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -232,7 +232,7 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -288,7 +288,7 @@ impl Resolve<ExecuteArgs> for StopContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -350,7 +350,7 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -401,7 +401,7 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
);
}
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -453,7 +453,7 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
);
}
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -503,7 +503,7 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
);
}
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -555,7 +555,7 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
);
}
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -605,7 +605,7 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
);
}
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -660,7 +660,7 @@ impl Resolve<ExecuteArgs> for PruneContainers {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -711,7 +711,7 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -765,7 +765,7 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -813,7 +813,7 @@ impl Resolve<ExecuteArgs> for DeleteImage {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -865,7 +865,7 @@ impl Resolve<ExecuteArgs> for PruneImages {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -916,7 +916,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -968,7 +968,7 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -1020,7 +1020,7 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -1072,7 +1072,7 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -1123,7 +1123,7 @@ impl Resolve<ExecuteArgs> for PruneSystem {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
+22 -93
View File
@@ -1,9 +1,7 @@
use std::{collections::HashSet, str::FromStr};
use std::collections::HashSet;
use anyhow::Context;
use database::mungos::mongodb::bson::{
doc, oid::ObjectId, to_bson, to_document,
};
use database::mungos::mongodb::bson::{doc, to_document};
use formatting::format_serror;
use interpolate::Interpolator;
use komodo_client::{
@@ -260,7 +258,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
}
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
@@ -366,21 +364,23 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
// host before restart.
maybe_pull_stack(&stack, Some(&mut update)).await?;
let mut update =
restart_services(stack.name, Vec::new(), user).await?;
if update.success {
// Need to update 'info.deployed_contents' with the
// latest contents so next check doesn't read the same diff.
update_deployed_contents_with_latest(
&stack.id,
stack.info.remote_contents,
&mut update,
)
.await;
}
Ok(update)
// The existing update is initialized to DeployStack,
// but also has not been created on database.
// Setup a new update here.
let req = ExecuteRequest::RestartStack(RestartStack {
stack: stack.name,
services: Vec::new(),
});
let update = init_execution_update(&req, user).await?;
let ExecuteRequest::RestartStack(req) = req else {
unreachable!()
};
req
.resolve(&ExecuteArgs {
user: user.clone(),
update,
})
.await
}
DeployIfChangedAction::Services { deploy, restart } => {
match (deploy.is_empty(), restart.is_empty()) {
@@ -401,22 +401,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
// PullStack in order to ensure latest repo contents on the
// host before restart. Only necessary if no "deploys" (deploy already pulls stack).
maybe_pull_stack(&stack, Some(&mut update)).await?;
let mut update =
restart_services(stack.name, restart, user).await?;
if update.success {
// Need to update 'info.deployed_contents' with the
// latest contents so next check doesn't read the same diff.
update_deployed_contents_with_latest(
&stack.id,
stack.info.remote_contents,
&mut update,
)
.await;
}
Ok(update)
restart_services(stack.name, restart, user).await
}
// Only deploy
(false, true) => {
@@ -428,8 +413,6 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
"Execute Deploys",
format!("Deploying: {}", deploy.join(", "),),
);
// This already updates 'stack.info.deployed_services',
// restart doesn't require this again.
let deploy_update =
deploy_services(stack.name.clone(), deploy, user)
.await?;
@@ -513,59 +496,6 @@ async fn restart_services(
.await
}
/// This can safely be called in [DeployStackIfChanged]
/// when there are ONLY changes to config files requiring restart,
/// AFTER the restart has been successfully completed.
///
/// In the case the if changed action is not FullDeploy,
/// the only file diff possible is to config files.
/// Also note either full or service deploy will already update 'deployed_contents'
/// making this method unnecessary in those cases.
///
/// Changes to config files after restart is applied should
/// be taken as the deployed contents, otherwise next changed check
/// will restart service again for no reason.
async fn update_deployed_contents_with_latest(
id: &str,
contents: Option<Vec<StackRemoteFileContents>>,
update: &mut Update,
) {
let Some(contents) = contents else {
return;
};
let contents = contents
.into_iter()
.map(|f| FileContents {
path: f.path,
contents: f.contents,
})
.collect::<Vec<_>>();
if let Err(e) = (async {
let contents = to_bson(&contents)
.context("Failed to serialize contents to bson")?;
let id =
ObjectId::from_str(id).context("Id is not valid ObjectId")?;
db_client()
.stacks
.update_one(
doc! { "_id": id },
doc! { "$set": { "info.deployed_contents": contents } },
)
.await
.context("Failed to update stack 'deployed_contents'")?;
anyhow::Ok(())
})
.await
{
update.push_error_log(
"Update content cache",
format_serror(&e.into()),
);
update.finalize();
let _ = update_update(update.clone()).await;
}
}
enum DeployIfChangedAction {
/// Changes to any compose or env files
/// always lead to this.
@@ -761,7 +691,7 @@ pub async fn pull_stack_inner(
.await?;
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(server, true).await;
update_cache_for_server(server).await;
Ok(res)
}
@@ -1033,7 +963,6 @@ impl Resolve<ExecuteArgs> for RunStackService {
command: self.command,
no_tty: self.no_tty,
no_deps: self.no_deps,
detach: self.detach,
service_ports: self.service_ports,
env: self.env,
workdir: self.workdir,
+4 -2
View File
@@ -77,8 +77,10 @@ impl Resolve<ExecuteArgs> for RunSync {
};
// get the action state for the sync (or insert default).
let action_state =
action_states().sync.get_or_insert_default(&sync.id).await;
let action_state = action_states()
.resource_sync
.get_or_insert_default(&sync.id)
.await;
// This will set action state back to default when dropped.
// Will also check to ensure sync not already busy before updating.
+2 -2
View File
@@ -131,8 +131,8 @@ impl Resolve<ReadArgs> for GetActionsSummary {
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.running > 0 => {
res.running += action_states.running;
(_, action_states) if action_states.running => {
res.running += 1;
}
(ActionState::Ok, _) => res.ok += 1,
(ActionState::Failed, _) => res.failed += 1,
+2 -2
View File
@@ -93,7 +93,7 @@ impl Resolve<ReadArgs> for GetResourceSyncActionState {
)
.await?;
let action_state = action_states()
.sync
.resource_sync
.get(&sync.id)
.await
.unwrap_or_default()
@@ -138,7 +138,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
continue;
}
if action_states
.sync
.resource_sync
.get(&resource_sync.id)
.await
.unwrap_or_default()
+8 -2
View File
@@ -16,7 +16,10 @@ impl Resolve<WriteArgs> for CreateAction {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
resource::create::<Action>(&self.name, self.config, user).await
Ok(
resource::create::<Action>(&self.name, self.config, user)
.await?,
)
}
}
@@ -32,7 +35,10 @@ impl Resolve<WriteArgs> for CopyAction {
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Action>(&self.name, config.into(), user).await
Ok(
resource::create::<Action>(&self.name, config.into(), user)
.await?,
)
}
}
+8 -2
View File
@@ -16,7 +16,10 @@ impl Resolve<WriteArgs> for CreateAlerter {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
resource::create::<Alerter>(&self.name, self.config, user).await
Ok(
resource::create::<Alerter>(&self.name, self.config, user)
.await?,
)
}
}
@@ -32,7 +35,10 @@ impl Resolve<WriteArgs> for CopyAlerter {
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Alerter>(&self.name, config.into(), user).await
Ok(
resource::create::<Alerter>(&self.name, config.into(), user)
.await?,
)
}
}
+17 -13
View File
@@ -50,7 +50,10 @@ impl Resolve<WriteArgs> for CreateBuild {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
resource::create::<Build>(&self.name, self.config, user).await
Ok(
resource::create::<Build>(&self.name, self.config, user)
.await?,
)
}
}
@@ -68,7 +71,10 @@ impl Resolve<WriteArgs> for CopyBuild {
.await?;
// reset version to 0.0.0
config.version = Default::default();
resource::create::<Build>(&self.name, config.into(), user).await
Ok(
resource::create::<Build>(&self.name, config.into(), user)
.await?,
)
}
}
@@ -180,7 +186,7 @@ async fn write_dockerfile_contents_git(
) -> serror::Result<Update> {
let WriteBuildFileContents { build: _, contents } = req;
let mut repo_args: RepoExecutionArgs = if !build
let mut clone_args: RepoExecutionArgs = if !build
.config
.files_on_host
&& !build.config.linked_repo.is_empty()
@@ -190,8 +196,8 @@ async fn write_dockerfile_contents_git(
} else {
(&build).into()
};
let root = repo_args.unique_path(&core_config().repo_directory)?;
repo_args.destination = Some(root.display().to_string());
let root = clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(root.display().to_string());
let build_path = build
.config
@@ -214,11 +220,11 @@ async fn write_dockerfile_contents_git(
})?;
}
let access_token = if let Some(account) = &repo_args.account {
git_token(&repo_args.provider, account, |https| repo_args.https = https)
let access_token = if let Some(account) = &clone_args.account {
git_token(&clone_args.provider, account, |https| clone_args.https = https)
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", repo_args.provider),
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
)?
} else {
None
@@ -229,7 +235,7 @@ async fn write_dockerfile_contents_git(
if !root.join(".git").exists() {
git::init_folder_as_repo(
&root,
&repo_args,
&clone_args,
access_token.as_deref(),
&mut update.logs,
)
@@ -243,11 +249,9 @@ async fn write_dockerfile_contents_git(
}
}
// Save this for later -- repo_args moved next.
let branch = repo_args.branch.clone();
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
repo_args,
clone_args,
&core_config().repo_directory,
access_token,
)
@@ -294,7 +298,7 @@ async fn write_dockerfile_contents_git(
&format!("{}: Commit Dockerfile", args.user.username),
&root,
&build_path.join(&dockerfile_path),
&branch,
&build.config.branch,
)
.await;
+8 -2
View File
@@ -16,7 +16,10 @@ impl Resolve<WriteArgs> for CreateBuilder {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
resource::create::<Builder>(&self.name, self.config, user).await
Ok(
resource::create::<Builder>(&self.name, self.config, user)
.await?,
)
}
}
@@ -32,7 +35,10 @@ impl Resolve<WriteArgs> for CopyBuilder {
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Builder>(&self.name, config.into(), user).await
Ok(
resource::create::<Builder>(&self.name, config.into(), user)
.await?,
)
}
}
+12 -5
View File
@@ -38,8 +38,10 @@ impl Resolve<WriteArgs> for CreateDeployment {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
resource::create::<Deployment>(&self.name, self.config, user)
.await
Ok(
resource::create::<Deployment>(&self.name, self.config, user)
.await?,
)
}
}
@@ -56,8 +58,10 @@ impl Resolve<WriteArgs> for CopyDeployment {
PermissionLevel::Read.into(),
)
.await?;
resource::create::<Deployment>(&self.name, config.into(), user)
.await
Ok(
resource::create::<Deployment>(&self.name, config.into(), user)
.await?,
)
}
}
@@ -149,7 +153,10 @@ impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
});
}
resource::create::<Deployment>(&self.name, config, user).await
Ok(
resource::create::<Deployment>(&self.name, config, user)
.await?,
)
}
}
+8 -3
View File
@@ -16,7 +16,10 @@ impl Resolve<WriteArgs> for CreateProcedure {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateProcedureResponse> {
resource::create::<Procedure>(&self.name, self.config, user).await
Ok(
resource::create::<Procedure>(&self.name, self.config, user)
.await?,
)
}
}
@@ -33,8 +36,10 @@ impl Resolve<WriteArgs> for CopyProcedure {
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Procedure>(&self.name, config.into(), user)
.await
Ok(
resource::create::<Procedure>(&self.name, config.into(), user)
.await?,
)
}
}
+5 -2
View File
@@ -42,7 +42,7 @@ impl Resolve<WriteArgs> for CreateRepo {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
resource::create::<Repo>(&self.name, self.config, user).await
Ok(resource::create::<Repo>(&self.name, self.config, user).await?)
}
}
@@ -58,7 +58,10 @@ impl Resolve<WriteArgs> for CopyRepo {
PermissionLevel::Read.into(),
)
.await?;
resource::create::<Repo>(&self.name, config.into(), user).await
Ok(
resource::create::<Repo>(&self.name, config.into(), user)
.await?,
)
}
}
+8 -3
View File
@@ -30,7 +30,10 @@ impl Resolve<WriteArgs> for CreateServer {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
resource::create::<Server>(&self.name, self.config, user).await
Ok(
resource::create::<Server>(&self.name, self.config, user)
.await?,
)
}
}
@@ -46,8 +49,10 @@ impl Resolve<WriteArgs> for CopyServer {
PermissionLevel::Read.into(),
)
.await?;
resource::create::<Server>(&self.name, config.into(), user).await
Ok(
resource::create::<Server>(&self.name, config.into(), user)
.await?,
)
}
}
+9 -6
View File
@@ -51,7 +51,10 @@ impl Resolve<WriteArgs> for CreateStack {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
resource::create::<Stack>(&self.name, self.config, user).await
Ok(
resource::create::<Stack>(&self.name, self.config, user)
.await?,
)
}
}
@@ -67,8 +70,10 @@ impl Resolve<WriteArgs> for CopyStack {
PermissionLevel::Read.into(),
)
.await?;
resource::create::<Stack>(&self.name, config.into(), user).await
Ok(
resource::create::<Stack>(&self.name, config.into(), user)
.await?,
)
}
}
@@ -279,8 +284,6 @@ async fn write_stack_file_contents_git(
}
}
// Save this for later -- repo_args moved next.
let branch = repo_args.branch.clone();
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
repo_args,
@@ -331,7 +334,7 @@ async fn write_stack_file_contents_git(
&format!("{username}: Write Stack File"),
&root,
&file_path,
&branch,
&stack.config.branch,
)
.await;
+13 -7
View File
@@ -68,8 +68,10 @@ impl Resolve<WriteArgs> for CreateResourceSync {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
resource::create::<ResourceSync>(&self.name, self.config, user)
.await
Ok(
resource::create::<ResourceSync>(&self.name, self.config, user)
.await?,
)
}
}
@@ -86,8 +88,14 @@ impl Resolve<WriteArgs> for CopyResourceSync {
PermissionLevel::Write.into(),
)
.await?;
resource::create::<ResourceSync>(&self.name, config.into(), user)
.await
Ok(
resource::create::<ResourceSync>(
&self.name,
config.into(),
user,
)
.await?,
)
}
}
@@ -315,8 +323,6 @@ async fn write_sync_file_contents_git(
}
}
// Save this for later -- repo_args moved next.
let branch = repo_args.branch.clone();
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
repo_args,
@@ -367,7 +373,7 @@ async fn write_sync_file_contents_git(
&format!("{}: Commit Resource File", args.user.username),
&root,
&resource_path.join(&file_path),
&branch,
&sync.config.branch,
)
.await;
+1 -14
View File
@@ -13,12 +13,9 @@ use komodo_client::{
server::Server, stack::Stack, sync::ResourceSync, tag::Tag,
},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use crate::{
config::core_config,
helpers::query::{get_tag, get_tag_check_owner},
resource,
state::db_client,
@@ -32,18 +29,8 @@ impl Resolve<WriteArgs> for CreateTag {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Tag> {
if core_config().disable_non_admin_create && !user.admin {
return Err(
anyhow!("Non admins cannot create tags")
.status_code(StatusCode::FORBIDDEN),
);
}
if ObjectId::from_str(&self.name).is_ok() {
return Err(
anyhow!("Tag name cannot be ObjectId")
.status_code(StatusCode::BAD_REQUEST),
);
return Err(anyhow!("tag name cannot be ObjectId").into());
}
let mut tag = Tag {
+2 -10
View File
@@ -32,7 +32,7 @@ impl Resolve<WriteArgs> for CreateLocalUser {
if !admin.admin {
return Err(
anyhow!("This method is admin-only.")
.status_code(StatusCode::FORBIDDEN),
.status_code(StatusCode::UNAUTHORIZED),
);
}
@@ -183,7 +183,7 @@ impl Resolve<WriteArgs> for DeleteUser {
if !admin.admin {
return Err(
anyhow!("This method is admin-only.")
.status_code(StatusCode::FORBIDDEN),
.status_code(StatusCode::UNAUTHORIZED),
);
}
if admin.username == self.user || admin.id == self.user {
@@ -220,14 +220,6 @@ impl Resolve<WriteArgs> for DeleteUser {
.delete_one(query)
.await
.context("Failed to delete user from database")?;
// Also remove user id from all user groups
if let Err(e) = db
.user_groups
.update_many(doc! {}, doc! { "$pull": { "users": &user.id } })
.await
{
warn!("Failed to remove deleted user from user groups | {e:?}");
};
Ok(user)
}
}
+7 -30
View File
@@ -10,9 +10,7 @@ use komodo_client::{
api::write::*,
entities::{komodo_timestamp, user_group::UserGroup},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use crate::state::db_client;
@@ -25,10 +23,7 @@ impl Resolve<WriteArgs> for CreateUserGroup {
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(
anyhow!("This call is admin-only")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("This call is admin-only").into());
}
let user_group = UserGroup {
name: self.name,
@@ -63,10 +58,7 @@ impl Resolve<WriteArgs> for RenameUserGroup {
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(
anyhow!("This call is admin-only")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
update_one_by_id(
@@ -92,10 +84,7 @@ impl Resolve<WriteArgs> for DeleteUserGroup {
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(
anyhow!("This call is admin-only")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
@@ -128,10 +117,7 @@ impl Resolve<WriteArgs> for AddUserToUserGroup {
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(
anyhow!("This call is admin-only")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
@@ -175,10 +161,7 @@ impl Resolve<WriteArgs> for RemoveUserFromUserGroup {
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(
anyhow!("This call is admin-only")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
@@ -222,10 +205,7 @@ impl Resolve<WriteArgs> for SetUsersInUserGroup {
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(
anyhow!("This call is admin-only")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
@@ -272,10 +252,7 @@ impl Resolve<WriteArgs> for SetEveryoneUserGroup {
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(
anyhow!("This call is admin-only")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
+15 -32
View File
@@ -4,9 +4,7 @@ use komodo_client::{
api::write::*,
entities::{Operation, ResourceTarget, variable::Variable},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use crate::{
helpers::{
@@ -24,13 +22,6 @@ impl Resolve<WriteArgs> for CreateVariable {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateVariableResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can create variables")
.status_code(StatusCode::FORBIDDEN),
);
}
let CreateVariable {
name,
value,
@@ -38,6 +29,10 @@ impl Resolve<WriteArgs> for CreateVariable {
is_secret,
} = self;
if !user.admin {
return Err(anyhow!("only admins can create variables").into());
}
let variable = Variable {
name,
value,
@@ -49,7 +44,7 @@ impl Resolve<WriteArgs> for CreateVariable {
.variables
.insert_one(&variable)
.await
.context("Failed to create variable on db")?;
.context("failed to create variable on db")?;
let mut update = make_update(
ResourceTarget::system(),
@@ -74,10 +69,7 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateVariableValueResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can update variables")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("only admins can update variables").into());
}
let UpdateVariableValue { name, value } = self;
@@ -95,7 +87,7 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
doc! { "$set": { "value": &value } },
)
.await
.context("Failed to update variable value on db")?;
.context("failed to update variable value on db")?;
let mut update = make_update(
ResourceTarget::system(),
@@ -115,7 +107,7 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
)
};
update.push_simple_log("Update Variable Value", log);
update.push_simple_log("update variable value", log);
update.finalize();
add_update(update).await?;
@@ -131,10 +123,7 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateVariableDescriptionResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can update variables")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("only admins can update variables").into());
}
db_client()
.variables
@@ -143,7 +132,7 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
doc! { "$set": { "description": &self.description } },
)
.await
.context("Failed to update variable description on db")?;
.context("failed to update variable description on db")?;
Ok(get_variable(&self.name).await?)
}
}
@@ -155,10 +144,7 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateVariableIsSecretResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can update variables")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("only admins can update variables").into());
}
db_client()
.variables
@@ -167,7 +153,7 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
doc! { "$set": { "is_secret": self.is_secret } },
)
.await
.context("Failed to update variable is secret on db")?;
.context("failed to update variable is secret on db")?;
Ok(get_variable(&self.name).await?)
}
}
@@ -178,17 +164,14 @@ impl Resolve<WriteArgs> for DeleteVariable {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteVariableResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can delete variables")
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!("only admins can delete variables").into());
}
let variable = get_variable(&self.name).await?;
db_client()
.variables
.delete_one(doc! { "name": &self.name })
.await
.context("Failed to delete variable on db")?;
.context("failed to delete variable on db")?;
let mut update = make_update(
ResourceTarget::system(),
@@ -197,7 +180,7 @@ impl Resolve<WriteArgs> for DeleteVariable {
);
update
.push_simple_log("Delete Variable", format!("{variable:#?}"));
.push_simple_log("delete variable", format!("{variable:#?}"));
update.finalize();
add_update(update).await?;
+2 -3
View File
@@ -187,8 +187,8 @@ impl GoogleOauthClient {
Ok(body)
} else {
let text = res.text().await.context(format!(
"method: POST | status: {status} | failed to get response text"
))?;
"method: POST | status: {status} | failed to get response text"
))?;
Err(anyhow!("method: POST | status: {status} | text: {text}"))
}
}
@@ -207,6 +207,5 @@ pub struct GoogleUser {
#[serde(rename = "sub")]
pub id: String,
pub email: String,
#[serde(default)]
pub picture: String,
}
-1
View File
@@ -250,7 +250,6 @@ pub fn core_config() -> &'static CoreConfig {
.unwrap_or(config.logging.opentelemetry_service_name),
},
pretty_startup_config: env.komodo_pretty_startup_config.unwrap_or(config.pretty_startup_config),
unsafe_unsanitized_startup_config: env.komodo_unsafe_unsanitized_startup_config.unwrap_or(config.unsafe_unsanitized_startup_config),
internet_interface: env.komodo_internet_interface.unwrap_or(config.internet_interface),
ssl_enabled: env.komodo_ssl_enabled.unwrap_or(config.ssl_enabled),
ssl_key_file: env.komodo_ssl_key_file.unwrap_or(config.ssl_key_file),
+12 -28
View File
@@ -16,16 +16,17 @@ use super::cache::Cache;
#[derive(Default)]
pub struct ActionStates {
pub server: Cache<String, Arc<ActionState<ServerActionState>>>,
pub stack: Cache<String, Arc<ActionState<StackActionState>>>,
pub build: Cache<String, Arc<ActionState<BuildActionState>>>,
pub deployment:
Cache<String, Arc<ActionState<DeploymentActionState>>>,
pub build: Cache<String, Arc<ActionState<BuildActionState>>>,
pub server: Cache<String, Arc<ActionState<ServerActionState>>>,
pub repo: Cache<String, Arc<ActionState<RepoActionState>>>,
pub procedure:
Cache<String, Arc<ActionState<ProcedureActionState>>>,
pub action: Cache<String, Arc<ActionState<ActionActionState>>>,
pub sync: Cache<String, Arc<ActionState<ResourceSyncActionState>>>,
pub resource_sync:
Cache<String, Arc<ActionState<ResourceSyncActionState>>>,
pub stack: Cache<String, Arc<ActionState<StackActionState>>>,
}
/// Need to be able to check "busy" with write lock acquired.
@@ -61,33 +62,17 @@ impl<States: Default + Busy + Copy + Send + 'static>
/// Returns a guard that returns the states to default (not busy) when dropped.
pub fn update(
&self,
update_fn: impl Fn(&mut States),
) -> anyhow::Result<UpdateGuard<'_, States>> {
self.update_custom(
update_fn,
|states| *states = Default::default(),
true,
)
}
/// Will acquire lock, optionally check busy, and if not will
/// run the provided update function on the states.
/// Returns a guard that calls the provided return_fn when dropped.
pub fn update_custom(
&self,
update_fn: impl Fn(&mut States),
return_fn: impl Fn(&mut States) + Send + 'static,
busy_check: bool,
handler: impl Fn(&mut States),
) -> anyhow::Result<UpdateGuard<'_, States>> {
let mut lock = self
.0
.lock()
.map_err(|e| anyhow!("Action state lock poisoned | {e:?}"))?;
if busy_check && lock.busy() {
return Err(anyhow!("Resource is busy"));
.map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?;
if lock.busy() {
return Err(anyhow!("resource is busy"));
}
update_fn(&mut *lock);
Ok(UpdateGuard(&self.0, Box::new(return_fn)))
handler(&mut *lock);
Ok(UpdateGuard(&self.0))
}
}
@@ -97,7 +82,6 @@ impl<States: Default + Busy + Copy + Send + 'static>
/// user could drop UpdateGuard.
pub struct UpdateGuard<'a, States: Default + Send + 'static>(
&'a Mutex<States>,
Box<dyn Fn(&mut States) + Send>,
);
impl<States: Default + Send + 'static> Drop
@@ -111,6 +95,6 @@ impl<States: Default + Send + 'static> Drop
return;
}
};
self.1(&mut *lock);
*lock = States::default();
}
}
+22 -27
View File
@@ -13,31 +13,26 @@ use database::mungos::{
options::FindOneOptions,
},
};
use komodo_client::{
busy::Busy,
entities::{
Operation, ResourceTarget, ResourceTargetVariant,
action::{Action, ActionState},
alerter::Alerter,
build::Build,
builder::Builder,
deployment::{Deployment, DeploymentState},
docker::container::{
ContainerListItem, ContainerStateStatusEnum,
},
permission::{PermissionLevel, PermissionLevelAndSpecifics},
procedure::{Procedure, ProcedureState},
repo::Repo,
server::{Server, ServerState},
stack::{Stack, StackServiceNames, StackState},
stats::SystemInformation,
sync::ResourceSync,
tag::Tag,
update::Update,
user::{User, admin_service_user},
user_group::UserGroup,
variable::Variable,
},
use komodo_client::entities::{
Operation, ResourceTarget, ResourceTargetVariant,
action::{Action, ActionState},
alerter::Alerter,
build::Build,
builder::Builder,
deployment::{Deployment, DeploymentState},
docker::container::{ContainerListItem, ContainerStateStatusEnum},
permission::{PermissionLevel, PermissionLevelAndSpecifics},
procedure::{Procedure, ProcedureState},
repo::Repo,
server::{Server, ServerState},
stack::{Stack, StackServiceNames, StackState},
stats::SystemInformation,
sync::ResourceSync,
tag::Tag,
update::Update,
user::{User, admin_service_user},
user_group::UserGroup,
variable::Variable,
};
use periphery_client::api::stats;
use tokio::sync::Mutex;
@@ -472,7 +467,7 @@ pub async fn get_action_state(id: &String) -> ActionState {
.action
.get(id)
.await
.map(|s| s.get().map(|s| s.busy()))
.map(|s| s.get().map(|s| s.running))
.transpose()
.ok()
.flatten()
@@ -488,7 +483,7 @@ pub async fn get_procedure_state(id: &String) -> ProcedureState {
.procedure
.get(id)
.await
.map(|s| s.get().map(|s| s.busy()))
.map(|s| s.get().map(|s| s.running))
.transpose()
.ok()
.flatten()
+4 -8
View File
@@ -45,14 +45,10 @@ async fn app() -> anyhow::Result<()> {
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
match (
config.pretty_startup_config,
config.unsafe_unsanitized_startup_config,
) {
(true, true) => info!("{:#?}", config),
(true, false) => info!("{:#?}", config.sanitized()),
(false, true) => info!("{:?}", config),
(false, false) => info!("{:?}", config.sanitized()),
if core_config().pretty_startup_config {
info!("{:#?}", config.sanitized());
} else {
info!("{:?}", config.sanitized());
}
// Init jwt client to crash on failure
+3 -39
View File
@@ -1,5 +1,3 @@
use std::sync::{Arc, OnceLock};
use async_timing_util::wait_until_timelength;
use database::mungos::{find::find_collect, mongodb::bson::doc};
use futures::future::join_all;
@@ -17,11 +15,10 @@ use komodo_client::entities::{
};
use periphery_client::api::{self, git::GetLatestCommit};
use serror::Serror;
use tokio::sync::Mutex;
use crate::{
config::core_config,
helpers::{cache::Cache, periphery_client},
helpers::periphery_client,
monitor::{alert::check_alerts, record::record_server_stats},
state::{db_client, deployment_status_cache, repo_status_cache},
};
@@ -113,47 +110,14 @@ async fn refresh_server_cache(ts: i64) {
}
};
let futures = servers.into_iter().map(|server| async move {
update_cache_for_server(&server, false).await;
update_cache_for_server(&server).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.
/// If forced, will still block against simultaneous update.
fn update_cache_for_server_controller()
-> &'static Cache<String, Arc<Mutex<i64>>> {
static CACHE: OnceLock<Cache<String, Arc<Mutex<i64>>>> =
OnceLock::new();
CACHE.get_or_init(Default::default)
}
/// The background loop will call this with force: false,
/// which exits early if the lock is busy or it was completed too recently.
/// If force is true, it will wait on simultaneous calls, and will
/// ignore the restriction on being completed too recently.
#[instrument(level = "debug")]
pub async fn update_cache_for_server(server: &Server, force: bool) {
// Concurrency controller to ensure it isn't done too often
// when it happens in other contexts.
let controller = update_cache_for_server_controller()
.get_or_insert_default(&server.id)
.await;
let mut lock = match controller.try_lock() {
Ok(lock) => lock,
Err(_) if force => controller.lock().await,
Err(_) => return,
};
let now = komodo_timestamp();
// early return if called again sooner than 1s.
if !force && *lock > now - 1_000 {
return;
}
*lock = now;
pub async fn update_cache_for_server(server: &Server) {
let (deployments, builds, repos, stacks) = tokio::join!(
find_collect(
&db_client().deployments,
-1
View File
@@ -21,7 +21,6 @@ pub async fn record_server_stats(ts: i64) {
ts,
sid: status.id.clone(),
cpu_perc: stats.cpu_perc,
load_average: stats.load_average.clone(),
mem_total_gb: stats.mem_total_gb,
mem_used_gb: stats.mem_used_gb,
disk_total_gb,
+10 -14
View File
@@ -81,7 +81,7 @@ pub async fn update_deployment_cache(
// If image already has tag, leave it,
// otherwise default the tag to latest
if image.contains(':') {
image.to_string()
image
} else {
format!("{image}:latest")
}
@@ -92,9 +92,6 @@ pub async fn update_deployment_cache(
..
}) = &container
{
// Docker will automatically strip `docker.io` from incoming image names re #468.
// Need to strip it in order to match by image name and find available updates.
let image = image.strip_prefix("docker.io/").unwrap_or(&image);
images
.iter()
.find(|i| i.name == image)
@@ -253,21 +250,20 @@ pub async fn update_stack_cache(
}
}.is_match(&container.name)
}).cloned();
// If image already has tag, leave it,
// otherwise default the tag to latest
let image = image.clone();
let image = if image.contains(':') {
image.to_string()
image
} else {
format!("{image}:latest")
image + ":latest"
};
let update_available = if let Some(ContainerListItem { image_id: Some(curr_image_id), .. }) = &container {
// Docker will automatically strip `docker.io` from incoming image names re #468.
// Need to strip it in order to match by image tag and find available update.
let image =
image.strip_prefix("docker.io/").unwrap_or(&image);
images
.iter()
.find(|i| i.name == image)
.map(|i| &i.id != curr_image_id)
.unwrap_or_default()
.iter()
.find(|i| i.name == image)
.map(|i| &i.id != curr_image_id)
.unwrap_or_default()
} else {
false
};
+1 -1
View File
@@ -188,7 +188,7 @@ impl super::KomodoResource for Deployment {
else {
return Ok(());
};
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
Ok(())
}
+11 -51
View File
@@ -34,10 +34,8 @@ use komodo_client::{
parsers::parse_string_list,
};
use partial_derive2::{Diff, MaybeNone, PartialDiff};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serde::{Serialize, de::DeserializeOwned};
use serror::AddStatusCodeError;
use crate::{
api::{read::ReadArgs, write::WriteArgs},
@@ -460,31 +458,22 @@ pub async fn create<T: KomodoResource>(
name: &str,
mut config: T::PartialConfig,
user: &User,
) -> serror::Result<Resource<T::Config, T::Info>> {
) -> anyhow::Result<Resource<T::Config, T::Info>> {
if !T::user_can_create(user) {
return Err(
anyhow!(
"User does not have permissions to create {}.",
T::resource_type()
)
.status_code(StatusCode::FORBIDDEN),
);
return Err(anyhow!(
"User does not have permissions to create {}.",
T::resource_type()
));
}
if name.is_empty() {
return Err(
anyhow!("Must provide non-empty name for resource")
.status_code(StatusCode::BAD_REQUEST),
);
return Err(anyhow!("Must provide non-empty name for resource."));
}
let name = T::validated_name(name);
if ObjectId::from_str(&name).is_ok() {
return Err(
anyhow!("Valid ObjectIds cannot be used as names")
.status_code(StatusCode::BAD_REQUEST),
);
return Err(anyhow!("valid ObjectIds cannot be used as names."));
}
// Ensure an existing resource with same name doesn't already exist
@@ -500,10 +489,7 @@ pub async fn create<T: KomodoResource>(
.into_iter()
.any(|r| r.name == name)
{
return Err(
anyhow!("Resource with name '{}' already exists", name)
.status_code(StatusCode::CONFLICT),
);
return Err(anyhow!("Must provide unique name for resource."));
}
let start_ts = komodo_timestamp();
@@ -867,15 +853,9 @@ pub async fn delete<T: KomodoResource>(
);
update.push_simple_log("Deleted Toml", toml);
tokio::join!(
async {
if let Err(e) = T::post_delete(&resource, &mut update).await {
update
.push_error_log("post delete", format_serror(&e.into()));
}
},
delete_from_alerters::<T>(&resource.id)
);
if let Err(e) = T::post_delete(&resource, &mut update).await {
update.push_error_log("post delete", format_serror(&e.into()));
}
refresh_all_resources_cache().await;
@@ -885,26 +865,6 @@ pub async fn delete<T: KomodoResource>(
Ok(resource)
}
async fn delete_from_alerters<T: KomodoResource>(id: &str) {
let target_bson = doc! {
"type": T::resource_type().as_ref(),
"id": id,
};
if let Err(e) = db_client()
.alerters
.update_many(Document::new(), doc! {
"$pull": {
"config.resources": &target_bson,
"config.except_resources": target_bson,
}
})
.await
.context("Failed to clear deleted resource from alerter whitelist / blacklist")
{
warn!("{e:#}");
}
}
// =======
#[instrument(level = "debug")]
-1
View File
@@ -300,7 +300,6 @@ async fn get_repo_state_from_db(id: &str) -> RepoState {
"$or": [
{ "operation": "CloneRepo" },
{ "operation": "PullRepo" },
{ "operation": "BuildRepo" },
],
})
.with_options(
+2 -2
View File
@@ -123,7 +123,7 @@ impl super::KomodoResource for Server {
created: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
update_cache_for_server(created, true).await;
update_cache_for_server(created).await;
Ok(())
}
@@ -145,7 +145,7 @@ impl super::KomodoResource for Server {
updated: &Self,
_update: &mut Update,
) -> anyhow::Result<()> {
update_cache_for_server(updated, true).await;
update_cache_for_server(updated).await;
Ok(())
}
+1 -1
View File
@@ -252,7 +252,7 @@ impl super::KomodoResource for Stack {
else {
return Ok(());
};
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
Ok(())
}
+2 -2
View File
@@ -113,7 +113,7 @@ impl super::KomodoResource for ResourceSync {
async fn busy(id: &String) -> anyhow::Result<bool> {
action_states()
.sync
.resource_sync
.get(id)
.await
.unwrap_or_default()
@@ -242,7 +242,7 @@ async fn get_resource_sync_state(
data: &ResourceSyncInfo,
) -> ResourceSyncState {
if let Some(state) = action_states()
.sync
.resource_sync
.get(id)
.await
.and_then(|s| {
+1 -1
View File
@@ -72,7 +72,7 @@ pub async fn execute_compose<T: ExecuteCompose>(
.push(T::execute(periphery, stack, services, extras).await?);
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(&server, true).await;
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
+1 -1
View File
@@ -242,7 +242,7 @@ async fn ensure_first_server_and_builder() {
return;
};
if let Err(e) = (CreateBuilder {
name: config.first_server_name.clone(),
name: String::from("Local"),
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_id: Some(server.id),
-1
View File
@@ -40,7 +40,6 @@ pub fn db_client() -> &'static Client {
.expect("db_client accessed before initialized")
}
/// Must be called in app startup sequence.
pub async fn init_db_client() {
let client = Client::new(&core_config().database)
.await
-1
View File
@@ -147,7 +147,6 @@ pub trait ExecuteResourceSync: ResourceSyncTrait {
sync_user(),
)
.await
.map_err(|e| e.error)
{
Ok(resource) => resource.id,
Err(e) => {
-1
View File
@@ -825,7 +825,6 @@ impl ExecuteResourceSync for Procedure {
sync_user(),
)
.await
.map_err(|e| e.error)
{
Ok(resource) => resource.id,
Err(e) => {
+1 -2
View File
@@ -1,7 +1,6 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
FROM rust:1.89.0-bullseye AS builder
RUN cargo install cargo-strip
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -11,7 +10,7 @@ COPY ./client/periphery ./client/periphery
COPY ./bin/periphery ./bin/periphery
# Compile app
RUN cargo build -p komodo_periphery --release && cargo strip
RUN cargo build -p komodo_periphery --release
# Final Image
FROM debian:bullseye-slim
+12 -7
View File
@@ -12,7 +12,7 @@ use interpolate::Interpolator;
use komodo_client::entities::{
EnvironmentVar, all_logs_success,
build::{Build, BuildConfig},
environment_vars_from_str, optional_string,
environment_vars_from_str, get_image_names, optional_string,
to_path_compatible_name,
update::Log,
};
@@ -25,7 +25,9 @@ use resolver_api::Resolve;
use tokio::fs;
use crate::{
build::{parse_build_args, parse_secret_args, write_dockerfile},
build::{
image_tags, parse_build_args, parse_secret_args, write_dockerfile,
},
config::periphery_config,
docker::docker_login,
helpers::{parse_extra_args, parse_labels},
@@ -124,9 +126,8 @@ impl Resolve<super::Args> for build::Build {
mut build,
repo: linked_repo,
registry_tokens,
mut replacers,
commit_hash,
additional_tags,
mut replacers,
} = self;
let mut logs = Vec::new();
@@ -144,6 +145,8 @@ impl Resolve<super::Args> for build::Build {
name,
config:
BuildConfig {
version,
image_tag,
build_path,
dockerfile_path,
build_args,
@@ -262,6 +265,8 @@ impl Resolve<super::Args> for build::Build {
// Get command parts
let image_names = get_image_names(&build);
// Add VERSION to build args (if not already there)
let mut build_args = environment_vars_from_str(build_args)
.context("Invalid build_args")?;
@@ -286,9 +291,9 @@ impl Resolve<super::Args> for build::Build {
let buildx = if *use_buildx { " buildx" } else { "" };
let image_tags = build
.get_image_tags_as_arg(commit_hash.as_deref(), &additional_tags)
.context("Failed to parse image tags into command")?;
let image_tags =
image_tags(&image_names, image_tag, version, &additional_tags)
.context("Failed to parse image tags into command")?;
let maybe_push = if should_push { " --push" } else { "" };
-4
View File
@@ -712,7 +712,6 @@ impl Resolve<super::Args> for ComposeRun {
command,
no_tty,
no_deps,
detach,
service_ports,
env,
workdir,
@@ -784,9 +783,6 @@ impl Resolve<super::Args> for ComposeRun {
}
let mut run_flags = String::from(" --rm");
if detach.unwrap_or_default() {
run_flags.push_str(" -d");
}
if no_tty.unwrap_or_default() {
run_flags.push_str(" --no-tty");
}
+1 -1
View File
@@ -347,7 +347,7 @@ async fn execute_command_on_terminal(
);
let full_command = format!(
"printf '\n{START_OF_OUTPUT}\n\n'; {command}; rc=$?; printf '\n{KOMODO_EXIT_CODE}%d\n{END_OF_OUTPUT}\n' \"$rc\"\n"
"printf '\n{START_OF_OUTPUT}\n\n'; {command}; rc=$? printf '\n{KOMODO_EXIT_CODE}%d\n{END_OF_OUTPUT}\n' \"$rc\"\n"
);
terminal
+29 -1
View File
@@ -6,7 +6,7 @@ use std::{
use anyhow::{Context, anyhow};
use formatting::format_serror;
use komodo_client::{
entities::{EnvironmentVar, update::Log},
entities::{EnvironmentVar, Version, update::Log},
parsers::QUOTE_PATTERN,
};
@@ -52,6 +52,34 @@ pub async fn write_dockerfile(
}
}
pub fn image_tags(
image_names: &[String],
custom_tag: &str,
version: &Version,
additional: &[String],
) -> anyhow::Result<String> {
let Version { major, minor, .. } = version;
let custom_tag = if custom_tag.is_empty() {
String::new()
} else {
format!("-{custom_tag}")
};
let mut res = String::new();
for image_name in image_names {
write!(
&mut res,
" -t {image_name}:latest{custom_tag} -t {image_name}:{version}{custom_tag} -t {image_name}:{major}.{minor}{custom_tag} -t {image_name}:{major}{custom_tag}"
)?;
for tag in additional {
write!(&mut res, " -t {image_name}:{tag}{custom_tag}")?;
}
}
Ok(res)
}
pub fn parse_build_args(build_args: &[EnvironmentVar]) -> String {
build_args
.iter()
+1 -2
View File
@@ -43,9 +43,8 @@ pub async fn validate_files(
"Validate Files",
format_serror(
&anyhow!(
"Missing files: {}", res.missing_files.join(", ")
"Ensure the run_directory and all file paths are correct."
)
.context("Ensure the run_directory and all file paths are correct.")
.context("A file doesn't exist after writing stack.")
.into(),
),
+1 -9
View File
@@ -2,8 +2,7 @@ use std::{cmp::Ordering, sync::OnceLock};
use async_timing_util::wait_until_timelength;
use komodo_client::entities::stats::{
SingleDiskUsage, SystemInformation, SystemLoadAverage,
SystemProcess, SystemStats,
SingleDiskUsage, SystemInformation, SystemProcess, SystemStats,
};
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
use tokio::sync::RwLock;
@@ -96,15 +95,8 @@ impl StatsClient {
network_egress_bytes += network.transmitted();
}
let load_avg = System::load_average();
SystemStats {
cpu_perc: self.system.global_cpu_usage(),
load_average: SystemLoadAverage {
one: load_avg.one,
five: load_avg.five,
fifteen: load_avg.fifteen,
},
mem_free_gb: self.system.free_memory() as f64 / BYTES_PER_GB,
mem_used_gb: (total_mem - available_mem) as f64 / BYTES_PER_GB,
mem_total_gb: total_mem as f64 / BYTES_PER_GB,
-1
View File
@@ -28,7 +28,6 @@ resolver_api.workspace = true
# external
tokio-tungstenite.workspace = true
derive_builder.workspace = true
urlencoding.workspace = true
serde_json.workspace = true
tokio-util.workspace = true
thiserror.workspace = true
-3
View File
@@ -379,9 +379,6 @@ pub struct RunStackService {
/// Do not start linked services
#[arg(long = "no-deps", action = SetTrue)]
pub no_deps: Option<bool>,
/// Detach container on run
#[arg(long = "detach", action = SetTrue)]
pub detach: Option<bool>,
/// Map service ports to the host
#[arg(long = "service-ports", action = SetTrue)]
pub service_ports: Option<bool>,
+1 -1
View File
@@ -68,7 +68,7 @@ impl Busy for ProcedureActionState {
impl Busy for ActionActionState {
fn busy(&self) -> bool {
self.running > 0
self.running
}
}
@@ -1,4 +1,5 @@
use serde::{
__private::de::{Content, ContentDeserializer},
Deserialize, Deserializer,
de::{IntoDeserializer, Visitor},
};
@@ -68,15 +69,17 @@ impl<'de, T: Deserialize<'de>> Visitor<'de>
let mut res =
Vec::with_capacity(seq.size_hint().unwrap_or_default());
loop {
match seq.next_element::<serde_json::Value>() {
Ok(Some(value)) => {
match T::deserialize(value.clone().into_deserializer()) {
match seq.next_element::<Content>() {
Ok(Some(content)) => {
match T::deserialize::<ContentDeserializer<'_, S::Error>>(
content.clone().into_deserializer(),
) {
Ok(item) => res.push(item),
Err(e) => {
// Since this is used to parse startup config (including logging config),
// the tracing logging is not initialized. Need to use eprintln.
eprintln!(
"WARN: failed to parse item in list | {value:?} | {e:?}",
"WARN: failed to parse item in list | {content:?} | {e:?}",
)
}
}
+2 -2
View File
@@ -222,8 +222,8 @@ impl Default for ActionConfig {
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
pub struct ActionActionState {
/// Number of instances of the Action currently running
pub running: u32,
/// Whether the action is currently running.
pub running: bool,
}
#[typeshare]
+1 -147
View File
@@ -1,4 +1,4 @@
use std::{fmt::Write, sync::OnceLock};
use std::sync::OnceLock;
use bson::{Document, doc};
use derive_builder::Builder;
@@ -26,127 +26,6 @@ use super::{
#[typeshare]
pub type Build = Resource<BuildConfig, BuildInfo>;
impl Build {
pub fn get_image_names(&self) -> Vec<String> {
let Build {
name,
config:
BuildConfig {
image_name,
image_registry,
..
},
..
} = self;
let name = if image_name.is_empty() {
name
} else {
image_name
};
// Local only
if image_registry.is_empty() {
return vec![name.to_string()];
}
image_registry
.iter()
.map(
|ImageRegistryConfig {
domain,
account,
organization,
}| {
match (
!domain.is_empty(),
!organization.is_empty(),
!account.is_empty(),
) {
// If organization and account provided, name under organization.
(true, true, true) => {
format!("{domain}/{organization}/{name}")
}
// Just domain / account provided
(true, false, true) => {
format!("{domain}/{account}/{name}")
}
// Otherwise, just use name (local only)
_ => name.to_string(),
}
},
)
.collect()
}
pub fn get_image_tags(
&self,
image_names: &[String],
commit_hash: Option<&str>,
additional: &[String],
) -> Vec<String> {
let BuildConfig {
version,
image_tag,
include_latest_tag,
include_version_tags: include_version_tag,
include_commit_tag,
..
} = &self.config;
let Version { major, minor, .. } = version;
let image_tag_postfix = if image_tag.is_empty() {
String::new()
} else {
format!("-{image_tag}")
};
let mut tags = Vec::new();
for image_name in image_names {
// Pure image tag passthrough when provided
if !image_tag.is_empty() {
tags.push(format!("{image_name}:{image_tag}"));
}
// `:latest` / `:latest-tag`
if *include_latest_tag {
tags.push(format!("{image_name}:latest{image_tag_postfix}"));
}
// `:1.19.5` + `:1.19` etc. / `1.19.5-tag`
if *include_version_tag {
tags
.push(format!("{image_name}:{version}{image_tag_postfix}"));
tags.push(format!(
"{image_name}:{major}.{minor}{image_tag_postfix}"
));
tags.push(format!("{image_name}:{major}{image_tag_postfix}"));
}
if *include_commit_tag && let Some(hash) = commit_hash {
tags.push(format!("{image_name}:{hash}{image_tag_postfix}"));
}
for tag in additional {
tags.push(format!("{image_name}:{tag}"))
}
}
tags
}
pub fn get_image_tags_as_arg(
&self,
commit_hash: Option<&str>,
additional: &[String],
) -> anyhow::Result<String> {
let mut res = String::new();
for image_tag in self.get_image_tags(
&self.get_image_names(),
commit_hash,
additional,
) {
write!(&mut res, " -t {image_tag}")?;
}
Ok(res)
}
}
#[typeshare]
pub type BuildListItem = ResourceListItem<BuildListItemInfo>;
@@ -290,24 +169,6 @@ pub struct BuildConfig {
#[builder(default)]
pub image_tag: String,
/// Push `:latest` / `:latest-image_tag` tags.
#[serde(default = "default_include_tag")]
#[builder(default = "default_include_tag()")]
#[partial_default(default_include_tag())]
pub include_latest_tag: bool,
/// Push build version semver `:1.19.5` + `1.19` / `:1.19.5-image_tag` tags.
#[serde(default = "default_include_tag")]
#[builder(default = "default_include_tag()")]
#[partial_default(default_include_tag())]
pub include_version_tags: bool,
/// Push commit hash `:a6v8h83` / `:a6v8h83-image_tag` tags.
#[serde(default = "default_include_tag")]
#[builder(default = "default_include_tag()")]
#[partial_default(default_include_tag())]
pub include_commit_tag: bool,
/// Configure quick links that are displayed in the resource header
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
@@ -483,10 +344,6 @@ fn default_auto_increment_version() -> bool {
true
}
fn default_include_tag() -> bool {
true
}
fn default_git_provider() -> String {
String::from("github.com")
}
@@ -520,9 +377,6 @@ impl Default for BuildConfig {
auto_increment_version: default_auto_increment_version(),
image_name: Default::default(),
image_tag: Default::default(),
include_latest_tag: default_include_tag(),
include_version_tags: default_include_tag(),
include_commit_tag: default_include_tag(),
links: Default::default(),
linked_repo: Default::default(),
git_provider: default_git_provider(),
@@ -125,8 +125,6 @@ pub struct Env {
pub komodo_logging_opentelemetry_service_name: Option<String>,
/// Override `pretty_startup_config`
pub komodo_pretty_startup_config: Option<bool>,
/// Override `unsafe_unsanitized_startup_config`
pub komodo_unsafe_unsanitized_startup_config: Option<bool>,
/// Override `transparent_mode`
pub komodo_transparent_mode: Option<bool>,
@@ -511,12 +509,6 @@ pub struct CoreConfig {
#[serde(default)]
pub pretty_startup_config: bool,
/// Unsafe: logs unsanitized config on startup,
/// in order to verify everything is being
/// passed correctly.
#[serde(default)]
pub unsafe_unsanitized_startup_config: bool,
// ===========
// = Pruning =
// ===========
@@ -737,7 +729,6 @@ impl Default for CoreConfig {
github_webhook_app: Default::default(),
logging: Default::default(),
pretty_startup_config: Default::default(),
unsafe_unsanitized_startup_config: Default::default(),
keep_stats_for_days: default_prune_days(),
keep_alerts_for_days: default_prune_days(),
resource_poll_interval: default_poll_interval(),
@@ -781,8 +772,6 @@ impl CoreConfig {
keep_alerts_for_days: config.keep_alerts_for_days,
logging: config.logging,
pretty_startup_config: config.pretty_startup_config,
unsafe_unsanitized_startup_config: config
.unsafe_unsanitized_startup_config,
transparent_mode: config.transparent_mode,
ui_write_disabled: config.ui_write_disabled,
disable_confirm_dialog: config.disable_confirm_dialog,
+51 -11
View File
@@ -5,6 +5,7 @@ use std::{
use anyhow::Context;
use async_timing_util::unix_timestamp_ms;
use build::ImageRegistryConfig;
use clap::Parser;
use derive_empty_traits::EmptyTraits;
use derive_variants::{EnumVariants, ExtractVariant};
@@ -128,6 +129,54 @@ pub fn optional_string(string: impl Into<String>) -> Option<String> {
}
}
pub fn get_image_names(
build::Build {
name,
config:
build::BuildConfig {
image_name,
image_registry,
..
},
..
}: &build::Build,
) -> Vec<String> {
let name = if image_name.is_empty() {
name
} else {
image_name
};
// Local only
if image_registry.is_empty() {
return vec![name.to_string()];
}
image_registry
.iter()
.map(
|ImageRegistryConfig {
domain,
account,
organization,
}| {
match (
!domain.is_empty(),
!organization.is_empty(),
!account.is_empty(),
) {
// If organization and account provided, name under organization.
(true, true, true) => {
format!("{domain}/{organization}/{name}")
}
// Just domain / account provided
(true, false, true) => format!("{domain}/{account}/{name}"),
// Otherwise, just use name (local only)
_ => name.to_string(),
}
},
)
.collect()
}
pub fn to_general_name(name: &str) -> String {
name.trim().replace('\n', "_").to_string()
}
@@ -462,17 +511,8 @@ impl RepoExecutionArgs {
&self,
access_token: Option<&str>,
) -> anyhow::Result<String> {
let access_token_at = match access_token {
Some(token) => match token.split_once(':') {
Some((username, token)) => format!(
"{}:{}@",
urlencoding::encode(username.trim()),
urlencoding::encode(token.trim())
),
None => {
format!("token:{}@", urlencoding::encode(token.trim()))
}
},
let access_token_at = match &access_token {
Some(token) => format!("token:{token}@"),
None => String::new(),
};
let protocol = if self.https { "https" } else { "http" };
-17
View File
@@ -45,9 +45,6 @@ pub struct SystemStatsRecord {
// basic stats
/// Cpu usage percentage
pub cpu_perc: f32,
/// Load average (1m, 5m, 15m)
#[serde(default)]
pub load_average: SystemLoadAverage,
/// Memory used in GB
pub mem_used_gb: f64,
/// Total memory in GB
@@ -75,9 +72,6 @@ pub struct SystemStatsRecord {
pub struct SystemStats {
/// Cpu usage percentage
pub cpu_perc: f32,
/// Load average (1m, 5m, 15m)
#[serde(default)]
pub load_average: SystemLoadAverage,
/// [1.15.9+]
/// Free memory in GB.
/// This is really the 'Free' memory, not the 'Available' memory.
@@ -181,14 +175,3 @@ pub struct SystemProcess {
/// Process disk write in KB/s
pub disk_write_kb: f64,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct SystemLoadAverage {
/// 1m load average
pub one: f64,
/// 5m load average
pub five: f64,
/// 15m load average
pub fifteen: f64,
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "komodo_client",
"version": "1.19.5",
"version": "1.19.1",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",
-5
View File
@@ -199,11 +199,6 @@ export function KomodoClient(url: string, options: InitOptions) {
} else {
// it is a single update
const update = res as any as Update;
if (update.status === UpdateStatus.Complete || !update._id?.$oid) {
return update;
}
return await poll_update_until_complete(update._id?.$oid!);
}
};
+2 -23
View File
@@ -598,12 +598,6 @@ export interface BuildConfig {
* independantly versioned tags.
*/
image_tag?: string;
/** Push `:latest` / `:latest-image_tag` tags. */
include_latest_tag: boolean;
/** Push build version semver `:1.19.5` + `1.19` / `:1.19.5-image_tag` tags. */
include_version_tags: boolean;
/** Push commit hash `:a6v8h83` / `:a6v8h83-image_tag` tags. */
include_commit_tag: boolean;
/** Configure quick links that are displayed in the resource header */
links?: string[];
/** Choose a Komodo Repo (Resource) to source the build files. */
@@ -1357,8 +1351,8 @@ export type ExportResourcesToTomlResponse = TomlResponse;
export type FindUserResponse = User;
export interface ActionActionState {
/** Number of instances of the Action currently running */
running: number;
/** Whether the action is currently running. */
running: boolean;
}
export type GetActionActionStateResponse = ActionActionState;
@@ -2408,15 +2402,6 @@ export interface SystemInformation {
export type GetSystemInformationResponse = SystemInformation;
export interface SystemLoadAverage {
/** 1m load average */
one: number;
/** 5m load average */
five: number;
/** 15m load average */
fifteen: number;
}
/** Info for a single disk mounted on the system. */
export interface SingleDiskUsage {
/** The mount point of the disk */
@@ -2478,8 +2463,6 @@ export enum Timelength {
export interface SystemStats {
/** Cpu usage percentage */
cpu_perc: number;
/** Load average (1m, 5m, 15m) */
load_average?: SystemLoadAverage;
/**
* [1.15.9+]
* Free memory in GB.
@@ -6114,8 +6097,6 @@ export interface SystemStatsRecord {
sid: string;
/** Cpu usage percentage */
cpu_perc: number;
/** Load average (1m, 5m, 15m) */
load_average?: SystemLoadAverage;
/** Memory used in GB */
mem_used_gb: number;
/** Total memory in GB */
@@ -7719,8 +7700,6 @@ export interface RunStackService {
no_tty?: boolean;
/** Do not start linked services */
no_deps?: boolean;
/** Detach container on run */
detach?: boolean;
/** Map service ports to the host */
service_ports?: boolean;
/** Extra environment variables for the run */
-2
View File
@@ -18,8 +18,6 @@ pub struct Build {
/// Propogate any secret replacers from core interpolation.
#[serde(default)]
pub replacers: Vec<(String, String)>,
/// Pass the commit hash to use with tagging
pub commit_hash: Option<String>,
/// Add more tags for this build in addition to the version tags.
#[serde(default)]
pub additional_tags: Vec<String>,
-3
View File
@@ -272,9 +272,6 @@ pub struct ComposeRun {
/// Do not start linked services
#[serde(default)]
pub no_deps: Option<bool>,
/// Detach container on run
#[serde(default)]
pub detach: Option<bool>,
/// Map service ports to the host
#[serde(default)]
pub service_ports: Option<bool>,
+3 -3
View File
@@ -9,14 +9,14 @@
## All fields with a "Default" provided are optional. If they are
## left out of the file, the "Default" value will be used.
## This file is bundled into the official image, `ghcr.io/moghtech/komodo-core`,
## This file is bundled into the official image, `ghcr.io/moghtech/komodo`,
## as the default config at `/config/.default.config.toml`.
## Komodo Core can start with no external config file mounted.
## Komodo can start with no external config file mounted.
## Most fields can also be configured using environment variables.
## Environment variables will override values set in this file.
## Can also use JSON or YAML if preferred. You can convert here:
## Can also use JSON or YAML if preffered. You can convert here:
## - YAML: https://it-tools.tech/toml-to-yaml
## - JSON: https://it-tools.tech/toml-to-json
+1 -1
View File
@@ -9,7 +9,7 @@
## Most fields can also be configured using cli arguments and environment variables.
## These will will override values set in this file. (cli args > env > config files).
## You can also use JSON or YAML if preferred. You can convert here:
## You can also use JSON or YAML if preffered. You can convert here:
## - YAML: https://it-tools.tech/toml-to-yaml
## - JSON: https://it-tools.tech/toml-to-json
+4 -4
View File
@@ -16,7 +16,7 @@
## Most fields can also be configured using environment variables.
## Environment variables will override values set in this file.
## You can also use JSON or YAML if preferred. You can convert here:
## You can also use JSON or YAML if preffered. You can convert here:
## - YAML: https://it-tools.tech/toml-to-yaml
## - JSON: https://it-tools.tech/toml-to-json
@@ -75,10 +75,10 @@ disable_container_exec = false
stats_polling_rate = "5-sec"
## How often Periphery polls the host for container stats,
## Env: PERIPHERY_CONTAINER_STATS_POLLING_RATE
## Env: PERIPHERY_STATS_POLLING_RATE
## Options: https://docs.rs/komodo_client/latest/komodo_client/entities/enum.Timelength.html
## Default: 30-sec
container_stats_polling_rate = "30-sec"
## Default: 5-sec
container_stats_polling_rate = "1-min"
## Whether stack actions should use `docker-compose ...`
## instead of `docker compose ...`.
+1 -2
View File
@@ -15,5 +15,4 @@ These provide alerting implementations which can be used with the `Custom` Alert
- [Telegram](https://github.com/mattsmallman/komodo-alert-to-telgram) by [mattsmallman](https://github.com/mattsmallman)
- [Ntfy](https://github.com/FoxxMD/deploy-ntfy-alerter) by [FoxxMD](https://github.com/FoxxMD)
- [Gotify](https://github.com/FoxxMD/deploy-gotify-alerter) by [FoxxMD](https://github.com/FoxxMD)
- [Apprise](https://github.com/FoxxMD/deploy-apprise-alerter) by [FoxxMD](https://github.com/FoxxMD)
- [Email](https://github.com/gutenye/email-notification/blob/main/src/templates/Komodo/Komodo.md) by [Guten Ye](https://github.com/gutenye)
- [Apprise](https://github.com/FoxxMD/deploy-apprise-alerter) by [FoxxMD](https://github.com/FoxxMD)
+2 -2
View File
@@ -71,7 +71,7 @@ permissions = [
## Administration
Users can be given Admin privileges by a `Super Admin` (only the first user is given this status, set with `super_admin: true` on a User document in database). Super admins will see the "Make Admin" button when on a User page `/users/${user_id}`.
Users can be given Admin priviledges by a `Super Admin` (only the first user is given this status, set with `super_admin: true` on a User document in database). Super admins will see the "Make Admin" button when on a User page `/users/${user_id}`.
These users have unrestricted access to all Komodo Resources. Additionally, these users can update other (non-admin) user's permissions on resources.
@@ -82,4 +82,4 @@ Users also have some configurable global permissions, these are:
- create server permission
- create build permission
Only users with these permissions (as well as admins) can add additional servers to Komodo, and can create additional builds, respectively.
Only users with these permissions (as well as admins) can add additional servers to Komodo, and can create additional builds, respectively.
-4
View File
@@ -14,10 +14,6 @@ Komodo also supports self hosted Oauth2 providers like [Authentik](https://docs.
- `<KOMODO_HOST>/auth/google/callback` for Google.
- `<KOMODO_HOST>/auth/oidc/callback` for OIDC.
### Authentik
Check out the [Authentik official support documentation](https://integrations.goauthentik.io/infrastructure/komodo/).
### Keycloak
- Create an [OIDC client](https://www.keycloak.org/docs/latest/server_admin/index.html#proc-creating-oidc-client_server_administration_guide) in Keycloak.
- Note down the `Client ID` that you enter (e.g.: "komodo"), you will need it for Komodo configuration
+3 -5
View File
@@ -5,12 +5,10 @@ To run Komodo, you will need Docker. See [the docker install docs](https://docs.
### Deploy with Docker Compose
- [**Using MongoDB**](./mongo.mdx)
- Lower CPU usage, Higher RAM usage.
- Some systems [do not support running the latest MongoDB versions](https://github.com/moghtech/komodo/issues/59).
- [**Using FerretDB** (Postgres)](./ferretdb.mdx)
:::info
Some systems [do not support running the latest MongoDB versions](https://github.com/moghtech/komodo/issues/59).
Users with these systems should use FerretDB instead.
:::
- Lower RAM usage, Higher CPU usage.
:::info
**FerretDB v1** users:
-1
View File
@@ -40,7 +40,6 @@
"jotai": "2.12.5",
"lucide-react": "0.511.0",
"monaco-editor": "0.52.2",
"monaco-yaml": "5.4.0",
"prettier": "3.5.3",
"react": "19.1.0",
"react-charts": "3.0.0-beta.57",
-3
View File
@@ -88,9 +88,6 @@ export function KomodoClient(url, options) {
else {
// it is a single update
const update = res;
if (update.status === UpdateStatus.Complete || !update._id?.$oid) {
return update;
}
return await poll_update_until_complete(update._id?.$oid);
}
};
+2 -22
View File
@@ -595,12 +595,6 @@ export interface BuildConfig {
* independantly versioned tags.
*/
image_tag?: string;
/** Push `:latest` / `:latest-image_tag` tags. */
include_latest_tag: boolean;
/** Push build version semver `:1.19.5` + `1.19` / `:1.19.5-image_tag` tags. */
include_version_tags: boolean;
/** Push commit hash `:a6v8h83` / `:a6v8h83-image_tag` tags. */
include_commit_tag: boolean;
/** Configure quick links that are displayed in the resource header */
links?: string[];
/** Choose a Komodo Repo (Resource) to source the build files. */
@@ -1463,8 +1457,8 @@ export type ExportAllResourcesToTomlResponse = TomlResponse;
export type ExportResourcesToTomlResponse = TomlResponse;
export type FindUserResponse = User;
export interface ActionActionState {
/** Number of instances of the Action currently running */
running: number;
/** Whether the action is currently running. */
running: boolean;
}
export type GetActionActionStateResponse = ActionActionState;
export type GetActionResponse = Action;
@@ -2523,14 +2517,6 @@ export interface SystemInformation {
container_exec_disabled: boolean;
}
export type GetSystemInformationResponse = SystemInformation;
export interface SystemLoadAverage {
/** 1m load average */
one: number;
/** 5m load average */
five: number;
/** 15m load average */
fifteen: number;
}
/** Info for a single disk mounted on the system. */
export interface SingleDiskUsage {
/** The mount point of the disk */
@@ -2590,8 +2576,6 @@ export declare enum Timelength {
export interface SystemStats {
/** Cpu usage percentage */
cpu_perc: number;
/** Load average (1m, 5m, 15m) */
load_average?: SystemLoadAverage;
/**
* [1.15.9+]
* Free memory in GB.
@@ -5868,8 +5852,6 @@ export interface SystemStatsRecord {
sid: string;
/** Cpu usage percentage */
cpu_perc: number;
/** Load average (1m, 5m, 15m) */
load_average?: SystemLoadAverage;
/** Memory used in GB */
mem_used_gb: number;
/** Total memory in GB */
@@ -7312,8 +7294,6 @@ export interface RunStackService {
no_tty?: boolean;
/** Do not start linked services */
no_deps?: boolean;
/** Detach container on run */
detach?: boolean;
/** Map service ports to the host */
service_ports?: boolean;
/** Extra environment variables for the run */
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -136,12 +136,12 @@ const GroupActionDialog = ({
return (
<Dialog open={!!action} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="max-h-[85vh] overflow-y-auto">
<DialogContent>
<DialogHeader>
<DialogTitle>Group Execute - {formatted}</DialogTitle>
</DialogHeader>
<div className="py-8 flex flex-col gap-4">
<ul className="p-4 bg-accent text-sm list-disc list-inside max-h-[300px] overflow-y-auto">
<ul className="p-4 bg-accent text-sm list-disc list-inside">
{selected.map((resource) => (
<li key={resource}>{resource}</li>
))}
+4 -15
View File
@@ -170,7 +170,6 @@ interface SectionProps {
actions?: ReactNode;
// otherwise items-start
itemsCenterTitleRow?: boolean;
className?: string;
}
export const Section = ({
@@ -181,9 +180,8 @@ export const Section = ({
actions,
children,
itemsCenterTitleRow,
className,
}: SectionProps) => (
<div className={cn("flex flex-col gap-4", className)}>
<div className="flex flex-col gap-4">
{(title || icon || titleRight || titleOther || actions) && (
<div
className={cn(
@@ -224,7 +222,6 @@ export const NewLayout = ({
}) => {
const [open, set] = useState(false);
const [loading, setLoading] = useState(false);
return (
<Dialog
open={open}
@@ -251,17 +248,9 @@ export const NewLayout = ({
variant="secondary"
onClick={async () => {
setLoading(true);
try {
await onConfirm();
set(false);
} catch (error: any) {
const status = error?.status || error?.response?.status;
if (status !== 409 && status !== 400) {
set(false);
}
} finally {
setLoading(false);
}
await onConfirm();
setLoading(false);
set(false);
}}
disabled={!enabled || loading}
>
+16 -15
View File
@@ -56,14 +56,12 @@ export const MonacoEditor = ({
onValueChange,
language: _language,
readOnly,
filename,
minHeight,
className,
}: {
value: string | undefined;
onValueChange?: (value: string) => void;
language: MonacoLanguage | undefined;
filename?: string;
readOnly?: boolean;
minHeight?: number;
className?: string;
@@ -141,7 +139,13 @@ export const MonacoEditor = ({
)}px`;
}, [editor, line_count]);
const { currentTheme } = useTheme();
const { theme: _theme } = useTheme();
const theme =
_theme === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: _theme;
const options: monaco.editor.IStandaloneEditorConstructionOptions = {
minimap: { enabled: false },
@@ -165,8 +169,7 @@ export const MonacoEditor = ({
<Editor
language={language}
value={value}
theme={currentTheme}
defaultPath={defaultPath(filename)}
theme={theme}
options={options}
onChange={(v) => onValueChange?.(v ?? "")}
onMount={(editor) => setEditor(editor)}
@@ -175,14 +178,6 @@ export const MonacoEditor = ({
);
};
const defaultPath = (filename?: string) => {
if (!filename) return undefined;
// Extract only the filename part of path,
// avoiding critical issue when path starts with '/'
const split = filename.split("/");
return split[split.length - 1];
};
const MIN_DIFF_HEIGHT = 100;
const MAX_DIFF_HEIGHT = 400;
@@ -227,7 +222,13 @@ export const MonacoDiffEditor = ({
)}px`;
}, [editor, line_count]);
const { currentTheme } = useTheme();
const { theme: _theme } = useTheme();
const theme =
_theme === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: _theme;
const options: monaco.editor.IStandaloneDiffEditorConstructionOptions = {
minimap: { enabled: true },
@@ -250,7 +251,7 @@ export const MonacoDiffEditor = ({
language={language}
original={original}
modified={modified}
theme={currentTheme}
theme={theme}
options={options}
onMount={(editor) => {
const modifiedEditor = editor.getModifiedEditor();
+2 -6
View File
@@ -1,4 +1,4 @@
import { useAllResources, useLocalStorage, useRead, useSettingsView, useUser } from "@lib/hooks";
import { useAllResources, useLocalStorage, useRead, useUser } from "@lib/hooks";
import { Button } from "@ui/button";
import {
CommandDialog,
@@ -126,7 +126,6 @@ const useOmniItems = (
): Record<string, OmniItem[]> => {
const user = useUser().data;
const resources = useAllResources();
const [_, setSettingsView] = useSettingsView();
return useMemo(() => {
const searchTerms = search
.toLowerCase()
@@ -167,10 +166,7 @@ const useOmniItems = (
type: "Server" as UsableResource,
label: "Users",
icon: <User className="w-4 h-4" />,
onSelect: () => {
setSettingsView("Users");
nav("/settings");
},
onSelect: () => nav("/users"),
template: false,
}) as OmniItem,
]
@@ -119,12 +119,11 @@ export const ActionComponents: RequiredResourceComponents = {
Actions: {
RunAction: ({ id }) => {
const running =
(useRead(
"GetActionActionState",
{ action: id },
{ refetchInterval: 5000 }
).data?.running ?? 0) > 0;
const running = useRead(
"GetActionActionState",
{ action: id },
{ refetchInterval: 5000 }
).data?.running;
const { mutate, isPending } = useExecute("RunAction");
const action = useAction(id);
if (!action) return null;
@@ -163,7 +163,6 @@ export const BuildConfig = ({
const version_component: ConfigComponent<Types.BuildConfig> = {
label: "Version",
labelHidden: true,
components: {
version: (_version, set) => {
const version =
@@ -174,7 +173,6 @@ export const BuildConfig = ({
<ConfigInput
className="text-lg w-[200px]"
label="Version"
boldLabel
description="Version the image with major.minor.patch. It can be interpolated using [[$VERSION]]."
placeholder="0.0.0"
value={version}
@@ -230,8 +228,6 @@ export const BuildConfig = ({
};
const imageName = (update.image_name ?? config.image_name) || name;
const customTag = update.image_tag ?? config.image_tag;
const customTagPostfix = customTag ? `-${customTag}` : "";
const general_common: ConfigComponent<Types.BuildConfig>[] = [
{
@@ -296,29 +292,6 @@ export const BuildConfig = ({
),
},
},
{
label: "Tagging",
labelHidden: true,
components: {
image_name: {
description: "Push the image under a different name",
placeholder: "Custom image name",
},
image_tag: {
description: `Push a custom tag, plus postfix the other tags (eg ':latest-${customTag ? customTag : "<TAG>"}').`,
placeholder: "Custom image tag",
},
include_latest_tag: {
description: `:latest${customTagPostfix}`,
},
include_version_tags: {
description: `:X.Y.Z${customTagPostfix} + :X.Y${customTagPostfix} + :X${customTagPostfix}`,
},
include_commit_tag: {
description: `:ae8f8ff${customTagPostfix}`,
},
},
},
{
label: "Links",
labelHidden: true,
@@ -341,6 +314,19 @@ export const BuildConfig = ({
];
const advanced: ConfigComponent<Types.BuildConfig>[] = [
{
label: "Tagging",
components: {
image_name: {
description: "Push the image under a different name",
placeholder: "Custom image name",
},
image_tag: {
description: "Postfix the image version with a custom tag.",
placeholder: "Custom image tag",
},
},
},
{
label: "Pre Build",
description:
+12 -25
View File
@@ -439,23 +439,12 @@ export const CopyResource = ({
const nav = useNavigate();
const inv = useInvalidate();
const { mutateAsync: copy } = useWrite(`Copy${type}`);
const onConfirm = async () => {
if (!name) return;
try {
const res = await copy({ id, name });
const { mutate } = useWrite(`Copy${type}`, {
onSuccess: (res) => {
inv([`List${type}s`]);
nav(`/${usableResourcePath(type)}/${res._id?.$oid}`);
setOpen(false);
} catch (error: any) {
// Keep dialog open for validation errors (409/400), close for system errors
const status = error?.status || error?.response?.status;
if (status !== 409 && status !== 400) {
setOpen(false);
}
}
};
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
@@ -483,8 +472,9 @@ export const CopyResource = ({
title="Copy"
icon={<Check className="w-4 h-4" />}
disabled={!name}
onClick={async () => {
await onConfirm();
onClick={() => {
mutate({ id, name });
setOpen(false);
}}
/>
</DialogFooter>
@@ -536,13 +526,10 @@ export const NewResource = ({
: {};
const onConfirm = async () => {
if (!name) toast({ title: "Name cannot be empty" });
const result = templateId
? await copy({ name, id: templateId })
: await create({ name, config });
const resourceId = result._id?.$oid;
if (resourceId) {
nav(`/${usableResourcePath(type)}/${resourceId}`);
}
const id = templateId
? (await copy({ name, id: templateId }))._id?.$oid!
: (await create({ name, config }))._id?.$oid!;
nav(`/${usableResourcePath(type)}/${id}`);
};
return (
<NewLayout
@@ -560,7 +547,7 @@ export const NewResource = ({
onKeyDown={(e) => {
if (!name) return;
if (e.key === "Enter") {
onConfirm().catch(() => {});
onConfirm();
}
}}
/>
@@ -1132,7 +1132,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
command: undefined,
no_tty: undefined,
no_deps: undefined,
detach: undefined,
service_ports: undefined,
env: undefined,
workdir: undefined,
@@ -1152,7 +1151,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
);
const [no_tty, setNoTty] = useState(!!params.no_tty);
const [no_deps, setNoDeps] = useState(!!params.no_deps);
const [detach, setDetach] = useState(!!params.detach);
const [service_ports, setServicePorts] = useState(!!params.service_ports);
const [workdir, setWorkdir] = useState(params.workdir ?? "");
const [user, setUser] = useState(params.user ?? "");
@@ -1177,7 +1175,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
);
setNoTty(!!params.no_tty);
setNoDeps(!!params.no_deps);
setDetach(!!params.detach);
setServicePorts(!!params.service_ports);
setWorkdir(params.workdir ?? "");
setUser(params.user ?? "");
@@ -1219,7 +1216,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
user: user || undefined,
entrypoint: entrypoint || undefined,
pull: pull ? true : undefined,
detach: detach ? true : undefined,
env,
} as any);
setOpen(false);
@@ -1277,10 +1273,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
<Switch checked={no_deps} onCheckedChange={setNoDeps} />
<span className="text-sm">No Dependencies</span>
</label>
<label className="flex items-center gap-2">
<Switch checked={detach} onCheckedChange={setDetach} />
<span className="text-sm">Detach</span>
</label>
<label className="flex items-center gap-2">
<Switch
checked={service_ports}
@@ -7,7 +7,7 @@ import { useExecute, useInvalidate, useRead, useWrite } from "@lib/hooks";
import { file_contents_empty, sync_no_changes } from "@lib/utils";
import { usePermissions } from "@lib/hooks";
import { NotebookPen, RefreshCcw, SquarePlay } from "lucide-react";
import { useFullResourceSync, useResourceSyncTabsView } from ".";
import { useFullResourceSync, usePendingView } from ".";
export const RefreshSync = ({ id }: { id: string }) => {
const inv = useInvalidate();
@@ -34,10 +34,11 @@ export const ExecuteSync = ({ id }: { id: string }) => {
{ refetchInterval: 5000 }
).data?.syncing;
const sync = useFullResourceSync(id);
const { view } = useResourceSyncTabsView(sync);
const [_pendingView] = usePendingView();
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
if (
view !== "Execute" ||
pendingView === "Commit" ||
!sync ||
sync_no_changes(sync) ||
!sync.info?.remote_contents
@@ -72,12 +73,11 @@ export const ExecuteSync = ({ id }: { id: string }) => {
export const CommitSync = ({ id }: { id: string }) => {
const { mutate, isPending } = useWrite("CommitSync");
const sync = useFullResourceSync(id);
const { view } = useResourceSyncTabsView(sync);
const { canWrite } = usePermissions({ type: "ResourceSync", id });
const [_pendingView] = usePendingView();
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
if (view !== "Commit" || !canWrite || !sync) {
return null;
}
if (pendingView === "Execute" || !canWrite || !sync) return null;
const freshSync =
!sync.config?.files_on_host &&
@@ -1,4 +1,4 @@
import { atomWithStorage, useRead, useUser } from "@lib/hooks";
import { atomWithStorage, useLocalStorage, useRead, useUser } from "@lib/hooks";
import { RequiredResourceComponents } from "@types";
import { Card } from "@ui/card";
import { Clock, FolderSync } from "lucide-react";
@@ -45,16 +45,23 @@ const ResourceSyncIcon = ({ id, size }: { id?: string; size: number }) => {
return <FolderSync className={cn(`w-${size} h-${size}`, state && color)} />;
};
type ResourceSyncTabsView = "Config" | "Info" | "Execute" | "Commit";
const syncTabsViewAtom = atomWithStorage<ResourceSyncTabsView>(
"sync-tabs-v4",
"Config"
const pendingViewAtom = atomWithStorage<"Execute" | "Commit">(
"sync-view-v1",
"Execute"
);
export const usePendingView = () => {
return useAtom(pendingViewAtom) as [
"Execute" | "Commit",
(view: "Execute" | "Commit") => void,
];
};
export const useResourceSyncTabsView = (
sync: Types.ResourceSync | undefined
) => {
const [_view, setView] = useAtom<ResourceSyncTabsView>(syncTabsViewAtom);
const ConfigInfoPending = ({ id }: { id: string }) => {
const [_view, setView] = useLocalStorage<"Config" | "Info" | "Pending">(
"sync-tabs-v3",
"Config"
);
const sync = useFullResourceSync(id);
const hideInfo = sync?.config?.files_on_host
? false
@@ -68,28 +75,13 @@ export const useResourceSyncTabsView = (
const view =
_view === "Info" && hideInfo
? "Config"
: (_view === "Execute" || _view === "Commit") && !showPending
: _view === "Pending" && !showPending
? sync?.config?.files_on_host ||
sync?.config?.repo ||
sync?.config?.linked_repo
? "Info"
: "Config"
: _view === "Commit" && !sync?.config?.managed
? "Execute"
: _view;
return {
view,
setView,
hideInfo,
showPending,
};
};
const ConfigInfoPending = ({ id }: { id: string }) => {
const sync = useFullResourceSync(id);
const { view, setView, hideInfo, showPending } =
useResourceSyncTabsView(sync);
: _view;
const title = (
<TabsList className="justify-start w-fit">
@@ -104,21 +96,12 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
Info
</TabsTrigger>
<TabsTrigger
value="Execute"
value="Pending"
className="w-[110px]"
disabled={!showPending}
>
Execute
Pending
</TabsTrigger>
{sync?.config?.managed && (
<TabsTrigger
value="Commit"
className="w-[110px]"
disabled={!showPending}
>
Commit
</TabsTrigger>
)}
</TabsList>
);
return (
@@ -129,10 +112,7 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
<TabsContent value="Info">
<ResourceSyncInfo id={id} titleOther={title} />
</TabsContent>
<TabsContent value="Execute">
<ResourceSyncPending id={id} titleOther={title} />
</TabsContent>
<TabsContent value="Commit">
<TabsContent value="Pending">
<ResourceSyncPending id={id} titleOther={title} />
</TabsContent>
</Tabs>
@@ -184,7 +164,7 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
},
GroupActions: () => (
<GroupActions type="ResourceSync" actions={["RunSync", "CommitSync"]} />
<GroupActions type="ResourceSync" actions={["RunSync"]} />
),
Table: ({ resources }) => (
@@ -10,7 +10,8 @@ import { cn, sanitizeOnlySpan } from "@lib/utils";
import { ConfirmButton } from "@components/util";
import { SquarePlay } from "lucide-react";
import { usePermissions } from "@lib/hooks";
import { useFullResourceSync, useResourceSyncTabsView } from ".";
import { useFullResourceSync, usePendingView } from ".";
import { Tabs, TabsList, TabsTrigger } from "@ui/tabs";
import { ResourceDiff } from "komodo_client/dist/types";
export const ResourceSyncPending = ({
@@ -23,16 +24,31 @@ export const ResourceSyncPending = ({
const syncing = useRead("GetResourceSyncActionState", { sync: id }).data
?.syncing;
const sync = useFullResourceSync(id);
const { view } = useResourceSyncTabsView(sync);
const { canExecute } = usePermissions({ type: "ResourceSync", id });
const [_pendingView, setPendingView] = usePendingView();
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
const { mutate, isPending } = useExecute("RunSync");
const loading = isPending || syncing;
return (
<Section titleOther={titleOther} className="min-h-[500px]">
<div className="flex items-center gap-4 pl-1 py-2 flex-wrap">
<div className="text-muted-foreground">{view} Mode:</div>
<Section
titleOther={titleOther}
>
<div className="flex items-center gap-4 py-2 flex-wrap">
{sync?.config?.managed && (
<Tabs value={pendingView} onValueChange={setPendingView as any}>
<TabsList className="justify-start w-fit">
<TabsTrigger value="Execute" className="w-[110px]">
Execute
</TabsTrigger>
<TabsTrigger value="Commit" className="w-[110px]">
Commit
</TabsTrigger>
</TabsList>
</Tabs>
)}
<div className="text-muted-foreground">{pendingView} Mode:</div>
<div className="flex items-center gap-1 flex-wrap">
{view === "Execute" && (
{pendingView === "Execute" && (
<>
Update resources in the
<div className="font-bold">UI</div>
@@ -40,7 +56,7 @@ export const ResourceSyncPending = ({
<div className="font-bold">file changes.</div>
</>
)}
{view === "Commit" && (
{pendingView === "Commit" && (
<>
Update resources in the
<div className="font-bold">file</div>
@@ -73,7 +89,7 @@ export const ResourceSyncPending = ({
) : undefined}
{/* Pending Deploy */}
{view === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
{pendingView === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
<Card>
<CardHeader
className={cn(
@@ -102,10 +118,13 @@ export const ResourceSyncPending = ({
<div className="flex items-center gap-4 font-mono">
<div
className={text_color_class_by_intention(
diff_type_intention(update.data.type, view === "Commit")
diff_type_intention(
update.data.type,
pendingView === "Commit"
)
)}
>
{view === "Commit"
{pendingView === "Commit"
? reverse_pending_type(update.data.type)
: update.data.type}{" "}
{update.target.type}
@@ -120,7 +139,7 @@ export const ResourceSyncPending = ({
/>
)}
</div>
{canExecute && view === "Execute" && (
{canExecute && pendingView === "Execute" && (
<ConfirmButton
title="Execute Change"
icon={<SquarePlay className="w-4 h-4" />}
@@ -149,7 +168,7 @@ export const ResourceSyncPending = ({
)}
{update.data.type === "Update" && (
<>
{view === "Execute" && (
{pendingView === "Execute" && (
<MonacoDiffEditor
original={update.data.data.current}
modified={update.data.data.proposed}
@@ -157,7 +176,7 @@ export const ResourceSyncPending = ({
readOnly
/>
)}
{view === "Commit" && (
{pendingView === "Commit" && (
<MonacoDiffEditor
original={update.data.data.proposed}
modified={update.data.data.current}
@@ -186,11 +205,13 @@ export const ResourceSyncPending = ({
className={cn(
"font-mono pb-2",
text_color_class_by_intention(
diff_type_intention(data.type, view === "Commit")
diff_type_intention(data.type, pendingView === "Commit")
)
)}
>
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
{pendingView === "Commit"
? reverse_pending_type(data.type)
: data.type}{" "}
Variable
</CardHeader>
<CardContent>
@@ -203,7 +224,7 @@ export const ResourceSyncPending = ({
)}
{data.type === "Update" && (
<>
{view === "Execute" && (
{pendingView === "Execute" && (
<MonacoDiffEditor
original={data.data.current}
modified={data.data.proposed}
@@ -211,7 +232,7 @@ export const ResourceSyncPending = ({
readOnly
/>
)}
{view === "Commit" && (
{pendingView === "Commit" && (
<MonacoDiffEditor
original={data.data.proposed}
modified={data.data.current}
@@ -240,11 +261,13 @@ export const ResourceSyncPending = ({
className={cn(
"font-mono pb-2",
text_color_class_by_intention(
diff_type_intention(data.type, view === "Commit")
diff_type_intention(data.type, pendingView === "Commit")
)
)}
>
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
{pendingView === "Commit"
? reverse_pending_type(data.type)
: data.type}{" "}
User Group
</CardHeader>
<CardContent>
@@ -257,7 +280,7 @@ export const ResourceSyncPending = ({
)}
{data.type === "Update" && (
<>
{view === "Execute" && (
{pendingView === "Execute" && (
<MonacoDiffEditor
original={data.data.current}
modified={data.data.proposed}
@@ -265,7 +288,7 @@ export const ResourceSyncPending = ({
readOnly
/>
)}
{view === "Commit" && (
{pendingView === "Commit" && (
<MonacoDiffEditor
original={data.data.proposed}
modified={data.data.current}
@@ -44,12 +44,6 @@ export const useServer = (id?: string) =>
(d) => d.id === id
);
// Helper function to check if server is available for API calls
export const useIsServerAvailable = (serverId?: string) => {
const server = useServer(serverId);
return server?.info.state === Types.ServerState.Ok;
};
export const useFullServer = (id: string) =>
useRead("GetServer", { server: id }, { refetchInterval: 10_000 }).data;
@@ -57,26 +51,24 @@ export const useFullServer = (id: string) =>
export const useVersionMismatch = (serverId?: string) => {
const core_version = useRead("GetVersion", {}).data?.version;
const server_version = useServer(serverId)?.info.version;
const unknown = !server_version || server_version === "Unknown";
const mismatch =
!!server_version && !!core_version && server_version !== core_version;
const mismatch = !!server_version && !!core_version && server_version !== core_version;
return { unknown, mismatch, hasVersionMismatch: mismatch && !unknown };
};
const Icon = ({ id, size }: { id?: string; size: number }) => {
const state = useServer(id)?.info.state;
const { hasVersionMismatch } = useVersionMismatch(id);
return (
<Server
className={cn(
`w-${size} h-${size}`,
state &&
stroke_color_class_by_intention(
server_state_intention(state, hasVersionMismatch)
)
state && stroke_color_class_by_intention(
server_state_intention(state, hasVersionMismatch)
)
)}
/>
);
@@ -145,7 +137,10 @@ const ConfigTabs = ({ id }: { id: string }) => {
</TabsList>
);
return (
<Tabs value={currentView} onValueChange={setView as any}>
<Tabs
value={currentView}
onValueChange={setView as any}
>
<TabsContent value="Config">
<ServerConfig id={id} titleOther={tabsList} />
</TabsContent>
@@ -226,10 +221,10 @@ export const ServerVersion = ({ id }: { id: string }) => {
const core_version = useRead("GetVersion", {}).data?.version;
const version = useServer(id)?.info.version;
const server_state = useServer(id)?.info.state;
const unknown = !version || version === "Unknown";
const mismatch = !!version && !!core_version && version !== core_version;
// Don't show version for disabled servers
if (server_state === Types.ServerState.Disabled) {
return (
@@ -247,14 +242,13 @@ export const ServerVersion = ({ id }: { id: string }) => {
</TooltipTrigger>
<TooltipContent>
<div>
Server is <span className="font-bold">disabled</span> - version
unknown.
Server is <span className="font-bold">disabled</span> - version unknown.
</div>
</TooltipContent>
</Tooltip>
);
}
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -312,11 +306,7 @@ export const ServerComponents: RequiredResourceComponents = {
),
Dashboard: () => {
const summary = useRead(
"GetServersSummary",
{},
{ refetchInterval: 15_000 }
).data;
const summary = useRead("GetServersSummary", {}, { refetchInterval: 15_000 }).data;
return (
<DashboardPieChart
data={[
@@ -373,19 +363,18 @@ export const ServerComponents: RequiredResourceComponents = {
State: ({ id }) => {
const state = useServer(id)?.info.state;
const { hasVersionMismatch } = useVersionMismatch(id);
// Show full version mismatch text
const displayState =
state === Types.ServerState.Ok && hasVersionMismatch
? "Version Mismatch"
: state === Types.ServerState.NotOk
? "Not Ok"
: state;
const displayState = state === Types.ServerState.Ok && hasVersionMismatch
? "Version Mismatch"
: state === Types.ServerState.NotOk
? "Not Ok"
: state;
return (
<StatusBadge
text={displayState}
intent={server_state_intention(state, hasVersionMismatch)}
<StatusBadge
text={displayState}
intent={server_state_intention(state, hasVersionMismatch)}
/>
);
},
@@ -395,13 +384,13 @@ export const ServerComponents: RequiredResourceComponents = {
Info: {
Version: ServerVersion,
Cpu: ({ id }) => {
const isServerAvailable = useIsServerAvailable(id);
const server = useServer(id);
const core_count =
useRead(
"GetSystemInformation",
{ server: id },
{
enabled: isServerAvailable,
enabled: server ? server.info.state !== "Disabled" : false,
refetchInterval: 5000,
}
).data?.core_count ?? 0;
@@ -412,34 +401,13 @@ export const ServerComponents: RequiredResourceComponents = {
</div>
);
},
LoadAvg: ({ id }) => {
const isServerAvailable = useIsServerAvailable(id);
const stats = useRead(
"GetSystemStats",
{ server: id },
{
enabled: isServerAvailable,
refetchInterval: 5000,
}
).data;
if (!stats?.load_average) return null;
const one = stats.load_average?.one;
return (
<div className="flex gap-2 items-center">
<Cpu className="w-4 h-4" />
{one.toFixed(2)}
</div>
);
},
Mem: ({ id }) => {
const isServerAvailable = useIsServerAvailable(id);
const server = useServer(id);
const stats = useRead(
"GetSystemStats",
{ server: id },
{
enabled: isServerAvailable,
enabled: server ? server.info.state !== "Disabled" : false,
refetchInterval: 5000,
}
).data;
@@ -451,12 +419,12 @@ export const ServerComponents: RequiredResourceComponents = {
);
},
Disk: ({ id }) => {
const isServerAvailable = useIsServerAvailable(id);
const server = useServer(id);
const stats = useRead(
"GetSystemStats",
{ server: id },
{
enabled: isServerAvailable,
enabled: server ? server.info.state !== "Disabled" : false,
refetchInterval: 5000,
}
).data;
@@ -627,12 +595,11 @@ export const ServerComponents: RequiredResourceComponents = {
const { hasVersionMismatch } = useVersionMismatch(id);
// Determine display state for header (longer text is okay in header)
const displayState =
server?.info.state === Types.ServerState.Ok && hasVersionMismatch
? "Version Mismatch"
: server?.info.state === Types.ServerState.NotOk
? "Not Ok"
: server?.info.state;
const displayState = server?.info.state === Types.ServerState.Ok && hasVersionMismatch
? "Version Mismatch"
: server?.info.state === Types.ServerState.NotOk
? "Not Ok"
: server?.info.state;
return (
<ResourcePageHeader
@@ -1,199 +0,0 @@
import { ResourceLink } from "@components/resources/common";
import { ServerComponents } from "@components/resources/server";
import { DataTable, SortableHeader } from "@ui/data-table";
import { useRead } from "@lib/hooks";
import { useMemo } from "react";
import { useIsServerAvailable } from ".";
export const ServerMonitoringTable = ({ search = "" }: { search?: string }) => {
const servers = useRead("ListServers", {}).data;
const searchSplit = useMemo(
() => search.toLowerCase().split(" ").filter((t) => t),
[search]
);
const filtered = useMemo(
() =>
servers?.filter((s) =>
searchSplit.length === 0
? true
: searchSplit.every((t) => s.name.toLowerCase().includes(t))
) ?? [],
[servers, searchSplit]
);
return (
<div className="flex flex-col gap-4">
<DataTable<any, any>
tableKey="servers-monitoring-v1"
data={filtered}
columns={[
{
accessorKey: "name",
size: 250,
header: ({ column }) => (
<SortableHeader column={column} title="System" />
),
cell: ({ row }) => (
<div className="flex items-center gap-2">
<ResourceLink type="Server" id={row.original.id} />
</div>
),
},
{
header: "CPU",
size: 200,
cell: ({ row }) => <CpuCell id={row.original.id} />,
},
{
header: "Memory",
size: 200,
cell: ({ row }) => <MemCell id={row.original.id} />,
},
{
header: "Disk",
size: 200,
cell: ({ row }) => <DiskCell id={row.original.id} />,
},
{
header: "Load Avg",
size: 160,
cell: ({ row }) => <LoadAvgCell id={row.original.id} />,
},
{
header: "Net",
size: 100,
cell: ({ row }) => <NetCell id={row.original.id} />,
},
{
header: "Agent",
size: 160,
cell: ({ row }) => <ServerComponents.Info.Version id={row.original.id} />,
},
]}
/>
</div>
);
};
const useStats = (id: string) => {
const isServerAvailable = useIsServerAvailable(id);
return useRead("GetSystemStats", { server: id }, {
enabled: isServerAvailable,
refetchInterval: 10_000
}).data;
};
const useServerThresholds = (id: string) => {
const isServerAvailable = useIsServerAvailable(id);
const config = useRead("GetServer", { server: id }, {
enabled: isServerAvailable
}).data?.config as any;
return {
cpuWarning: config?.cpu_warning ?? 75,
cpuCritical: config?.cpu_critical ?? 90,
memWarning: config?.mem_warning ?? 75,
memCritical: config?.mem_critical ?? 90,
diskWarning: config?.disk_warning ?? 75,
diskCritical: config?.disk_critical ?? 90,
};
};
const Bar = ({ valuePerc, intent }: { valuePerc?: number; intent: "Good" | "Warning" | "Critical" }) => {
const w = Math.max(0, Math.min(100, valuePerc ?? 0)) / 100;
const color = intent === "Good" ? "bg-green-500" : intent === "Warning" ? "bg-orange-500" : "bg-red-500";
return (
<span className="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span className={`absolute inset-0 w-full h-full origin-left ${color}`} style={{ transform: `scaleX(${w})` }} />
</span>
);
};
const CpuCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const cpu = stats?.cpu_perc ?? 0;
const { cpuWarning: warning, cpuCritical: critical } = useServerThresholds(id);
const intent: "Good" | "Warning" | "Critical" =
cpu < warning ? "Good" : cpu < critical ? "Warning" : "Critical";
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{cpu.toFixed(2)}%</span>
<Bar valuePerc={cpu} intent={intent} />
</div>
);
};
const MemCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const used = stats?.mem_used_gb ?? 0;
const total = stats?.mem_total_gb ?? 0;
const perc = total > 0 ? (used / total) * 100 : 0;
const { memWarning: warning, memCritical: critical } = useServerThresholds(id);
const intent: "Good" | "Warning" | "Critical" =
perc < warning ? "Good" : perc < critical ? "Warning" : "Critical";
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{perc.toFixed(1)}%</span>
<Bar valuePerc={perc} intent={intent} />
</div>
);
};
const DiskCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const used = stats?.disks?.reduce((acc, d) => acc + (d.used_gb || 0), 0) ?? 0;
const total = stats?.disks?.reduce((acc, d) => acc + (d.total_gb || 0), 0) ?? 0;
const perc = total > 0 ? (used / total) * 100 : 0;
const { diskWarning: warning, diskCritical: critical } = useServerThresholds(id);
const intent: "Good" | "Warning" | "Critical" =
perc < warning ? "Good" : perc < critical ? "Warning" : "Critical";
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{perc.toFixed(1)}%</span>
<Bar valuePerc={perc} intent={intent} />
</div>
);
};
const formatRate = (bytes?: number) => {
const b = bytes ?? 0;
const kb = 1024;
const mb = kb * 1024;
const gb = mb * 1024;
if (b >= gb) return `${(b / gb).toFixed(2)} GB/s`;
if (b >= mb) return `${(b / mb).toFixed(2)} MB/s`;
if (b >= kb) return `${(b / kb).toFixed(2)} KB/s`;
return `${b.toFixed(0)} B/s`;
};
const NetCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const ingress = stats?.network_ingress_bytes ?? 0;
const egress = stats?.network_egress_bytes ?? 0;
return (
<span className="tabular-nums whitespace-nowrap">
{formatRate(ingress + egress)}
</span>
);
};
const LoadAvgCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const one = stats?.load_average?.one;
const five = stats?.load_average?.five;
const fifteen = stats?.load_average?.fifteen;
if (one === undefined || five === undefined || fifteen === undefined) {
return (
<div className="w-full flex items-center gap-[.35em] tabular-nums text-muted-foreground tracking-tight">
<span>N/A</span>
</div>
);
}
return (
<div className="w-full flex items-center gap-[.35em] tabular-nums tracking-tight">
<span>{one.toFixed(2)}</span>
<span>{five.toFixed(2)}</span>
<span>{fifteen.toFixed(2)}</span>
</div>
);
};
@@ -3,19 +3,13 @@ import { useRead } from "@lib/hooks";
import { Types } from "komodo_client";
import { useMemo } from "react";
import { useStatsGranularity } from "./hooks";
import { Loader2, OctagonAlert } from "lucide-react";
import { Loader2 } from "lucide-react";
import { AxisOptions, Chart } from "react-charts";
import { convertTsMsToLocalUnixTsInMs } from "@lib/utils";
import { useTheme } from "@ui/theme";
import { fmt_utc_date } from "@lib/formatting";
type StatType =
| "Cpu"
| "Memory"
| "Disk"
| "Network Ingress"
| "Network Egress"
| "Load Average";
type StatType = "Cpu" | "Memory" | "Disk" | "Network Ingress" | "Network Egress";
type StatDatapoint = { date: number; value: number };
@@ -35,34 +29,18 @@ export const StatChart = ({
granularity,
});
const seriesData = useMemo(() => {
if (!data?.stats) return [] as { label: string; data: StatDatapoint[] }[];
const records = [...data.stats].reverse();
if (type === "Load Average") {
const one = records.map((s) => ({
date: convertTsMsToLocalUnixTsInMs(s.ts),
value: s.load_average?.one ?? 0,
}));
const five = records.map((s) => ({
date: convertTsMsToLocalUnixTsInMs(s.ts),
value: s.load_average?.five ?? 0,
}));
const fifteen = records.map((s) => ({
date: convertTsMsToLocalUnixTsInMs(s.ts),
value: s.load_average?.fifteen ?? 0,
}));
return [
{ label: "1m", data: one },
{ label: "5m", data: five },
{ label: "15m", data: fifteen },
];
}
const single = records.map((stat) => ({
date: convertTsMsToLocalUnixTsInMs(stat.ts),
value: getStat(stat, type),
}));
return [{ label: type, data: single }];
}, [data, type]);
const stats = useMemo(
() =>
data?.stats
.map((stat) => {
return {
date: convertTsMsToLocalUnixTsInMs(stat.ts),
value: getStat(stat, type),
};
})
.reverse(),
[data]
);
return (
<div className={className}>
@@ -72,11 +50,8 @@ export const StatChart = ({
<Loader2 className="w-8 h-8 animate-spin" />
</div>
) : (
<InnerStatChart
type={type}
stats={seriesData.flatMap((s) => s.data)}
seriesData={seriesData}
/>
stats &&
stats.length > 0 && <InnerStatChart type={type} stats={stats} />
)}
</div>
);
@@ -89,13 +64,17 @@ const BYTES_PER_KB = 1024.0;
export const InnerStatChart = ({
type,
stats,
seriesData,
}: {
type: StatType;
stats: StatDatapoint[] | undefined;
seriesData?: { label: string; data: StatDatapoint[] }[];
}) => {
const { currentTheme } = useTheme();
const { theme: _theme } = useTheme();
const theme =
_theme === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: _theme;
const min = stats?.[0]?.date ?? 0;
const max = stats?.[stats.length - 1]?.date ?? 0;
@@ -117,13 +96,10 @@ export const InnerStatChart = ({
cursor: (_value?: Date) => false,
},
};
}, [min, max, diff]);
}, []);
// Determine the dynamic scaling for network-related types
const allValues = (seriesData ?? [{ data: stats ?? [] }]).flatMap((s) =>
s.data.map((d) => d.value)
);
const maxStatValue = Math.max(...(allValues.length ? allValues : [0]));
const maxStatValue = Math.max(...(stats?.map((d) => d.value) ?? [0]));
const { unit, maxUnitValue } = useMemo(() => {
if (type === "Network Ingress" || type === "Network Egress") {
@@ -137,13 +113,6 @@ export const InnerStatChart = ({
return { unit: "TB", maxUnitValue: BYTES_PER_GB * 1024 }; // Larger scale for high values
}
}
if (type === "Load Average") {
// Leave unitless; set max slightly above observed
return {
unit: "",
maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2,
};
}
return { unit: "", maxUnitValue: 100 }; // Default for CPU, memory, disk
}, [type, maxStatValue]);
@@ -151,8 +120,7 @@ export const InnerStatChart = ({
(): AxisOptions<StatDatapoint>[] => [
{
getValue: (datum) => datum.value,
elementType: type === "Load Average" ? "line" : "area",
stacked: type !== "Load Average",
elementType: "area",
min: 0,
max: maxUnitValue,
formatters: {
@@ -160,9 +128,7 @@ export const InnerStatChart = ({
<div className="text-lg font-mono">
{(type === "Network Ingress" || type === "Network Egress") && unit
? `${(value ?? 0) / (maxUnitValue / 1024)} ${unit}`
: type === "Load Average"
? `${(value ?? 0).toFixed(2)}`
: `${value?.toFixed(2)}%`}
: `${value?.toFixed(2)}%`}
</div>
),
},
@@ -170,31 +136,19 @@ export const InnerStatChart = ({
],
[type, maxUnitValue, unit]
);
if ((seriesData?.[0]?.data.length ?? 0) < 2) {
return (
<div className="w-full h-full flex gap-4 justify-center items-center">
<OctagonAlert className="w-6 h-6" />
<h1>Not enough data yet, choose a smaller interval.</h1>
</div>
);
}
return (
<Chart
options={{
data: seriesData ?? [{ label: type, data: stats ?? [] }],
data: [
{
label: type,
data: stats ?? [],
},
],
primaryAxis: timeAxis,
secondaryAxes: valueAxis,
defaultColors:
type === "Load Average"
? [
hex_color_by_intention("Good"),
hex_color_by_intention("Neutral"),
hex_color_by_intention("Unknown"),
]
: [getColor(type)],
dark: currentTheme === "dark",
defaultColors: [getColor(type)],
dark: theme === "dark",
padding: {
left: 10,
right: 10,
@@ -1,7 +1,13 @@
import { Section } from "@components/layouts";
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
import { Progress } from "@ui/progress";
import { Cpu, Database, Loader2, MemoryStick, Search } from "lucide-react";
import {
Cpu,
Database,
Loader2,
MemoryStick,
Search,
} from "lucide-react";
import { useLocalStorage, usePermissions, useRead } from "@lib/hooks";
import { Types } from "komodo_client";
import { DataTable, SortableHeader } from "@ui/data-table";
@@ -18,7 +24,6 @@ import {
} from "@ui/select";
import { DockerResourceLink, ShowHideButton } from "@components/util";
import { filterBySplit } from "@lib/utils";
import { useIsServerAvailable } from ".";
export const ServerStats = ({
id,
@@ -30,23 +35,17 @@ export const ServerStats = ({
const [interval, setInterval] = useStatsGranularity();
const { specific } = usePermissions({ type: "Server", id });
const isServerAvailable = useIsServerAvailable(id);
const stats = useRead(
"GetSystemStats",
{ server: id },
{
enabled: isServerAvailable,
refetchInterval: 10_000
}
{ refetchInterval: 10_000 }
).data;
const info = useRead("GetSystemInformation", { server: id }, { enabled: isServerAvailable }).data;
const info = useRead("GetSystemInformation", { server: id }).data;
// Get all the containers with stats
const containers = useRead("ListDockerContainers", {
server: id,
}, {
enabled: isServerAvailable
}).data?.filter((c) => c.stats);
const [showContainers, setShowContainers] = useLocalStorage(
"stats-show-container-table-v1",
@@ -121,7 +120,6 @@ export const ServerStats = ({
{/* Current Overview */}
<Section title="Current">
<div className="flex flex-col xl:flex-row gap-4">
<LOAD_AVERAGE id={id} stats={stats} />
<CPU stats={stats} />
<RAM stats={stats} />
<DISK stats={stats} />
@@ -337,11 +335,6 @@ export const ServerStats = ({
}
>
<div className="flex flex-col gap-8">
<StatChart
server_id={id}
type="Load Average"
className="w-full h-[250px]"
/>
<StatChart server_id={id} type="Cpu" className="w-full h-[250px]" />
<StatChart
server_id={id}
@@ -502,82 +495,6 @@ const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {
);
};
const LOAD_AVERAGE = ({
id,
stats,
}: {
id: string;
stats: Types.SystemStats | undefined;
}) => {
if (!stats?.load_average) return null;
const { one = 0, five = 0, fifteen = 0 } = stats.load_average || {};
const isServerAvailable = useIsServerAvailable(id);
const cores = useRead("GetSystemInformation", { server: id }, { enabled: isServerAvailable }).data?.core_count;
const pct = (load: number) =>
cores && cores > 0 ? Math.min((load / cores) * 100, 100) : undefined;
const textColor = (load: number) => {
const p = pct(load);
if (p === undefined) return "text-muted-foreground";
return p <= 50
? "text-green-600"
: p <= 80
? "text-yellow-600"
: "text-red-600";
};
return (
<Card className="w-full">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle>Load Average</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Current Load */}
<div className="space-y-2">
<div className="flex items-baseline justify-between">
<span
className={`text-3xl font-bold tabular-nums ${textColor(one)}`}
>
{one.toFixed(2)}
</span>
<span className="text-sm text-muted-foreground">
{cores && cores > 0
? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores`
: "N/A"}
</span>
</div>
<Progress value={pct(one) ?? 0} className="h-2" />
</div>
{/* Time Intervals */}
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4 text-sm">
{[
["1m", one],
["5m", five],
["15m", fifteen],
].map(([label, value]) => (
<div className="space-y-1" key={label as string}>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">{label}</span>
<span
className={`font-medium tabular-nums ${textColor(value as number)}`}
>
{(value as number).toFixed(2)}
</span>
</div>
<Progress value={pct(value as number) ?? 0} className="h-1" />
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
};
const RAM = ({ stats }: { stats: Types.SystemStats | undefined }) => {
const used = stats?.mem_used_gb;
const total = stats?.mem_total_gb;

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